Two sides of the same class the router-smoke leak (#932) traced to
test_pronunciation_api's importlib.reload teardown:
- test_pronunciation_api now re-runs init_db() on the restored data dir so
the reloaded core.db/main.app is never left on a schema-less DB.
- test_db_migration_safety catches db_module.MigrationError dynamically
instead of the collection-bound name, so a reload that rebinds the class
can't make pytest.raises miss it.
Both orderings (real + reversed) now pass; no product change.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(tts): user pronunciation dictionary (expressive-tts slice 1)
Per-term, per-language pronunciation overrides applied to text before synthesis,
so names, brands, and acronyms come out right across generate, longform, and dub.
Closes part of the #1 perceived-quality gap vs ElevenLabs (pronunciation
dictionaries). First slice of docs/specs/01-expressive-tts.md.
- Schema: additive `pronunciation_entries` table (alembic 0008, mirrored into
_BASE_SCHEMA; tested upgrade — idempotent, downgrade, converge, back-compat).
- Service: extend pronunciation.py to load enabled entries (cached) and apply
longest-first, word-boundary-aware, per-language (global '*' + lang match,
lang overrides global), reusing the existing ReDoS-safe matcher.
- Inline one-off `[[term|replacement]]` overrides that don't persist and don't
collide with [voice:]/[pause]/[Name]/SSML-lite (resolved pre-chunking).
- API: /pronunciation CRUD + /test dry-run + import/export (loopback-guarded).
- Apply point: generation.py after language resolves, before chunking — covers
native + pluggable engines.
- UI: PronunciationPanel in Settings → General; all strings via i18n.
- Tests: migration lifecycle, CRUD, per-language, precedence, inline override,
apply-at-synth. Route snapshot regenerated (+7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): bound inline-override regex (ReDoS) + annotate parameterized UPDATE
CodeQL flagged py/polynomial-redos on the [[...]] inline-override regex: [^\]]
also matches [, so an unterminated run of [ allowed O(n) rescans from O(n)
positions. Bound the inner class to {0,256} (linear; an inline override is a
short respelling). Annotate the dynamic UPDATE (B608) — its column fragments are
fixed literals and every value is a bound parameter; not an injection vector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>