Flatten skill category directory structure

This commit is contained in:
2026-05-20 17:04:50 +08:00
parent 0dd552a6f7
commit 63f2baa4bd
354 changed files with 0 additions and 0 deletions
@@ -0,0 +1,319 @@
# 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 `<location>` 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 <chapter_number>
```
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 <chapter_number>
```
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 <chapter_number>
```
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 `&lt;#0.6#&gt;` (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 `&lt;` / `&gt;` 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 <chapter_number>
```
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 <chapter_number>
```
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 <chapter_number>
```
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 <chapter_number>
```
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 <chapter_number>
```
> **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.24.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