* feat(audiobook): per-chapter preview + resume + chapter fault-isolation (PR 3/8)
Builds on the shared core (#408) and metadata UI (#409). Chapter-level control,
the spec's PR 3.
Shared core:
- chapter_cache_key(spans, sr, engine_id, voice_sig) — deterministic content
hash of a chapter's audio inputs. Same inputs → reuse; any change (text,
voice, order, pauses, sr, engine, resolved-voice signature) → re-render.
Backend (audiobook router):
- Chapter WAVs are now content-addressed in OUTPUTS_DIR/audiobook_cache. A
re-run after a failure/interruption reuses already-rendered chapters and only
synthesizes the missing/changed ones (resume). Job emits `cached` per chapter
and `cached_chapters`/`failed_chapters` on done.
- Per-chapter fault isolation: a chapter that throws emits `chapter_error` and
the job continues; the m4b assembles from the successful chapters. Re-running
retries only the failed (un-cached) chapters.
- POST /audiobook/preview — render a single chapter to audition it; shares the
same cache so a preview warms the full run and a re-preview is instant.
- _build_synth now exposes resolve + engine_id; _prepare_synth unifies the
omnivoice/generic paths for both the job and preview.
Frontend:
- Plan view: a ▶ preview button per chapter with inline playback.
- Done panel: "reused N chapters" + "N failed — click Create to retry" notes.
Tests: chapter_cache_key determinism + sensitivity (8); preview validation +
cache-hit-skips-synth (3). 55 backend + 326 frontend green; build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audiobook): mark cache-key SHA1 usedforsecurity=False (bandit B324)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First slice of the Stories+Audiobook convergence (spec:
docs/specs/2026-06-13-stories-audiobook-maturity.md). Both features will compile
to one server-side chapterized renderer; this lands the shared pure builders and
wires them behind Audiobook.
New `backend/services/longform_render.py` (all pure, unit-tested without
ffmpeg/torch):
- build_ffmetadata(chapters, global_meta) — FFMETADATA1 with an optional global
tag block (title/author→artist/narrator→composer/year→date/genre/description→
comment) + chapter table.
- build_loudnorm_filter(preset) — `-af loudnorm` for ACX (~-19 LUFS, -3 dBTP) or
podcast (-16 LUFS); off/unknown → None. Opt-in, so default behavior stays
platform-identical.
- validate_cover_image — jpg/png + 8 MB cap guard.
- build_render_cmd — generalizes the m4b mux: m4b|mp3, optional cover
(attached_pic) + loudness, bitrate validated.
- build_concat_list — moved here.
`services/audiobook.py`: build_chapter_ffmetadata / build_m4b_cmd / build_concat_list
are now backward-compatible wrappers over the core (existing imports + tests
unchanged).
`POST /audiobook`: now accepts optional `format` (m4b|mp3), `loudness`,
`cover_path`, and `metadata` and passes them through — backend-complete; the UI
for these lands in PR 2.
Tests: tests/test_longform_render.py (28) + existing test_audiobook.py (11) green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>