From 09944ec4e43147ad2a72eefcf499a4d85d8edc43 Mon Sep 17 00:00:00 2001 From: Richard R Date: Mon, 6 Apr 2026 11:28:18 -0600 Subject: [PATCH] refactor(audiobooks,tts,config): tighten abort handling, progress accounting, equality checks, and voice fetch timeout Improve robustness across audiobook pipeline, TTS catalog, and preferences comparison. - audiobooks: centralize abort creation with createAudiobookAbortError and replace repeated ad-hoc throws with it; avoid reprocessing non-completed chapters when collecting existing indices; update progress accounting by advancing processedLength and emitting onProgress after saving a chapter. - tts: add a 10s AbortController timeout when fetching custom OpenAI voices, wire the signal to fetch, and ensure the timeout is cleared on success or failure; fall back cleanly when endpoint doesn't support voices. - config: harden deepEqual by checking mismatched array vs object shapes early to avoid incorrect comparisons. No user-visible behavior changes besides improved cancellation, progress reporting, and more resilient remote voice discovery. --- src/lib/client/audiobooks/pipeline.ts | 27 +++++++++++++------------- src/lib/client/config/preferences.ts | 3 +++ src/lib/shared/tts-provider-catalog.ts | 9 +++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) 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; }