Flatten skill category directory structure
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
# 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 25–40 words for English narration, 15–25 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.
|
||||
Reference in New Issue
Block a user