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.
This commit is contained in:
parent
7d9d8de967
commit
09944ec4e4
3 changed files with 26 additions and 13 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -205,15 +205,23 @@ async function fetchDeepinfraVoices(apiKey: string): Promise<string[]> {
|
|||
}
|
||||
|
||||
async function fetchCustomOpenAiVoices(baseUrl: string, apiKey: string): Promise<string[] | null> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue