# Phase 4: Chapter Audio Generation ## Script Setup (Run Once Per Session) Read `skillScriptsPath` from the state file — written by Phase 0 with the skill's real install path: ```bash export AUDIOBOOK_SCRIPTS=$(python3 -c " import json, os from pathlib import Path audiobook_root = Path(os.getcwd()) / '.audiobook' hits = list(audiobook_root.glob('*/.audiobook-state.json')) if audiobook_root.exists() else [] if not hits: print('ERROR: state file not found — run Phase 0 first'); exit(1) data = json.loads(hits[0].read_text()) val = data.get('skillScriptsPath', '').strip() if not val: print('ERROR: skillScriptsPath missing from state — re-run Phase 0'); exit(1) if not Path(val).is_dir(): print(f'ERROR: skillScriptsPath does not exist: {val}'); exit(1) print(val) ") echo "AUDIOBOOK_SCRIPTS=$AUDIOBOOK_SCRIPTS" ``` A clean path (no ERROR) means scripts are ready. **If you see `skillScriptsPath missing from state`:** Do NOT re-run Phase 0 just for this. Instead, derive the path directly from the `available_skills` list visible in your system context — find the `` entry for the audiobook skill (e.g. `file:///Users/alice/.hub/skills/audiobook/SKILL.md`), strip `file://` and `/SKILL.md`, append `/scripts`. Then patch the state file: ```python import json from pathlib import Path audiobook_root = Path(os.getcwd()) / ".audiobook" state_path = next(audiobook_root.glob("*/.audiobook-state.json")) state = json.loads(state_path.read_text()) state["skillScriptsPath"] = "/derived/path/to/audiobook/scripts" # replace with actual state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2)) ``` Then re-run the Script Setup block above. --- ## 4.1 Core Principle: TTS Built-in Silence **Never add silence or crossfade via ffmpeg in post-processing.** All silence must come from `<#X#>` pause markers in the TTS text. TTS-generated silence shares the same noise floor and audio characteristics as the speech, preventing waveform discontinuities at splice points. ## 4.2 TTS Text Silence Padding Each segment's TTS text needs head/tail silence padding. **This is applied automatically by the Step 1 script — do NOT manually edit segment text.** The chapter script from Phase 2 already contains inline `<#X#>` pause markers. Step 1 preserves ALL existing markers and adds head/tail padding on top. **Padding rules (applied automatically in Step 1):** | Position | Head Padding | Tail Padding | |----------|-------------|-------------| | First segment of book | `<#2#>` | `<#0.2#>` | | Last segment of book | `<#0.2#>` | `<#3#>` | | Chapter ending (not last) | `<#0.2#>` | `<#2#>` | | Normal segment | `<#0.2#>` | `<#0.2#>` | ## 4.3 TTS Generation (Parallel with Pre-Indexed Plan) **The ONLY rule that matters: 1 text per call, max 3 calls per wave. Run every script below — do NOT skip any.** Generation uses **parallel `audios_generation` calls** with a **pre-indexed generation plan** to guarantee correct playback order regardless of completion timing. Each segment from the chapter script becomes **one entry** in the generation plan — no merging or grouping. ### Step 1: Parse Chapter Script & Build Generation Plan (MANDATORY script) This script does everything: 1. Reads the chapter script (preserving ALL `<#X#>` markers from Phase 2) 2. Parses segment delimiters and headers 3. Maps voice labels to voice_ids via `voice_settings.json` 4. Applies 4.2 head/tail padding 5. Outputs `generation_plan.json` 6. **Prints every wave's calls — use these directly for Step 2** ```bash python3 $AUDIOBOOK_SCRIPTS/build_generation_plan.py ``` The script auto-detects chapter title from `chapters.md` and first/last chapter from `.audiobook-state.json`. It also validates voice assignment and warns if dialogue segments fall back to narrator voice. **This script is MANDATORY — run it first, then use its output for Step 2.** ### Step 2: Fire TTS calls wave by wave Use the exact calls printed by Step 1. Each call has exactly **1 text** and **1 filename**. **Rules:** - Fire up to 3 calls per wave (copy the calls from Step 1 output) — **fire all 3 simultaneously (in parallel), NOT one by one** - Wait for ALL calls in a wave to complete before starting the next wave - No sleep needed between successful waves - **If any call fails → sleep 30 seconds, then continue to the next wave.** Do NOT retry the failed call inline. All failures are handled after Step 3.5 by the Failure & Retry script, which reads parameters from `generation_plan.json` (guaranteed correct). Inline retry risks using wrong parameters from LLM context. **Example (from Step 1 output):** ``` # Wave 1 (fire these 3 calls in parallel): audios_generation(texts=["<#0.2#>She opened the door.<#0.5#>"], voice_id="narrator", speed=0.9, filenames=["ch1_idx_0"]) audios_generation(texts=["Hello!"], voice_id="alice", speed=1.0, filenames=["ch1_idx_1"]) audios_generation(texts=["<#0.2#>He waved back.<#0.2#>"], voice_id="narrator", speed=0.9, filenames=["ch1_idx_2"]) # Wait for wave 1 to complete. Then wave 2: audios_generation(texts=["Good morning."], voice_id="bob", speed=1.0, filenames=["ch1_idx_3"]) ... ``` **DO NOT:** - Put multiple texts in one call: `texts=["a", "b", "c"]` is WRONG - Invent your own filenames — use the `ch{N}_idx_{M}` from Step 1 - Skip Step 1 and construct calls yourself ### Step 3: Match blobs to plan (MANDATORY script) After all TTS calls complete, run this to map generated audio files back to plan entries: ```bash python3 $AUDIOBOOK_SCRIPTS/match_blobs.py ``` The script reads `generation_plan.json` + `assets.json`, matches by filename (last-write-wins for retries), and updates `generation_plan.json` with `blob_path` for each entry. ### Step 3.5: Verify blob content (MANDATORY script) Verify every matched blob's metadata matches the plan (catches wrong voice_id or text from misfired calls): ```bash python3 $AUDIOBOOK_SCRIPTS/verify_blobs.py ``` The script compares voice_id and cleaned text from `assets.json` metadata against `generation_plan.json`. Mismatched entries have their `blob_path` cleared so the Retry script picks them up. **How `clean_text()` works (important for debugging false positives):** The script normalises both sides before comparing: 1. **`html.unescape()`** — the chapter script (Phase 2 output) may store pause markers as `<#0.6#>` (HTML-escaped) rather than `<#0.6#>`. The plan inherits this encoding. The TTS metadata always stores raw markers. Unescaping before stripping ensures both sides match. 2. **Smart-quote → ASCII normalisation** — the plan may contain Unicode curly quotes (`"` / `"` / `'` / `'`). The TTS engine normalises these to straight ASCII quotes (`"` / `'`) in stored metadata. Both sides are normalised before comparison. If you see unexpected "text mismatch" failures on Step 3.5, check `generation_plan.json` for `<` / `>` or `\u201c` / `\u201d` — these indicate the source script was HTML-escaped or contained smart quotes. The script handles both automatically; no manual fix needed. ### Step 3.6: Organize segment files (MANDATORY script) Move segment audio files from the project root into `audio/chapter_{N}/segments/` and update `assets.json` paths: ```bash python3 $AUDIOBOOK_SCRIPTS/organize_segment_files.py ``` The script reads `generation_plan.json` for filenames, moves `.mp3` and `_subtitle.json` files, and updates `assets.json` path fields. Idempotent — safe to re-run after retries. ### Step 4: Write manifest (MANDATORY script) ```bash python3 $AUDIOBOOK_SCRIPTS/write_manifest.py ``` The script reads `generation_plan.json`, writes `manifest.txt` in playback order, and verifies all blob files exist on disk. ### Failure & Retry (MANDATORY script) If Step 3 reports unmatched segments, run this to get retry calls: ```bash python3 $AUDIOBOOK_SCRIPTS/print_retry_calls.py ``` The script cleans old workspace `.mp3` files (prevents gateway dedup suffix), then prints exact retry calls grouped by wave (max 3 per wave). **Retry procedure:** 1. Run Step 3 (blob matching) + Step 3.5 (verification) to identify all failures 2. Run the Failure & Retry script above — it reads `generation_plan.json` and prints exact calls 3. Sleep 20 seconds (let system recover from transient issues) 4. Fire the printed calls (max 3 per wave) — do NOT modify the parameters 5. Re-run Step 3 + Step 3.5 → re-run this script → repeat until all matched **CRITICAL:** Never construct retry calls yourself. Always use this script's output. The script reads parameters from `generation_plan.json`, ensuring text, voice_id, speed, and filename are all correct. Manual retry from memory is the #1 cause of wrong audio content. Never skip a failed segment. The manifest requires every segment to have a blob_path. ### TTS text limits - Each text should not exceed 2500 characters - If a single segment is too long, split by sentence into sub-segments connected with `<#0.2#>` - Sub-segments share the same voice_id and get consecutive indices in the plan ## 4.4 Concatenation & Export ### Step 1: Verify manifest ```bash python3 $AUDIOBOOK_SCRIPTS/write_manifest.py ``` This is the same script as Step 4 — it writes `manifest.txt` and verifies every file exists. Run it again here to confirm nothing changed between generation and concatenation. ### Step 2: Delegate to editing sub-agent Pass the **exact ordered file list from manifest.txt** to the editing sub-agent. **Do NOT re-sort or re-order** — the manifest is the single source of truth. ``` 请帮我拼接以下音频片段成一个完整的章节文件。 不需要 crossfade、fade 或 DC removal — 直接拼接即可。 片段列表(按顺序排列,不要重新排序): 1. /abs/path/to/.hilo/.blobs/{uuid1}.mp3 2. /abs/path/to/.hilo/.blobs/{uuid2}.mp3 3. /abs/path/to/.hilo/.blobs/{uuid3}.mp3 ... 格式要求:MP3, 44100Hz, 256kbps 输出文件名:chapter_{N}.mp3 完成后请返回输出文件路径和时长。 ``` **Important:** - Input file list MUST come from `manifest.txt` - Number each file to make order unambiguous - Do NOT specify an output path — let the editing agent decide - The editing sub-agent automatically handles canvas registration (writes to both project root and `.hilo/.blobs/`, registers in `assets.json`) ### Filename rules - Individual segments: `ch{N}_idx_{M}` (generated by Step 1 script, do not change) - Final chapter audio: `chapter_{N}.mp3` - **Never use Chinese characters in any audio filename** — canvas registration silently fails ## 4.5 Audio Quality Check After each chapter's final audio is exported, run the quality checker: ```bash python3 $AUDIOBOOK_SCRIPTS/audio_check.py /path/to/exported_chapter.mp3 ``` > **ffmpeg dependency:** `audio_check.py` and `generate_assembly_report.py` both shell out to `ffmpeg` / `ffprobe`. If `ffmpeg` is not on `$PATH`, prepend the binary location: > ```bash > PATH="/opt/homebrew/Cellar/ffmpeg/8.1/bin:$PATH" python3 $AUDIOBOOK_SCRIPTS/audio_check.py ... > ``` > Find the binary with: `find /usr /opt /Users -name "ffmpeg" -type f 2>/dev/null | head -3` If issues are found, delegate fixes to the **editing sub-agent**: - **Pops/clicks**: Apply adeclick/adeclip filter - **DC offset > 1%**: Apply dcshift correction - **Clipping**: Reduce volume and regenerate the affected segments **Energy spikes are normal:** The quality checker reports "energy spikes" at segment boundaries (silence → speech onset from hard-cut concatenation). These are expected and require no action — they are not audio artifacts. ## 4.6 Assembly Report After the quality check passes, generate a markdown assembly report. This is the **single source of truth** for users to inspect, locate, and request re-edits of any segment. ### What it contains A markdown table with one row per segment: | Column | Content | |--------|---------| | `#` | Segment index (primary identifier for re-edit requests) | | `Start` / `End` | Precise timestamps `hh:mm:ss.ss` via ffprobe | | `Voice ID` | The actual `voice_id` used | | `Local File` | Root-directory filename (matches canvas display) | | `Text` | Clean spoken text with `<#X#>` markers stripped | ### Generation script ```bash python3 $AUDIOBOOK_SCRIPTS/generate_assembly_report.py ``` > **ffmpeg dependency:** This script uses `ffprobe` to compute timestamps. If `ffprobe` is not on `$PATH`, prepend the binary location (same as 4.5 above). The script reads `generation_plan.json`, uses ffprobe to compute timestamps, maps blob refs to local filenames via `assets.json`, and writes `{book_name}_chapter_{N}_assembly.md` to the project root. Auto-detects language for tips text. > **Canvas note:** The canvas does not render markdown tables — only header lines are previewed. Users can open the assembly report in any markdown viewer for the full table. ### Re-edit workflow When the user requests changes (e.g. "segment #14 sounds too fast"): 1. Look up index 14 in `generation_plan.json` for `voice_id`, `speed`, `text`, `filename` 2. Regenerate: `audios_generation(texts=[new_text], voice_id=..., speed=..., filenames=[filename])` 3. Re-run Step 3 blob matching to pick up the new blob (latest entry with same filename wins) 4. Rewrite `manifest.txt` from updated plan 5. Re-concatenate via editing sub-agent 6. Re-run quality check and regenerate assembly report ## 4.7 Final User Message (REQUIRED) After the assembly report is generated, send a completion message. Match the user's language. Include: - Final audio filename (UUID on canvas) - Duration - Cast list (narrator + characters) - Assembly report filename - How to request fixes (mention segment # number) - Options: generate next chapter / re-edit a segment ## 4.8 Multi-Chapter Generation > **Note:** True background/parallel chapter generation is not currently supported. > Chapters are processed one at a time (sequentially) to ensure quality and allow user review between chapters. **After the first chapter is confirmed by the user:** 1. Tell the user how many chapters remain and ask if they want to continue generating them 2. For each remaining chapter, follow the full 4.2–4.6 workflow in order: - Build generation plan → Fire TTS waves → Match blobs → Verify → Organize → Write manifest → Concatenate → Quality check → Assembly report 3. After each chapter completes, update `chaptersCompleted` in `.audiobook-state.json` 4. Notify the user that chapter N is done, show duration and assembly report filename, then ask whether to proceed to the next chapter or stop ## Error Handling - TTS generation failure: Sleep 20s then retry. Never skip — keep retrying until success. - File parsing failure: Prompt the user to check file format - Audio concatenation failure: Check file paths and formats