Add onloaderror retries
This commit is contained in:
parent
4e07e737f0
commit
54dea5da36
3 changed files with 163 additions and 61 deletions
|
|
@ -35,6 +35,14 @@ export default defineConfig({
|
||||||
screenshot: 'only-on-failure',
|
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 */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|
@ -72,12 +80,4 @@ export default defineConfig({
|
||||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
// 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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,60 @@ const splitIntoSentences = (text: string): string[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
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 {
|
try {
|
||||||
const { text } = await req.json();
|
// Get the raw body text first to validate it's not empty
|
||||||
if (!text) {
|
const rawBody = await req.text();
|
||||||
return NextResponse.json({ error: 'No text provided' }, { status: 400 });
|
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) {
|
if (text.length <= MAX_BLOCK_LENGTH) {
|
||||||
|
|
@ -65,6 +115,9 @@ export async function POST(req: NextRequest) {
|
||||||
return NextResponse.json({ sentences });
|
return NextResponse.json({ sentences });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing text:', 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 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
* @returns {Promise<string[]>} Array of processed sentences
|
* @returns {Promise<string[]>} Array of processed sentences
|
||||||
*/
|
*/
|
||||||
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
|
const processTextToSentences = useCallback(async (text: string): Promise<string[]> => {
|
||||||
if (text.length === 0) {
|
if (text.length < 1) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -524,58 +524,107 @@ export function TTSProvider({ children }: { children: ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const MAX_RETRIES = 3;
|
||||||
// Get the processed audio data URI directly from processSentence
|
const INITIAL_RETRY_DELAY = 1000; // 1 second
|
||||||
const audioDataUri = await processSentence(sentence);
|
|
||||||
if (!audioDataUri) {
|
|
||||||
throw new Error('No audio data generated');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force unload any previous Howl instance to free up resources
|
const createHowl = async (retryCount = 0): Promise<Howl | null> => {
|
||||||
if (activeHowl) {
|
try {
|
||||||
activeHowl.unload();
|
// Get the processed audio data URI directly from processSentence
|
||||||
}
|
const audioDataUri = await processSentence(sentence);
|
||||||
|
if (!audioDataUri) {
|
||||||
const howl = new Howl({
|
throw new Error('No audio data generated');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
setActiveHowl(howl);
|
// Force unload any previous Howl instance to free up resources
|
||||||
return howl;
|
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) {
|
} catch (error) {
|
||||||
console.error('Error playing TTS:', error);
|
console.error('Error playing TTS:', error);
|
||||||
setActiveHowl(null);
|
setActiveHowl(null);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue