tts-voice-creator-clone-and.../docs/ARCHITECTURE.md
mARTin-B78 40e42590cc Release v1.6.0: a11y (WCAG AA), i18n (DE), PWA, perf, tests, Cast UX
Cast: card/list views, sort & filter, online voice picker, "Hear a line"
sample button, AI character notes, import auto-save.

Platform: WCAG 2.1 AA accessibility pass; German UI translation + language
picker; installable PWA with offline shell; GZip + content-visibility
virtualization + lazy images + Rehearser PCM memory cap (mobile stability);
Playwright suite (desktop + iPhone); opt-in minified bundle build.

Fixes: screenplay parser false characters; Fish-Speech inline-tag tones;
narrator/voice pickers list full library; clone GUI rework; fish.audio
import dedup; voice-ID rename; bulk-delete modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:23:35 +02:00

4.3 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, …).
  • 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 minifyscripts/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.jsparseScript, detectCharacters (pure; only call-time SPEAKER_COLORS).
  • rehearser-cast.jsrenderCastList, sort/filter, online voice picker, character research.
  • rehearser-stage.js — playback, synth cache, pre-decode window, transport.
  • rehearser-train.js — mic record / STT compare.
  • rehearser-library.js — IndexedDB session store, import/export, auto-save.

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).