openreader/src/lib/client/audiobooks/adapters/epub.ts
Richard R d6ae2baa6f refactor(tts,config,audiobook): centralize TTS provider catalog and extract audiobook pipeline
- 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.
2026-04-04 17:53:57 -06:00

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;
},
};
}