Files
Popiai-skill/audio-creation/audiobook/phases/03-voice-preview.md
T

299 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 3: Voice Preview & Feedback
## 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.
---
## Speed Rules (Reference)
Speed is assigned **by role**, not by the voice's natural characteristics:
| Role | Default Speed |
|------|--------------|
| Narrator | **0.9** |
| All characters (actors) | **1.0** |
User may override via the preview page feedback buttons.
---
## Workflow
Execute these steps **in this exact order**:
```
Step 1: Get voice candidates → Step 2: Extract preview text
→ Step 3: Generate preview samples → Step 4: Build preview data JSON
→ Step 5: Launch preview page (BLOCKING) → Step 6: Handle feedback
```
---
## Step 1: Get Voice Candidates
Read character list from `analysis/characters.md`. Call `get_voice_id` **once per role** — one for narrator, one per character.
### Voice Selection Priority (filter layer by layer)
1. **Language match** (hard): Chinese books → Chinese voices only. English books → English voices only.
2. **Gender match** (hard): Male characters → male voices. Female characters → female voices.
3. **Age match** (hard): Elderly → aged/gravelly. Young → bright/energetic. Middle-aged → steady/mature.
4. **Personality match** (soft): Best style match (cheerful / calm / authoritative, etc.)
### Narrator Voice Rules
- Use **storyteller/narrator-type voices** suitable for sustained narration
- Must be easy to listen to without fatigue (this is the "baseline" voice)
- **Narrator and character voices must not overlap**
### Calling Pattern
```
# ✅ CORRECT — one call per role, returns a list of all matching voices
# then YOU pick 3 candidates from the returned list
get_voice_id(user_requirement="English male narrator storytelling calm warm")
get_voice_id(user_requirement="English female young warm gentle emotional")
# ❌ WRONG — calling 3 times per role to "get 3 candidates"
# get_voice_id already returns multiple results; one call is enough
get_voice_id(user_requirement="English male narrator warm")
get_voice_id(user_requirement="English male narrator calm")
get_voice_id(user_requirement="English male narrator cinematic")
# ❌ WRONG — combining all roles into one call
get_voice_id(user_requirement="English audiobook: (1) narrator; (2) female young; (3) male middle-aged")
```
**Concurrency:** `get_voice_id` is mostly a local cache lookup, but max **4 parallel calls** to be safe.
**Fallback:** If no results, consult `references/voices-backup-overseas.md`.
> **Note:** `female-chengshu` (mature female voice) has known audio artifact issues — **do not use**.
### Output
Select **exactly 3 candidates** (1 primary + 2 alternates) per role. Candidates should differ in style to give the user real choice. Each needs: `voice_id`, `name`, `description`.
---
## Step 2: Extract Preview Text
Preview samples should be **~10 seconds** of speech (roughly 2540 words for English narration, 1525 words for dialogue). Do NOT use long paragraphs.
Good examples:
```
# Narrator (~10s): 1 atmospheric sentence
"The museum closed at six.<#0.5#> By six fifteen, the last guard had finished his rounds<#0.3#> and the motion sensors armed themselves with a faint electronic chirp.<#0.6#>"
# Character dialogue (~8s): 1-2 punchy lines
"What are you doing in the building, Margaret?<#0.3#> Your department doesn't have after-hours access this week.<#0.5#>"
```
---
## Step 3: Generate Preview Samples
Generate one preview audio per **candidate voice** per role. Use the **same text** for all candidates of the same role (for fair A/B comparison).
### Step 3.1: Build Preview Generation Plan
**Before any `audios_generation` call**, write `voice_samples/preview_plan.json`.
**Do NOT add `filename` fields** — the `print_preview_calls.py` script auto-assigns normalized filenames (pattern: `preview_{role}_{index}`).
```json
[
{
"role": "narrator",
"sample_text": "<#0.2#>The museum closed at six...<#0.2#>",
"speed": 0.9,
"candidates": [
{ "voice_id": "English_CaptivatingStoryteller", "voice_name": "Captivating Storyteller" },
{ "voice_id": "English_WarmNarrator", "voice_name": "Warm Narrator" },
{ "voice_id": "English_DeepVoice", "voice_name": "Deep Voice" }
]
},
{
"role": "alice",
"sample_text": "What a beautiful day.",
"speed": 1.0,
"candidates": [
{ "voice_id": "English_Kind-heartedGirl", "voice_name": "Kind Girl" },
{ "voice_id": "English_GentleWoman", "voice_name": "Gentle Woman" },
{ "voice_id": "English_WarmFemale", "voice_name": "Warm Female" }
]
}
]
```
**Validation before proceeding:**
- Every role must have **exactly 3 candidates** with distinct `voice_id` values
- Narrator candidates must NOT overlap with any character's candidates
- `sample_text` must be identical for all candidates within the same role
### Step 3.2: Generate preview samples (MANDATORY script)
Run the bundled script to print all `audios_generation` calls grouped by wave:
```bash
python3 $AUDIOBOOK_SCRIPTS/print_preview_calls.py
```
The script auto-assigns `filename` fields (pattern: `preview_{role}_{index}`) if missing and writes them back to `preview_plan.json`.
Then fire the printed calls wave by wave:
- 1 text per call, max 3 calls per wave — **fire all calls in a wave simultaneously (in parallel), NOT one by one**
- Wait for ALL calls in a wave to complete before the next wave
- If any call fails, sleep 30 seconds then continue to the next wave. Do NOT retry inline.
**DO NOT** batch multiple candidates into one call or skip the `filenames` parameter.
### Step 3.3: Map blobs to candidates (MANDATORY script)
After all calls complete, run the bundled script to match generated blobs back to candidates via `filename`:
```bash
python3 $AUDIOBOOK_SCRIPTS/match_preview_blobs.py
```
The script reads `preview_plan.json` + `assets.json`, matches by filename (last-write-wins), and updates `preview_plan.json` with `blob_path` for each candidate.
### Step 3.4: Organize preview files (MANDATORY script)
Move preview audio files from the project root into `voice_samples/samples/` and update `assets.json` paths:
```bash
python3 $AUDIOBOOK_SCRIPTS/organize_preview_files.py
```
The script reads `preview_plan.json` for filenames, moves `.mp3` and `_subtitle.json` files, and updates `assets.json` path fields. Idempotent — safe to re-run.
---
## Step 4: Build Preview Data JSON (MANDATORY script)
Run the bundled script to generate `preview_data.json` from `preview_plan.json` (with `blob_path` from Step 3.3):
```bash
python3 $AUDIOBOOK_SCRIPTS/build_preview_data.py [--lang zh]
```
If `--lang` is omitted, auto-detects from `.audiobook-state.json` or book content. Set `--lang zh` when the user communicates in Chinese, `--lang en` otherwise.
The script reads `preview_plan.json`, transfers `blob_path` to `samplePath`, and validates all audio files exist.
**Do NOT construct preview_data.json by hand.** Always use this script.
---
## Step 5: Launch Preview Page
**⚠️ This is the most commonly skipped step. Do NOT skip it.**
This is a **blocking operation** — the script runs a local server and waits for the user to submit feedback on the page.
### Action sequence (all three are required):
**1. Send notification to the user:**
> All voice preview samples are ready! Opening the Voice Preview page now.
> On the page you can switch voices, mark speed/volume, add notes, and click Confirm & Continue when done.
(Use Chinese if the user communicates in Chinese. Adapt the message naturally — no need to copy a template verbatim.)
**2. Launch the preview server (BLOCKING call):**
```bash
python3 $AUDIOBOOK_SCRIPTS/render_preview.py /path/to/preview_data.json
```
The script prints `PREVIEW_URL=http://127.0.0.1:<port>`, opens the browser, and **blocks until the user clicks "Confirm & Continue"**. After submission, it writes `voice_settings.json` and `feedback.md` to the same directory as the input JSON, then exits.
**3. After the script exits**, read the generated `voice_settings.json` and proceed to Step 6.
> **Note on canvas**: `audios_generation` auto-registers every generated audio into `assets.json`, so preview samples appear on the canvas. This is expected and acceptable — they will be visually superseded by the final chapter audio.
---
## Step 6: Feedback Handling
### Speed/Volume Feedback
The preview page uses radio-style toggles:
- Speed: `"slower"` / `"ok"` / `"faster"`
- Volume: `"louder"` / `"ok"` / `"quieter"`
**Speed adjustment: ±0.03 per step** (e.g. "faster" → 0.9 → 0.93, "slower" → 0.9 → 0.87).
### Submit behavior
- **Empty feedback**: LGTM — save selections to `voice_settings.json`, proceed to Phase 4
- **With feedback text**:
- Speed/volume tweak → apply new parameters, offer to regenerate
- New voice request → re-query `get_voice_id`, regenerate, refresh preview
- Other → handle based on content
### voice_settings.json Format
The `label` field must match the voice tag used in `scripts/chapter_N.md` (e.g., `voice: zhang_shu`).
```json
{
"pauseDensity": "medium",
"narrator": {
"voiceId": "Chinese (Mandarin)_Lyrical_Voice",
"speed": "ok",
"volume": "ok"
},
"characters": [
{
"name": "张叔",
"label": "zhang_shu",
"voiceId": "Chinese (Mandarin)_Humorous_Elder",
"speed": "ok",
"volume": "ok"
}
]
}
```
**Important:** Field name is `voiceId` (camelCase) — this matches what the HTML preview page saves.