From 54dea5da36abbe072f4ae0c5f5b13bb0aad67020 Mon Sep 17 00:00:00 2001 From: Richard Roberson Date: Sun, 2 Mar 2025 22:13:08 -0700 Subject: [PATCH] Add onloaderror retries --- playwright.config.ts | 16 ++-- src/app/api/nlp/route.ts | 61 ++++++++++++++- src/contexts/TTSContext.tsx | 147 ++++++++++++++++++++++++------------ 3 files changed, 163 insertions(+), 61 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 39b0863..1b5edfb 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -35,6 +35,14 @@ export default defineConfig({ screenshot: 'only-on-failure', }, + /* Run your local dev server before starting the tests */ + webServer: { + command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev', + url: 'http://localhost:3003', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, + /* Configure projects for major browsers */ projects: [ { @@ -72,12 +80,4 @@ export default defineConfig({ // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run build && npm run start', - url: 'http://localhost:3003', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - }, }); diff --git a/src/app/api/nlp/route.ts b/src/app/api/nlp/route.ts index 95d93a1..2bea22f 100644 --- a/src/app/api/nlp/route.ts +++ b/src/app/api/nlp/route.ts @@ -48,10 +48,60 @@ const splitIntoSentences = (text: string): string[] => { }; export async function POST(req: NextRequest) { + // First check if the request body is empty + const contentLength = req.headers.get('content-length'); + if (!contentLength || parseInt(contentLength) === 0) { + return NextResponse.json( + { error: 'Request body is empty' }, + { status: 400 } + ); + } + + // Check content type + const contentType = req.headers.get('content-type'); + if (!contentType?.includes('application/json')) { + return NextResponse.json( + { error: 'Content-Type must be application/json' }, + { status: 400 } + ); + } + try { - const { text } = await req.json(); - if (!text) { - return NextResponse.json({ error: 'No text provided' }, { status: 400 }); + // Get the raw body text first to validate it's not empty + const rawBody = await req.text(); + if (!rawBody?.trim()) { + return NextResponse.json( + { error: 'Request body is empty' }, + { status: 400 } + ); + } + + // Try to parse the JSON + let body; + try { + body = JSON.parse(rawBody); + } catch (e) { + console.error('JSON parse error:', e); + return NextResponse.json( + { error: 'Invalid JSON format' }, + { status: 400 } + ); + } + + // Validate the parsed body has the required text field + if (!body || typeof body !== 'object') { + return NextResponse.json( + { error: 'Invalid request body format' }, + { status: 400 } + ); + } + + const { text } = body; + if (!text || typeof text !== 'string') { + return NextResponse.json( + { error: 'Missing or invalid text field' }, + { status: 400 } + ); } if (text.length <= MAX_BLOCK_LENGTH) { @@ -65,6 +115,9 @@ export async function POST(req: NextRequest) { return NextResponse.json({ sentences }); } catch (error) { console.error('Error processing text:', error); - return NextResponse.json({ error: 'Failed to process text' }, { status: 500 }); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); } } diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index 7044586..d85414b 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -150,7 +150,7 @@ export function TTSProvider({ children }: { children: ReactNode }) { * @returns {Promise} Array of processed sentences */ const processTextToSentences = useCallback(async (text: string): Promise => { - if (text.length === 0) { + if (text.length < 1) { return []; } @@ -524,58 +524,107 @@ export function TTSProvider({ children }: { children: ReactNode }) { return; } - try { - // Get the processed audio data URI directly from processSentence - const audioDataUri = await processSentence(sentence); - if (!audioDataUri) { - throw new Error('No audio data generated'); - } + const MAX_RETRIES = 3; + const INITIAL_RETRY_DELAY = 1000; // 1 second - // Force unload any previous Howl instance to free up resources - if (activeHowl) { - activeHowl.unload(); - } - - const howl = new Howl({ - src: [audioDataUri], - format: ['mp3'], - html5: true, - preload: true, - pool: 5, - onplay: () => { - setIsProcessing(false); - if ('mediaSession' in navigator) { - navigator.mediaSession.playbackState = 'playing'; - } - }, - onpause: () => { - if ('mediaSession' in navigator) { - navigator.mediaSession.playbackState = 'paused'; - } - }, - onend: () => { - howl.unload(); - setActiveHowl(null); - if (isPlaying) { - advance(); - } - }, - onloaderror: (id, error) => { - console.warn('Error loading audio:', error); - setIsProcessing(false); - setActiveHowl(null); - howl.unload(); - setIsPlaying(false); - }, - onstop: () => { - setIsProcessing(false); - howl.unload(); + const createHowl = async (retryCount = 0): Promise => { + try { + // Get the processed audio data URI directly from processSentence + const audioDataUri = await processSentence(sentence); + if (!audioDataUri) { + throw new Error('No audio data generated'); } - }); - setActiveHowl(howl); - return howl; + // Force unload any previous Howl instance to free up resources + if (activeHowl) { + activeHowl.unload(); + } + return new Howl({ + src: [audioDataUri], + format: ['mp3', 'mpeg'], + html5: true, + preload: true, + pool: 5, + onplay: () => { + setIsProcessing(false); + if ('mediaSession' in navigator) { + navigator.mediaSession.playbackState = 'playing'; + } + }, + onplayerror: function(this: Howl, error) { + console.warn('Howl playback error:', error); + // Try to recover by forcing HTML5 audio mode + if (this.state() === 'loaded') { + this.unload(); + this.once('load', () => { + this.play(); + }); + this.load(); + } + }, + onloaderror: async function(this: Howl, error) { + console.warn(`Error loading audio (attempt ${retryCount + 1}/${MAX_RETRIES}):`, error); + + if (retryCount < MAX_RETRIES) { + // Calculate exponential backoff delay + const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount); + console.log(`Retrying in ${delay}ms...`); + + // Wait for the delay + await new Promise(resolve => setTimeout(resolve, delay)); + + // Try to create a new Howl instance + const retryHowl = await createHowl(retryCount + 1); + if (retryHowl) { + setActiveHowl(retryHowl); + retryHowl.play(); + } + } else { + console.error('Max retries reached, moving to next sentence'); + setIsProcessing(false); + setActiveHowl(null); + this.unload(); + setIsPlaying(false); + + toast.error('Audio loading failed after retries. Moving to next sentence...', { + id: 'audio-load-error', + style: { + background: 'var(--background)', + color: 'var(--accent)', + }, + duration: 2000, + }); + + advance(); + } + }, + onend: function(this: Howl) { + this.unload(); + setActiveHowl(null); + if (isPlaying) { + advance(); + } + }, + onstop: function(this: Howl) { + setIsProcessing(false); + this.unload(); + } + }); + } catch (error) { + console.error('Error creating Howl instance:', error); + return null; + } + }; + + try { + const howl = await createHowl(); + if (howl) { + setActiveHowl(howl); + return howl; + } + + throw new Error('Failed to create Howl instance'); } catch (error) { console.error('Error playing TTS:', error); setActiveHowl(null);