diff --git a/src/lib/client/audiobooks/pipeline.ts b/src/lib/client/audiobooks/pipeline.ts index 295cef4..56c9def 100644 --- a/src/lib/client/audiobooks/pipeline.ts +++ b/src/lib/client/audiobooks/pipeline.ts @@ -86,6 +86,12 @@ function isAbortLikeError(error: unknown): boolean { return error instanceof Error && (error.name === 'AbortError' || error.message.includes('cancelled')); } +function createAudiobookAbortError(): Error { + const error = new Error('Audiobook generation cancelled'); + error.name = 'AbortError'; + return error; +} + export async function runAudiobookGeneration({ adapter, apiKey, @@ -120,7 +126,9 @@ export async function runAudiobookGeneration({ const existingData = await getAudiobookStatus(bookId); if (existingData.chapters && existingData.chapters.length > 0) { for (const chapter of existingData.chapters) { - existingIndices.add(chapter.index); + if (chapter.status === 'completed') { + existingIndices.add(chapter.index); + } } } } catch (error) { @@ -130,10 +138,7 @@ export async function runAudiobookGeneration({ for (const chapter of chapters) { if (signal?.aborted) { - if (bookId) { - return bookId; - } - throw new Error('Audiobook generation cancelled'); + throw createAudiobookAbortError(); } const trimmedText = chapter.text.trim(); @@ -167,10 +172,7 @@ export async function runAudiobookGeneration({ ); if (signal?.aborted) { - if (bookId) { - return bookId; - } - throw new Error('Audiobook generation cancelled'); + throw createAudiobookAbortError(); } if (!bookId) { @@ -186,10 +188,7 @@ export async function runAudiobookGeneration({ onProgress((processedLength / totalLength) * 100); } catch (error) { if (isAbortLikeError(error)) { - if (bookId) { - return bookId; - } - throw new Error('Audiobook generation cancelled'); + throw createAudiobookAbortError(); } console.error('Error processing chapter:', error); @@ -200,6 +199,8 @@ export async function runAudiobookGeneration({ bookId, format: effectiveFormat, }); + processedLength += trimmedText.length; + onProgress((processedLength / totalLength) * 100); } } diff --git a/src/lib/client/config/preferences.ts b/src/lib/client/config/preferences.ts index 261a000..5091078 100644 --- a/src/lib/client/config/preferences.ts +++ b/src/lib/client/config/preferences.ts @@ -8,6 +8,9 @@ function isObjectLike(value: unknown): value is Record { function deepEqual(left: unknown, right: unknown): boolean { if (left === right) return true; + if (Array.isArray(left) !== Array.isArray(right)) return false; + if (isObjectLike(left) !== isObjectLike(right)) return false; + if (Array.isArray(left) && Array.isArray(right)) { if (left.length !== right.length) return false; for (let i = 0; i < left.length; i += 1) { diff --git a/src/lib/shared/tts-provider-catalog.ts b/src/lib/shared/tts-provider-catalog.ts index 4713b34..fd26aff 100644 --- a/src/lib/shared/tts-provider-catalog.ts +++ b/src/lib/shared/tts-provider-catalog.ts @@ -205,15 +205,23 @@ async function fetchDeepinfraVoices(apiKey: string): Promise { } async function fetchCustomOpenAiVoices(baseUrl: string, apiKey: string): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + }, 10_000); + try { const normalizedBaseUrl = baseUrl.replace(/\/+$/, ''); const response = await fetch(`${normalizedBaseUrl}/audio/voices`, { + signal: controller.signal, headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, }); + clearTimeout(timeoutId); + if (!response.ok) { return null; } @@ -221,6 +229,7 @@ async function fetchCustomOpenAiVoices(baseUrl: string, apiKey: string): Promise const data = await response.json(); return Array.isArray(data.voices) ? data.voices : null; } catch { + clearTimeout(timeoutId); console.log('Custom endpoint does not support voices, using defaults'); return null; }