- Introduce a shared tts-provider-catalog to centralize provider/model/voice defaults
and feature flags (supportsTtsInstructions, providerSupportsCustomModel, resolveProviderModels,
getDefaultVoices, etc.). Replace ad-hoc provider logic across server and client with catalog calls.
- Replace inline voice/model UI logic in SettingsModal, AudiobookExportModal, TTSContext,
generate server code, and voice hook to use catalog helpers (resolveTtsSettingsViewModel,
supportsTtsInstructions, getDefaultVoices).
- Extract audiobook generation responsibilities into a small client-side pipeline:
- Add createEpubAudiobookSourceAdapter and createPdfAudiobookSourceAdapter adapter modules
to prepare chapters from EPUB/PDF sources.
- Add runAudiobookGeneration and regenerateAudiobookChapter pipeline functions to handle
chapter preparation, progress tracking, request header construction, and retry/abort behavior.
- Wire adapters/pipeline into EPUBContext and PDFContext to simplify and centralize audiobook flow.
- Move config preference helpers out of ConfigContext into dedicated client modules:
- buildSyncedPreferencePatch (lib/client/config/preferences.ts)
- applyConfigUpdate and getVoicePreferenceKey (lib/client/config/updates.ts)
- Use these helpers in ConfigContext to keep update logic small and testable.
- Add TTS settings view model resolver (lib/client/settings/tts-settings.ts) and tests
for the tts-provider catalog and config helpers (tests/unit/tts-provider-catalog.spec.ts).
- Minor usages updated: AudiobookExportModal, SettingsModal, useVoiceManagement, TTSContext,
server tts generation, and various context files to consume the new modules.
Why: Reduce duplication, improve testability, and separate concerns for provider metadata,
UI view-model logic, and audiobook generation flow. Breaking changes: none.
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
import type { NavItem } from 'epubjs';
|
|
|
|
import type { AudiobookSourceAdapter, PreparedAudiobookChapter } from '@/lib/client/audiobooks/pipeline';
|
|
|
|
export interface ExtractedEpubSection {
|
|
text: string;
|
|
href: string;
|
|
}
|
|
|
|
interface EpubAudiobookAdapterOptions {
|
|
extractBookText: () => Promise<ExtractedEpubSection[]>;
|
|
getTocItems: () => NavItem[];
|
|
}
|
|
|
|
async function buildPreparedEpubChapters({
|
|
extractBookText,
|
|
getTocItems,
|
|
}: EpubAudiobookAdapterOptions): Promise<PreparedAudiobookChapter[]> {
|
|
const sections = await extractBookText();
|
|
const chapters = getTocItems();
|
|
const sectionTitleMap = new Map<string, string>();
|
|
|
|
for (const chapter of chapters) {
|
|
if (!chapter.href) continue;
|
|
const chapterBaseHref = chapter.href.split('#')[0];
|
|
const chapterTitle = chapter.label.trim();
|
|
|
|
for (const section of sections) {
|
|
const sectionBaseHref = section.href.split('#')[0];
|
|
if (section.href === chapter.href || sectionBaseHref === chapterBaseHref) {
|
|
sectionTitleMap.set(section.href, chapterTitle);
|
|
}
|
|
}
|
|
}
|
|
|
|
return sections.map((section, index) => ({
|
|
index,
|
|
title: sectionTitleMap.get(section.href) || `Chapter ${index + 1}`,
|
|
text: section.text,
|
|
}));
|
|
}
|
|
|
|
export function createEpubAudiobookSourceAdapter(options: EpubAudiobookAdapterOptions): AudiobookSourceAdapter {
|
|
return {
|
|
noContentMessage: 'No text content found in book',
|
|
noAudioGeneratedMessage: 'No audio was generated from the book content',
|
|
prepareChapters: async () => buildPreparedEpubChapters(options),
|
|
prepareChapter: async (chapterIndex: number) => {
|
|
const chapters = await buildPreparedEpubChapters(options);
|
|
const chapter = chapters[chapterIndex];
|
|
if (!chapter) {
|
|
throw new Error('Invalid chapter index');
|
|
}
|
|
return chapter;
|
|
},
|
|
};
|
|
}
|