tts-voice-creator-clone-and.../docs/ARCHITECTURE.md

6.2 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 configured independently from available/health, with only bounded probe endpoint, HTTP status, and sanitized error details returned to the UI.
  • Frontend: ~15k lines of plain global-scope JS in static/js/*.js, loaded in order by static/loader.js (no bundler/build step). Sections are HTML fragments in static/sections/*.html fetched at runtime. Modules communicate via globals (window._voices, $, escHtml, toast, saveMeta, loadVoiceLibrary, …) and some runtime monkey-patching (e.g. renderVoiceList is reassigned in voice-library.js).

Already optimised

  • GZip responses (server.py) — ~75% smaller text transfer.
  • Static caching middleware (immutable for versioned JS/CSS).
  • Native virtualization: content-visibility:auto on 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-first sw.js (served from root for / scope), installable, offline shell.
  • Concurrency pool (runPool in utils.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:

  1. Add "type": "module" loading path; keep a compat shim that re-exposes the few cross-module symbols on window during transition.
  2. Convert leaf modules first (utils, settings, voice-picker), exporting explicitly and importing where used.
  3. Replace the renderVoiceList monkey-patch with a normal function + an onRender hook.
  4. Switch loader.js to a single <script type="module"> entry that imports the graph; esbuild --bundle then produces one minified, tree-shaken file.

2. Split rehearser.js (was 196 KB) — IN PROGRESS

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-time SPEAKER_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-voice-picker.js — voice preview state, local matching, fish.audio search/import, and candidate assignment; DOM event registration remains in rehearser-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; the casting pipeline remains in audiobook.js.
  • ✅ audiobook-cast.js — cast Markdown export, legacy page-stamping migration, deterministic speaker/sentence/duplicate/alias normalization helpers, and the live cast-view renderer.
  • ✅ 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 in rehearser.js.
  • ✅ rehearser-library.js — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain in rehearser.js until their UI/event-handler dependencies are isolated.

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

  • Replace remaining silent .catch(() => {}) with logErr(context, e) (helper added in utils.js).
  • Full a11y sweep (icon-button aria-labels, focus traps, keyboard nav in createSearchablePicker) — run the web-design-guidelines review.
  • If ever exposed beyond localhost: add an auth token + rate limiting (SSRF is already guarded in core/validation.py).