5.5 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— stage font/cast sidebar controls and cast-strip rendering; playback and transport remain inrehearser.js. - ✅
rehearser-voice-picker.js— voice preview state and matched-choice presentation; import and assignment actions remain inrehearser.js. - ✅
rehearser-cast.js— cast view, sort/filter state handlers, and cast toolbar wiring; card rendering remains inrehearser.js. - ✅
rehearser-voice-design.js— character development, backend selection, and backend-refresh synchronization. - ✅
audiobook-parse.js— dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains inaudiobook.js. - ✅
audiobook-cast.js— cast Markdown export and legacy page-stamping migration helpers; live casting remains inaudiobook.js. - ✅
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. - ✅
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; import/export and auto-save remain inrehearser.jsuntil 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(() => {})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).