12 KiB
Architecture & optimization notes
Current shape
- Backend: FastAPI, modular —
server.py(app + middleware) →routes/*.py(admin, tts, library, sources, stt, conversation, docker, settings) →core/*.py(audio, voice, config, validation, tts_helpers, …). - Provider status: TTS/STT discovery responses expose
configuredindependently fromavailable/health, with only bounded probe endpoint, HTTP status, and sanitized error details returned to the UI. - Frontend: feature code is split into manifest-driven lazy modules in
static/js/*.js, loaded in order bystatic/loader.js. Sections are HTML fragments instatic/sections/*.htmlfetched at runtime. Legacy modules still communicate through compatibility globals (window._voices,$,escHtml,toast,saveMeta,loadVoiceLibrary, …), while extracted public APIs are published underwindow.TTVC.
Already optimised
- GZip responses (
server.py) — ~75% smaller text transfer. - Static caching middleware (immutable for versioned JS/CSS).
- Native virtualization:
content-visibility:autoon long-list rows (voice library, online cards, cast cards). - Lazy images (
loading="lazy" decoding="async") across all big lists. - Rehearser memory cap: decoded PCM is kept to a sliding window (
REH_DECODE_WINDOW) around the playhead. - AudioContext instances are closed (iOS limit).
- PWA:
manifest.webmanifest+ network-firstsw.js(served from root for/scope), installable, offline shell. - Concurrency pool (
runPoolinutils.js) for bulk network ops. - Smoke tests: Playwright (
tests/, desktop + iPhone profiles).
Build tooling (opt-in) — DONE
npm run minify → scripts/minify.mjs concatenates the feature modules in loader order into static/dist/main.min.js and minifies whitespace/syntax only (identifiers kept — inline HTML onclick="navTo(...)" and cross-file refs rely on global names). loader.js loads this single bundle when window.APP_USE_BUNDLE === true (or ?bundle=1), else falls back to per-file loading — so editing static/js/* stays live by default. Verified end-to-end by tests/bundle.spec.js. Rebuild the bundle after JS edits if you ship with it enabled.
Staged migration plan (the two big refactors — do incrementally, behind the smoke tests)
1. Decouple globals → ES modules
The global scope + monkey-patching is the root cause of the recurring "wiring/timing" bugs (controls bound before the section mounts, etc.). Migrate one file at a time:
- Add
"type": "module"loading path; keep a compat shim that re-exposes the few cross-module symbols onwindowduring transition. - Convert leaf modules first (pure Rehearser/Audiobook helpers are now privately scoped), exporting explicitly and importing where used.
- Replace the
renderVoiceListmonkey-patch with a normal function + anonRenderhook. - Switch
loader.jsto a single<script type="module">entry that imports the graph;esbuild --bundlethen produces one minified, tree-shaken file.
2. Split rehearser.js (was 196 KB) — RESPONSIBILITY SPLIT COMPLETE
Done incrementally as global-scope ordered files (no ES-module change needed) — each loaded before rehearser.js in loader.js + added to the bundle list in scripts/minify.mjs, verified by tests/rehearser.spec.js (parse → cast) and the smoke suite:
- ✅
rehearser-parse.js—parseScript,detectCharacters(pure; only call-timeSPEAKER_COLORS). - ✅
rehearser-cast.js—renderCastList, sort/filter, online voice picker, and character research. - ✅
rehearser-stage.js— stage font/cast sidebar controls and cast-strip rendering. - ✅
rehearser-playback.js— WebAudio playback, transport state, bounded decoded-audio prefetch, and persistent line-audio cache/synthesis controls. - ✅
rehearser-io.js— rehearsal serialization, library save/load, autosave, session/file export, and screenplay/PDF imports. - ✅
rehearser-recording.js— microphone recording overlay, level meter, and recorded-line controls. - ✅
rehearser-training-ui.js— training-mode state, cue playback, transcription, recording, and phase controls. - ✅
rehearser-voice-picker.js— voice preview state, local matching, fish.audio search/import, and candidate assignment; DOM event registration remains inrehearser-cast.js. - ✅
rehearser-cast.js— cast view, full cast-card rendering, sort/filter state handlers, and delegated cast toolbar wiring. - ✅
rehearser-voice-design.js— character development, backend selection, and backend-refresh synchronization. - ✅
audiobook-segmentation.js— quote segmentation, conservative turn-taking, and dialogue-piece validation. - ✅
audiobook-parse.js— dialogue detection, dehyphenation, and speaker-heuristic constants. - ✅
audiobook-cast.js— cast Markdown export, legacy page-stamping migration, deterministic speaker/sentence/duplicate/alias normalization helpers, and the live cast-view renderer. - ✅
audiobook-pipeline.js— LLM casting, unknown-speaker recast, automatic repair, and cast-view orchestration. - ✅
audiobook-preview-export.js— editable cast preview, Rehearser handoff, chapter-aware export, and export browsing. - ✅ Stateful Audiobook modules use private scopes with explicit legacy exports; their public APIs are namespace-tested.
- ✅ Rehearser playback uses a private scope with explicit transport/cache compatibility exports and a namespace-tested API.
- ✅ Rehearser I/O uses a private scope with explicit serialization/library/import compatibility exports.
- ✅ Rehearser recording uses a private scope with explicit microphone/overlay compatibility exports.
- ✅ Rehearser core uses a private scope with explicit state, pagination, and cast-management compatibility exports.
- ✅ Audiobook core uses a private scope with explicit reader-side state and handoff compatibility exports.
- ✅ Reader uses a private scope with explicit document, playback, and search compatibility exports.
- ✅ Rehearser library persistence uses a private scope with explicit SQLite/IndexedDB compatibility exports.
- ✅ Audiobook network transport uses a private scope with explicit timeout/stream compatibility exports.
- ✅ Rehearser stage controls use a private scope with explicit scaling and cast-sidebar compatibility exports.
- ✅ Rehearser voice-picker uses a private scope with explicit preview, search, and assignment compatibility exports.
- ✅ Rehearser voice-design uses a private scope with explicit development and backend-refresh compatibility exports.
- ✅ Rehearser cast rendering uses a private scope with explicit view/filter/sort compatibility exports.
- ✅ Studio navigation and DOM-borrowing coordination use a private scope with explicit compatibility exports.
- ✅ Character library persistence and rendering use a private scope with explicit identity/image compatibility exports.
- ✅ Settings state and backend configuration use a private scope with live compatibility accessors and a namespace API.
- ✅ TTS preview and performance-history helpers use a private scope with explicit preview/history compatibility exports.
- ✅ Voice-clone audio, recording, and design controls use a private scope with explicit compatibility exports.
- ✅ Voice-source discovery and import helpers use a private scope with explicit source-list/import compatibility exports.
- ✅ TTS routing configuration and routing-log UI use a private scope with live route/sound compatibility accessors.
- ✅ Engine browser, custom-card, and local-container helpers use a private scope with an explicit namespace API.
- ✅ Combined library production rendering and Reader/Rehearser handoffs use a private scope with an explicit namespace API.
- ✅ Integration endpoint and API-snippet helpers use a private scope with explicit voice/endpoint compatibility exports.
- ✅ AI-backend section interactions and snippet initialization use a private scope with an explicit namespace API.
- ✅ STT backend, recording, voice-loading, and STT-to-TTS helpers use a private scope with live compatibility accessors.
- ✅ Generation audio/history/playlist helpers use a private scope with explicit compatibility exports and a namespace API.
- ✅ Voice-library loading, rendering, benchmarking, and import workflows use a private scope with live compatibility exports.
- ✅ Character-sheet extraction and prompt helpers use a private scope with explicit compatibility exports and a namespace API.
- ✅ Character-library rendering, detail, avatar, and generation actions use a private scope with compatibility exports and a namespace API.
- ✅ Voice-inspector selection state uses a private scope with a compatibility export and namespace API.
- ✅ Shared utility helpers use a private scope with explicit compatibility exports and a
window.TTVC.utilsAPI. - ✅ Character-table metadata and column/completeness helpers use a private scope with a namespace API.
- ✅ Seed-finder generation and caching helpers use a private scope with compatibility exports and a namespace API.
- ✅ Audiobook cast serialization and speaker normalization use a private scope with the existing cast namespace API.
- ✅ SillyTavern character-card import/export helpers use a private scope with a namespace API.
- ✅ i18n dictionaries and language state use a private scope with compatibility exports and a namespace API.
- ✅ Conversation settings/log state and About helpers use a private scope with compatibility exports and a namespace API.
- ✅ All remaining application frontend modules are either privately scoped or intentionally stateless bootstrap/cache IIFEs;
legacy globals are explicit compatibility exports and new integrations use
window.TTVCnamespaces. - ✅
static/css/resource-diagnostics.css— isolated Performance/resource diagnostic panel styles. - ✅
static/css/background-jobs.css— isolated background-job history and retry-panel styles. - ✅
static/css/voice-design.css— isolated Voice Design form, samples, and preview-match styles. - ✅
static/css/rehearser.css— isolated Rehearser modal, cast, stage, training, bookshelf, and script-view styles. - ✅
static/css/audiobook.css— isolated Reader/Audiobook controls, casting feed, page layout, character overlays, and review styles. - ✅
rehearser-train.js— transcript normalization, LCS comparison, and word-level training feedback; microphone/state wiring remains inrehearser.js. - ✅
rehearser-library.js— SQLite persistence helpers and IndexedDB migration; library rendering remains ordered with the main module because it owns the phase UI.
Pattern (proven): extract a cohesive block whose only cross-file references resolve at call time (event handlers), not at module load. Top-level const rehState and top-level addEventListener registrations must stay ordered — extract those last and load them after their dependencies. Re-run npx playwright test after each extraction.
Remaining recommendations
- Complete the eventual ES-module/global-compatibility migration one leaf at a time; current behavior intentionally keeps
legacy globals available while the
window.TTVCsurface is adopted. - Complete the eventual ES-module/global-compatibility migration one leaf at a time; current behavior intentionally keeps
legacy globals available while the
window.TTVCsurface is adopted. - Replace remaining silent
.catch(() => {})withlogErr(context, e)(helper added inutils.js). - Full a11y sweep (icon-button
aria-labels, focus traps, keyboard nav increateSearchablePicker) — run theweb-design-guidelinesreview. - If ever exposed beyond localhost: add an auth token + rate limiting (SSRF is already guarded in
core/validation.py).