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>
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 bystatic/loader.js(no bundler/build step). Sections are HTML fragments instatic/sections/*.htmlfetched at runtime. Modules communicate via globals (window._voices,$,escHtml,toast,saveMeta,loadVoiceLibrary, …) and some runtime monkey-patching (e.g.renderVoiceListis reassigned invoice-library.js).
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 (
utils,settings,voice-picker), 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) — 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-timeSPEAKER_COLORS). - ⬜
rehearser-cast.js—renderCastList, 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(() => {})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).