Skip to content

Commit 54dea5d

Browse files
committed
Add onloaderror retries
1 parent 4e07e73 commit 54dea5d

File tree

3 files changed

+162
-60
lines changed

3 files changed

+162
-60
lines changed

playwright.config.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ export default defineConfig({
3535
screenshot: 'only-on-failure',
3636
},
3737

38+
/* Run your local dev server before starting the tests */
39+
webServer: {
40+
command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev',
41+
url: 'http://localhost:3003',
42+
reuseExistingServer: !process.env.CI,
43+
timeout: 120 * 1000,
44+
},
45+
3846
/* Configure projects for major browsers */
3947
projects: [
4048
{
@@ -72,12 +80,4 @@ export default defineConfig({
7280
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
7381
// },
7482
],
75-
76-
/* Run your local dev server before starting the tests */
77-
webServer: {
78-
command: 'npm run build && npm run start',
79-
url: 'http://localhost:3003',
80-
reuseExistingServer: !process.env.CI,
81-
timeout: 120 * 1000,
82-
},
8383
});

src/app/api/nlp/route.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,60 @@ const splitIntoSentences = (text: string): string[] => {
4848
};
4949

5050
export async function POST(req: NextRequest) {
51+
// First check if the request body is empty
52+
const contentLength = req.headers.get('content-length');
53+
if (!contentLength || parseInt(contentLength) === 0) {
54+
return NextResponse.json(
55+
{ error: 'Request body is empty' },
56+
{ status: 400 }
57+
);
58+
}
59+
60+
// Check content type
61+
const contentType = req.headers.get('content-type');
62+
if (!contentType?.includes('application/json')) {
63+
return NextResponse.json(
64+
{ error: 'Content-Type must be application/json' },
65+
{ status: 400 }
66+
);
67+
}
68+
5169
try {
52-
const { text } = await req.json();
53-
if (!text) {
54-
return NextResponse.json({ error: 'No text provided' }, { status: 400 });
70+
// Get the raw body text first to validate it's not empty
71+
const rawBody = await req.text();
72+
if (!rawBody?.trim()) {
73+
return NextResponse.json(
74+
{ error: 'Request body is empty' },
75+
{ status: 400 }
76+
);
77+
}
78+
79+
// Try to parse the JSON
80+
let body;
81+
try {
82+
body = JSON.parse(rawBody);
83+
} catch (e) {
84+
console.error('JSON parse error:', e);
85+
return NextResponse.json(
86+
{ error: 'Invalid JSON format' },
87+
{ status: 400 }
88+
);
89+
}
90+
91+
// Validate the parsed body has the required text field
92+
if (!body || typeof body !== 'object') {
93+
return NextResponse.json(
94+
{ error: 'Invalid request body format' },
95+
{ status: 400 }
96+
);
97+
}
98+
99+
const { text } = body;
100+
if (!text || typeof text !== 'string') {
101+
return NextResponse.json(
102+
{ error: 'Missing or invalid text field' },
103+
{ status: 400 }
104+
);
55105
}
56106

57107
if (text.length <= MAX_BLOCK_LENGTH) {
@@ -65,6 +115,9 @@ export async function POST(req: NextRequest) {
65115
return NextResponse.json({ sentences });
66116
} catch (error) {
67117
console.error('Error processing text:', error);
68-
return NextResponse.json({ error: 'Failed to process text' }, { status: 500 });
118+
return NextResponse.json(
119+
{ error: 'Internal server error' },
120+
{ status: 500 }
121+
);
69122
}
70123
}

src/contexts/TTSContext.tsx

Lines changed: 97 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
150150
* @returns {Promise<string[]>} Array of processed sentences
151151
*/
152152
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
153-
if (text.length === 0) {
153+
if (text.length < 1) {
154154
return [];
155155
}
156156

@@ -524,58 +524,107 @@ export function TTSProvider({ children }: { children: ReactNode }) {
524524
return;
525525
}
526526

527-
try {
528-
// Get the processed audio data URI directly from processSentence
529-
const audioDataUri = await processSentence(sentence);
530-
if (!audioDataUri) {
531-
throw new Error('No audio data generated');
532-
}
527+
const MAX_RETRIES = 3;
528+
const INITIAL_RETRY_DELAY = 1000; // 1 second
533529

534-
// Force unload any previous Howl instance to free up resources
535-
if (activeHowl) {
536-
activeHowl.unload();
537-
}
530+
const createHowl = async (retryCount = 0): Promise<Howl | null> => {
531+
try {
532+
// Get the processed audio data URI directly from processSentence
533+
const audioDataUri = await processSentence(sentence);
534+
if (!audioDataUri) {
535+
throw new Error('No audio data generated');
536+
}
538537

539-
const howl = new Howl({
540-
src: [audioDataUri],
541-
format: ['mp3'],
542-
html5: true,
543-
preload: true,
544-
pool: 5,
545-
onplay: () => {
546-
setIsProcessing(false);
547-
if ('mediaSession' in navigator) {
548-
navigator.mediaSession.playbackState = 'playing';
549-
}
550-
},
551-
onpause: () => {
552-
if ('mediaSession' in navigator) {
553-
navigator.mediaSession.playbackState = 'paused';
554-
}
555-
},
556-
onend: () => {
557-
howl.unload();
558-
setActiveHowl(null);
559-
if (isPlaying) {
560-
advance();
561-
}
562-
},
563-
onloaderror: (id, error) => {
564-
console.warn('Error loading audio:', error);
565-
setIsProcessing(false);
566-
setActiveHowl(null);
567-
howl.unload();
568-
setIsPlaying(false);
569-
},
570-
onstop: () => {
571-
setIsProcessing(false);
572-
howl.unload();
538+
// Force unload any previous Howl instance to free up resources
539+
if (activeHowl) {
540+
activeHowl.unload();
573541
}
574-
});
575542

576-
setActiveHowl(howl);
577-
return howl;
543+
return new Howl({
544+
src: [audioDataUri],
545+
format: ['mp3', 'mpeg'],
546+
html5: true,
547+
preload: true,
548+
pool: 5,
549+
onplay: () => {
550+
setIsProcessing(false);
551+
if ('mediaSession' in navigator) {
552+
navigator.mediaSession.playbackState = 'playing';
553+
}
554+
},
555+
onplayerror: function(this: Howl, error) {
556+
console.warn('Howl playback error:', error);
557+
// Try to recover by forcing HTML5 audio mode
558+
if (this.state() === 'loaded') {
559+
this.unload();
560+
this.once('load', () => {
561+
this.play();
562+
});
563+
this.load();
564+
}
565+
},
566+
onloaderror: async function(this: Howl, error) {
567+
console.warn(`Error loading audio (attempt ${retryCount + 1}/${MAX_RETRIES}):`, error);
568+
569+
if (retryCount < MAX_RETRIES) {
570+
// Calculate exponential backoff delay
571+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
572+
console.log(`Retrying in ${delay}ms...`);
573+
574+
// Wait for the delay
575+
await new Promise(resolve => setTimeout(resolve, delay));
576+
577+
// Try to create a new Howl instance
578+
const retryHowl = await createHowl(retryCount + 1);
579+
if (retryHowl) {
580+
setActiveHowl(retryHowl);
581+
retryHowl.play();
582+
}
583+
} else {
584+
console.error('Max retries reached, moving to next sentence');
585+
setIsProcessing(false);
586+
setActiveHowl(null);
587+
this.unload();
588+
setIsPlaying(false);
589+
590+
toast.error('Audio loading failed after retries. Moving to next sentence...', {
591+
id: 'audio-load-error',
592+
style: {
593+
background: 'var(--background)',
594+
color: 'var(--accent)',
595+
},
596+
duration: 2000,
597+
});
598+
599+
advance();
600+
}
601+
},
602+
onend: function(this: Howl) {
603+
this.unload();
604+
setActiveHowl(null);
605+
if (isPlaying) {
606+
advance();
607+
}
608+
},
609+
onstop: function(this: Howl) {
610+
setIsProcessing(false);
611+
this.unload();
612+
}
613+
});
614+
} catch (error) {
615+
console.error('Error creating Howl instance:', error);
616+
return null;
617+
}
618+
};
619+
620+
try {
621+
const howl = await createHowl();
622+
if (howl) {
623+
setActiveHowl(howl);
624+
return howl;
625+
}
578626

627+
throw new Error('Failed to create Howl instance');
579628
} catch (error) {
580629
console.error('Error playing TTS:', error);
581630
setActiveHowl(null);

0 commit comments

Comments
 (0)