---
name: local-audio
description: "AI audio production with two engines: local (Kokoro-82M, offline and free) plus cloud (ElevenLabs, best human feel). Covers (1) voiceover synthesis in Chinese, English, Korean and other languages, including the routing rule that non-Chinese, non-English languages go through the video model's native audio, (2) transcription of audio/video into SRT subtitles, (3) short music and ambience beds, and states the hard human-feel metrics (reverb / speech rate / pauses / timbre separation) plus the audio-source ledger red line. Use when the user mentions 'voiceover', 'speech synthesis', 'TTS', 'generate subtitles', 'transcribe audio', 'make a background track', 'human feel', 'sounds like a robot', 'voice cloning'."
---

> Original skill · commissioned by the user · English edition, 2026-11-12

# AI Audio Production (local + cloud, dual engine)

| Engine | Script | Use for |
|---|---|---|
| **Doubao speech synthesis 2.0** | `say_volc.py` | **First choice for Chinese**. Trained on native Chinese, most natural. Single key `<API_KEY_ENV>`, already configured and working |
| Cloud ElevenLabs | `say_cloud.py` | Best for English; usable for Chinese but **carries a foreign accent** (all preset voices are native English speakers) |
| Local Kokoro-82M | `say.py` | Free, offline, never uploaded, bulk drafts, placeholder audio |
| Local transcription | `transcribe.py` | Subtitles, transcription (always local, never uploaded) |
| Local music | `music.py` | Short ambient music beds |

### Language routing (**pick the engine by language first, then choose the voice**)

| Target language | First choice | Fallback | Notes |
|---|---|---|---|
| **Chinese** | Doubao TTS 2.0 (`say_volc.py`) | — | Trained on native Chinese; ElevenLabs cannot fix this (see next section) |
| **English** | ElevenLabs (`say_cloud.py`) | Doubao (usable) | the account's remaining quota |
| **Korean / Japanese / other non-Chinese, non-English** | **video model native audio** (Seedance 2.0+, dialogue written into the prompt) | local Kokoro (multilingual, drafts only) | ⚠️ see "non-Chinese, non-English languages" below |

#### Non-Chinese, non-English languages: **check the video model's native audio first, do not reach for TTS by default**

Conclusion from a past 10-second Korean clip: **the Korean voice in the final cut was not produced by TTS**.
It was native audio produced by Seedance in the **same inference pass** that generated the video
(dialogue plus ambience in one go). Reverse transcription re-verified it as **`ko` with 0.98 confidence**,
word-for-word identical to the script — **the model really is speaking Korean**.

- **Why prefer it**: the model guarantees audio-picture sync itself, so there is no lip-sync alignment, and
  one TTS pass plus duration fitting is skipped.
- **Cost**: **not separately controllable**, it tends to add business on its own, and a silent scene may
  spontaneously produce voices → key shots still need a human listen.
- **How to write it**: put **only "scene + ambience + verbatim dialogue"** in the prompt, do not describe
  characters (describing characters triggers the real-person check; after a rejection, removing the
  character description from the same image passes).
- **When to fall back to TTS**: when you need **one timbre across shots**, **per-sentence control of
  pauses**, or the model does not support that language. Then use multilingual Kokoro (draft grade) or a
  cloud option that allows commercial use.

### Engine choice for Chinese voiceover (the hard-won conclusion)

**Always prefer Doubao (Volcano Engine) for Chinese.** In the operator's own listening tests, even
ElevenLabs voices that are native Chinese speakers in the library sound like "a foreigner speaking
Chinese", because the underlying model is a general multilingual model whose prosody and tone habits are
English-first. Doubao is ByteDance speech technology (same lineage as CapCut and Dreamina), trained on
Chinese from the start; switching voices cannot close the gap.

**Do not deliver Chinese finals produced by Kokoro** — 82M parameters, flat intonation with no breath; the
user's own verdict was "sounds like a robot".

Runtime is fixed at `<TOOLS_DIR>` (**must be a pure ASCII path**, see pitfall 1).
Python environment: `<TOOLS_DIR>/venv/Scripts/python.exe`
Credentials: `<API_KEY_ENV>` is already set in the environment (User scope; the scripts read it themselves,
no manual setup needed).
**The account is on a limited plan: capped quota, no commercial license, no voice cloning — always confirm
with `--quota` before placing an order.**

## ⚠️ Engine choice: think it through first

**Default to cloud.** The user has explicitly said local Kokoro "feels entirely robotic, with no human
feel".

- Need **human feel / a formal final cut / delivery to a client** → `say_cloud.py` (ElevenLabs), **do not
  use Kokoro**
- Need **fully offline / zero cost / never upload the material / large batches of voice tests** →
  `say.py` (Kokoro)
- Unsure: **ask the user first**, or default to cloud

Kokoro's role is "a free offline stand-in factory", not "a pleasant voice actor". Its ceiling is 82 million
parameters: flat intonation, no breath, no emotional range — no amount of parameter tuning fixes that.

## 0. Doubao speech synthesis (first choice for Chinese)

```powershell
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_volc.py" --check
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_volc.py" "text" -v zh_female_xiaohe_uranus_bigtts -o out.mp3
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_volc.py" --list-speakers
```

### Authentication (verified once — do not copy older tutorials)

The Volcano **new console has deprecated "AppID + Access Token"** and uses a single key instead:

| Item | Value |
|---|---|
| Environment variable | `<API_KEY_ENV>` (UUID format, 36 characters) |
| Request header | `X-Api-Key: <key>` |
| Resource header | `X-Api-Resource-Id: seed-tts-2.0` |
| Endpoint | `https://openspeech.bytedance.com/api/v3/tts/unidirectional` |

**The voice must match the resource**: `seed-tts-2.0` only accepts **2.0 voices** whose IDs end in
`zh_xxx_xxxx_uranus_bigtts`. Using a 1.0 voice ending in `_moon_`/`_mars_` returns:
`code=55000000 "resource ID is mismatched with speaker related resource"`.
(Older material's `volc.service_type.10029` returns `code=45000030 requested resource not granted` for the
current account, i.e. that resource is not enabled.)

### Voices verified as working (15, verified once)

Female: `zh_female_xiaohe_uranus_bigtts` (Xiaohe 2.0), `zh_female_vv_uranus_bigtts` (Vivi),
`zh_female_linjianvhai_uranus_bigtts` (Girl Next Door 2.0), `zh_female_cancan_uranus_bigtts` (Intellectual
Cancan), `zh_female_gaolengyujie_uranus_bigtts` (Aloof Older Sister 2.0),
`zh_female_tianmeitaozi_uranus_bigtts` (Sweet Peach 2.0), `zh_female_roumeinvyou_uranus_bigtts` (Gentle
Girlfriend), `zh_female_sajiaoxuemei_uranus_bigtts` (Coy Schoolgirl)

Male: `zh_male_ruyaqingnian_uranus_bigtts` (Refined Young Man), `zh_male_dayi_uranus_bigtts` (Dayi),
`zh_male_liufei_uranus_bigtts` (Liu Fei), `zh_male_wennuanahu_uranus_bigtts` (Warm A'Hu),
`zh_male_m191_uranus_bigtts` (Yunzhou), `zh_male_sophie_uranus_bigtts` (Charming Sophie),
`zh_male_sunwukong_uranus_bigtts` (Monkey King)

Full voice list: https://www.volcengine.com/docs/6561/1257544

Emotion and speed: `--emotion` / `--emotion-scale 0-5` / `-s` speed percentage. 2.0 voices also support
dialects, and

**The default speed for Chinese talking-head voiceover is `-s 6` (do not use the baseline).** Measured
across two different projects: at baseline speed the user judged it "a touch slow" and "not natural
enough"; after raising it to **+6%** both were confirmed as "close to a normal human speaker" (user quote
at the time: "the voice speed in the 103.4-second version is close to a normal human"). For short-form
talking-head video, hand it `-s 6` straight away; do not render a baseline version first and rework it.
tone (coy / flirtatious / argumentative) and other advanced parameters (see the official docs, not yet
wired into this script).

Performance: 38 characters in about 1.5 seconds, far faster than local Kokoro.

### Error code quick reference

| Response | Meaning |
|---|---|
| `55000000` | Voice does not match the resource → switch to a `_uranus_bigtts` voice |
| `45000030` | That resource is not enabled |
| `45000010` | `X-Api-Key` is invalid |
| `45000000` | Auth header missing (means Bearer was used by mistake) |

## 1. Cloud synthesis (best for English)
```powershell
# Chinese (eleven_v3 by default, its best Chinese performance)
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_cloud.py" "text" -v Sarah -o out.mp3
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_cloud.py" --file script.txt -v George -m eleven_multilingual_v2 -o out.mp3

# List voices / check quota / convert to wav
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_cloud.py" --list-voices
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say_cloud.py" --quota
```

**Model choice (measured conclusions for Chinese)**:

| Model | Chinese performance |
|---|---|
| `eleven_v3` | ✅ **default**. Most accurate pronunciation, most expressive, supports emotion tags such as `[excited]` `[whispers]` |
| `eleven_multilingual_v2` | Stable and usable, high pronunciation accuracy |
| `eleven_turbo_v2_5` | ❌ Chinese pronunciation regresses badly (in a past test "this thing" came out as "this twelve"), **do not use for Chinese** |

Voices: `Sarah` (mature, steady female), `George` (warm narrative male), `Lily` (British female),
`Alice`, `Brian`, `Daniel` and 21 preset voices in total. The "also supports Chinese" note here only means
**it can technically synthesise it**; it does **not** mean it suits Chinese voiceover — Chinese always goes
to Doubao, see "Engine choice for Chinese voiceover" above. (Past retrospective: because of this line, an
explainer-video project first went and evaluated ElevenLabs anyway, and the user called a stop to it.)

Emotion control: `-s/--stability` (**lower means more dynamic**, default 0.42), `--style` (performative
feel, default 0.35). For a livelier read, drop stability to around 0.3.

### ⛔ Account quota and licensing (confirm first, never promise from guesswork)

The account currently runs on a **limited plan**, verified once (`say_cloud.py --quota`):

- Quota: **the account's monthly character allowance**, roughly a few tens of minutes of Chinese speech
  in total. Every voice test and every regeneration **consumes characters**, and regenerating does not
  return them. Run `--quota` before a bulk job to see what is left.
- **Voice cloning: unavailable** (`can_use_instant_voice_cloning=False`). The free tier cannot clone
  voices; that needs **Starter ($6/month)** or above. **Do not promise cloning to a user unless you have
  confirmed the plan was upgraded.**
- **The free tier carries no commercial license**: the official terms state "cannot be used for any
  commercial purpose", and public release must credit `elevenlabs.io` or `11.ai` in the title. Commercial
  rights start at Starter. If the user intends client work, selling, or delivery to a customer, this must
  be raised.
- The free tier has **a small number of custom voice slots**; the vendor says they can be used together
  with Voice Design (generate a voice from a text description); the community says creating a Voice Design
  voice requires Starter, unverified. **Get consent before creating voice assets under the user's account.**

## 2. Local synthesis (offline / free)

```powershell
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say.py" "text" -v zf_001 -o a.wav
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/say.py" --list-voices
```

Parameters: `-v` voice, `-s` speed, `--format wav|mp3`, `--model fp32|int8` (**fp32 is the default**; int8
is faster but quantisation introduces a noise floor), `--zh-ver 1.1|1.0` (1.1 is the default,
Chinese-specific build), `--gap` pause between sentences.

Chinese voices (the v1.1-zh dedicated model, 103 real speakers): female `zf_001`–`zf_099` (55 voices),
male `zm_009`–`zm_100` (45 voices). English uses the v1.0 voice pack (`af_*`/`am_*`/`bf_*`/`bm_*`).
**The two voice sets are not interchangeable**; switch with `--zh-ver`.

Local performance: about 1x real time.

## 3. Subtitle transcription (always local, never uploaded)

```powershell
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/transcribe.py" "input.mp4" -l zh -m medium --txt
```

`-m`: `tiny`/`base`/`small`(default)/`medium`/`large-v3`. Measured on CPU, `small` ≈ 1x real time and
`medium` ≈ 0.4x real time. **Homophone errors happen** (in a past test "long text" came out as "rising
text"); read the whole transcript before delivery.

## 4. Music generation (short clips only)

```powershell
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/music.py" "calm ambient pad" -d 10 -o bgm.wav
```

Write prompts in English. On CPU about 9.4x real time (30 seconds ≈ 5 minutes). **It cannot sing vocals**,
so it cannot produce a finished song.

## Key pitfalls

1. **The path must be pure ASCII**. espeak-ng on Windows cannot read a path containing Chinese characters,
   silently falls back to the compile-time default path and reports
   `Error processing file ... phontab: Illegal byte sequence`; the path in the error is the CI build
   machine's (`<BUILD_PATH>`), which is very easy to misread as a broken install. The runtime is therefore
   fixed at `<TOOLS_DIR>`.
2. **The Chinese phoneme version must match the model**. v1.1-zh goes with `ZHG2P(version="1.1")`
   (zhuyin-style notation), v1.0 goes with `version="1.0"` (IPA). Mixing them fails completely.
3. **Never switch back to espeak-ng phonemisation**. kokoro-onnx defaults to espeak and Chinese
   pronunciation comes out completely scrambled (in a past test "the whole pipeline depends on no network
   at all" was read as gibberish).
4. **Pin `av` to 18.x**. faster-whisper declares `av>=11` with no upper bound; 19.x removed the
   `metadata_errors` parameter and raises `TypeError`; 14.4 and below have no Python 3.12 prebuilt wheel.
5. **Model file names**: fp32 has no suffix (`kokoro-v1.1-zh.onnx`), int8 appends `.int8`.
6. **Cloud voice name matching**: ElevenLabs' `name` field is "Sarah - Mature, Reassuring, Confident";
   matching by short name needs `split(" - ")[0]`; `say_cloud.py` already handles this.

## Quality self-check

**Transcription can only verify "was it read correctly / is it the right language", never "does it sound
human".**
The full re-verification flow (including language verification) is in §9; the hard human-feel metrics are
in §7.
---

## 5. Dialogue / final-cut mixing flow (verified)

### Flow

1. **Write the script structure**: annotate every line with
   `(character, text, emotion, emotion intensity, speed offset, pause seconds after the line)`. Give pauses
   by meaning (0.7s hesitation, 0.3s interruption), not one uniform value.
2. **Synthesise line by line** (Doubao), using different parameters per emotion, not one flat pass:
   - Coy → `happy ×3`
   - Angry → `angry ×4` + speed `+8`
   - Playing weak → `sad ×2`
   - Feeling wronged → `sad ×3` + speed `-5` (slow down)
   - Perfunctory / impatient → `neutral` + speed `+10`
3. **Assemble**: decode each line to 24k mono WAV and **join them with numpy directly** (inserting silence
   segments), writing one dialogue track. **Do not use the ffmpeg concat demuxer** — in a past test it
   silently stopped halfway and only emitted the first two segments.
4. **BGM**: generate roughly a 20-second loop locally with `music.py` (warm lo-fi / piano / no drums).
   Prompts in English. On CPU, 20 seconds ≈ 4 minutes.
5. **Mix**: sidechain ducking — BGM drops automatically while someone speaks and rises again afterwards.

### Mix filters (write them to a file and use `-filter_complex_script`)

```
[1:a]aformat=sample_rates=24000:channel_layouts=mono,volume=-7dB,afade=t=in:st=0:d=2.5[bkg];
[0:a]aformat=sample_rates=24000:channel_layouts=mono,apad=pad_dur=2.5,asplit=2[vox][sc];
[bkg][sc]sidechaincompress=threshold=0.015:ratio=10:attack=5:release=600[duck];
[vox][duck]amix=inputs=2:duration=longest:normalize=0[mix];
[mix]afade=t=out:st=<dialogue_duration-0.03>:d=2.5,alimiter=limit=0.82,volume=1.5dB[out]
```

```powershell
ffmpeg -y -i dialogue.wav -stream_loop -1 -i bgm.wav -filter_complex_script filters.txt -map "[out]" -t 51 -ar 44100 final.wav
```

Target level: mean ≈ −20 dB, peak ≈ −3 dB. Verify with `-af volumedetect -f null -`.

### Three pitfalls you will hit

1. **An ffmpeg label can only be used once**. Using `[vox]` both as the sidechain trigger and in the mix
   raises `Invalid stream specifier: vox` / `matches no streams` (the message is highly misleading).
   Duplicate it first with `asplit=2`.
2. **Passing `-filter_complex` through PowerShell gets mangled by the parser**. Even when the string looks
   complete, ffmpeg reports missing labels. **Always write it to a file and use
   `-filter_complex_script`.**
3. **`sidechaincompress` requires the same sample rate and channel count on both inputs**; align them with
   `aformat` first. BGM from MusicGen is 32kHz while dialogue is usually 24kHz.

### Must check before delivery

- `ffprobe` the duration against expectation (**is the count right**, do not just check that the file exists)
- `volumedetect` to confirm peaks are not clipping
- Reverse-transcribe locally with `transcribe.py` to confirm every line is present and in the right order
---

## 6. Doubao BGM generation (Seed-Audio 1.0, via AI MediaKit)

Cloud music/audio generation, far higher quality than local MusicGen. Script: `music_volc.py`.

```powershell
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/music_volc.py" "warm piano background music, night ambience" -o bgm.mp3
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/music_volc.py" "late-night study, quiet piano solo" --name "late-night study"   # store in the asset library
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/music_volc.py" --check
```

Without `-o` it is stored automatically in the asset library `<TOOLS_DIR>/assets/bgm/`, together with a
sibling `.json` holding the metadata (prompt / model / timestamp / normalisation gain / rights statement)
so it can be searched and reused later.

### Activation prerequisites (four steps, all required)

| Step | Location |
|---|---|
| ① Enable "audio generation 1.0" | Doubao speech console → activation management |
| ② **Cross-service authorization** for the Doubao speech SA | AI MediaKit → basic configuration → authorization management → authorize |
| ③ Create a **MediaKit API Key** | AI MediaKit → basic configuration → API Key management |
| ④ Create a Doubao speech API Key under the **Default project** | Doubao speech → API Key |

Environment variables: the MediaKit key is stored in `<API_KEY_ENV>`; the Doubao speech key is stored in
`<API_KEY_ENV>`.

### Four pitfalls already hit

1. **Audio generation does not go through `openspeech.bytedance.com`**. That is the speech synthesis (TTS)
   API. Audio generation must go through the AI MediaKit MaaS proxy
   `https://mediakit.cn-beijing.volces.com/api/v1/tools/call-maas-proxy`,
   with `api=doubao.audio_generation` and `maas_params.model=seed-audio-1.0`.
2. **The two credential sets cannot be mixed**. The MediaKit key (`<API_KEY>`) and the Doubao speech API key
   are two different keys; hitting a MediaKit endpoint with the speech key returns 403.
3. **`code=45000030` cannot be used to judge "whether it is enabled"**. In a past test a made-up resource
   ID (`fake-resource-9z`) returned the same error code; it cannot distinguish "not enabled" from
   "resource does not exist".
4. **`no available speech api key found for project default`** means MediaKit looks up the Doubao speech API
   key **by project name**. The API Key management page in the Doubao speech console only shows keys of the
   **Default project**; a mismatched project is not found → one must be created under Default.

### Two characteristics you must know

- **The raw output level is extremely low**: peak about **-24 dB**, nearly inaudible as delivered.
  `music_volc.py` already normalises automatically (a past run applied +22.6 dB). **Do not skip this step**,
  otherwise you will misjudge it as "generation failed".
- **Duration is fixed at about 16.6 seconds**: `audio_config` has no duration field, and changing the
  prompt does not change it.

### Rights and compliance

- Content generated after the service is formally enabled: **intellectual property belongs to the account
  owner** (dedicated terms for generative model services, 3.5)
- ⚠️ **But content generated by playing in the "experience center" is covered by terms stating "you may not
  claim intellectual property in the generated content"** — do not treat audio tried out in the playground
  as an asset
- The **AI-generated label must be retained** (Administrative Measures for the Labelling of
  AI-Generated Synthetic Content); it must not be removed or obscured
### Mixing parameter tuning log (measured)

- `release=320` is too short → **BGM bounces back inside small pauses within a sentence**, and the music
  audibly "jumps in fits". In a past test the BGM jumped between -30.9dB (properly ducked) and -21.6dB
  (bounced back during pauses) during dialogue. **Switching to `release=600`** smoothed it out clearly.
- BGM level: `-7dB` (with dialogue averaging about -24.5dB, the background sits a steady ~10dB lower)
- **If the BGM asset is shorter than the dialogue, it must be looped seamlessly**: chain several copies with
  `acrossfade=d=2`; a plain concat produces an audible seam. Doubao audio generation is fixed at 16.6
  seconds while dialogue is often longer, so this step cannot be skipped.
- Verify ducking in isolation: export the ducked BGM alone (`[vox]anullsink`) and compare the level during
  dialogue against a BGM-only section.

---

## 7. Hard human-feel metrics (**measurable — never say "natural enough" from feeling**)

The user's acceptance feedback, verbatim: "it feels entirely robotic, no human feel, no sense of a living
person". This is not a timbre problem, it is **missing performance parameters**. Every row below is
checkable numerically; go through all of them before delivery:

| Metric | Target | What a miss sounds like | How to fix |
|---|---|---|---|
| **Room reverb** | wet/dry **15–30%** | **dry = broadcast-read tone** (the most fatal one) | add a reverb layer, never deliver a fully dry voice |
| **Speech rate** | dialogue **4.0–5.5 characters/second** | **> 6.5 characters/second = broadcast-read tone** | slow down; if it does not fit, **rewrite the words**, do not force-compress |
| **Pause between sentences** | **0.25–0.5 s** | median **< 0.20 s = reading a script** | give pauses by meaning (hesitation 0.7s / interruption 0.3s), **never one uniform value** |
| **Timbre across shots** | **one voice per character**, unchanged across shots and episodes | the same person with two different voices | write the timbre into the character card and freeze it |
| **Character separation** | pitch **offset by ≥15%** between characters | the audience cannot tell who is speaking | change the voice, do not pick within the same register |
| **Generation granularity** | **synthesise sentence by sentence** | one whole-block generation = flat tone, rushed line endings | one sentence per call, each with its own emotion parameters |

> **Changing the timbre alone cannot produce human feel** — of the six rows above, reverb, pauses and
> generation granularity have nothing to do with timbre.
> After listening to six different voices (Sarah / George / Lily / the newest cloud model), the user's
> conclusion was **"they all sound like foreigners speaking Chinese"**: the problem is the engine and the
> prosody, not picking from the voice list.

## 8. Audio-source ledger and the placeholder-audio ban (delivery red line)

**Keep an `audio/provenance.csv` for every project**, recording one row per item:

`file | type (voice/BGM/SFX/ambience) | source (Doubao TTS/ElevenLabs/Kokoro/music library/AI-generated/self-made) | licence status | commercial use allowed | generated at`

**Placeholder audio forbidden from delivery** (it may only exist in drafts and must be replaced before S6):

- **Navigation-style TTS** such as Windows SAPI / Baidu `gettts`
- BGM made with a synth
- Synthesised noise-style SFX (**placeholder only**)
- Kokoro Chinese finals (see the conclusion above)

> RULE: **the picture can be cut, the sound cannot.** Delivering with placeholders unreplaced → the final
> cut is guaranteed to be judged as a "draft / ad".

## 9. Language and content re-verification (**do not only verify "was it read correctly"**)

```powershell
# Transcribe with an explicit language: verifies both "was it read correctly" and "is it actually this language"
& "<TOOLS_DIR>/venv/Scripts/python.exe" "<TOOLS_DIR>/transcribe.py" "final-cut.mp4" -l ko -m medium
```

- **Verify the language**: in a past run `-l ko` reverse transcription reported `ko` at 0.98 confidence,
  which is the only reason it was safe to say "this is Korean voice".
- **Verify the content**: compare the transcript word by word against the script — **every line present, in
  the right order**.
- ⚠️ **Transcription can only prove "read correctly + right language", never "does it sound human"**.
  **Naturalness is only judgeable by human ear**; never assert "the audio quality is fine" to a user on the
  strength of a transcript — that mistake has been made once.

---

## 10. Lessons feedback protocol (**mandatory after every project delivery, 5 minutes**)

> **This section says what to record; how to do it is in the `project-retro` skill** (evidence collection →
> three-way attribution → rule audit → change table → approval → codification).

This skill **accumulates experience project by project**. Everything is accumulated in `LESSONS.md`, with
the same mechanism as the two sibling skills (`short-drama-script`, `video-production-pipeline`).

**When to trigger**: after a project delivery (or when the user says "retrospective" / "sum it up").

**Follow the three retrospective questions, do not improvise**:

1. **What was reworked this time?** (rework = a missing rule, or a rule that exists but did not take effect)
2. **What did it cost?** (re-records / character quota / manual labour) — **anything with a real cost can be
   promoted after a single occurrence**
3. **Which existing rule never fired?** If the rule exists and still did not catch it, the **landing spot is
   wrong**

**Then**: append one row to `LESSONS.md` (`observing`), **do not rush to change the rules**.

| State transition | Threshold |
|---|---|
| `observing` → `promoted` | **reproduced ≥2 times**, or **once with a real cost** (re-record / wasted quota / delivery rejected) |
| `promoted` → `deprecated` | not reproduced across 3 consecutive projects |

**Where to promote it**:

| Nature of the lesson | Landing spot |
|---|---|
| Selection decisions (which engine/voice/model works) | the matching subsection of `SKILL.md` (e.g. §language routing, §Chinese voiceover conclusion) |
| Perceptual conclusions about parameters (reverb/pauses/speed/looping) | `SKILL.md` §7 hard human-feel metrics, or the tuning log in §5/§6 |
| Interface / error codes | `SKILL.md` §0 authentication and error-code quick reference |
| **Mechanically checkable** (duration/level/peak/character count) | currently **no landing spot** — the scripts live under `<TOOLS_DIR>`, not inside this skill (see the known gap in §5) |
| Merely "this timbre did not suit my taste this time" | leave it in `LESSONS.md`, **do not promote** |

> ⚠️ **Three hard constraints**
> 1. **Lessons must not live only in the conversation.** The selection conclusions from a past run (all six
>    test listens failed, Kokoro is not deliverable) were only added to §0 of this skill after the user
>    pressed for it. **A lesson not written into `LESSONS.md` was never accumulated.**
> 2. **Perceptual conclusions must carry a comparison**: writing "it does not sound human" is useless; write
>    "the same text was tried with 6 voices/models and all failed, where ③Sarah ④George ⑤Lily ⑥the newest
>    cloud model all sounded like foreigners speaking Chinese" — that is what turns into a selection rule.
> 3. **Before changing anything, read `LESSONS.md` §5 "the skill's own correction history"** so a conclusion
>    that already burned someone is not reverted.

**Self-check when editing this skill**: this skill **has no executable regression cases yet** (the scripts
are not inside the skill directory, see `LESSONS.md` §5). When changing a rule, fall back to **manual
review**: take a known-good output and confirm the conclusion is not overturned.

---

## Working discipline (three hard rules, **read before changing anything**)

**① Research first** — before creating or heavily changing a skill or process, you must first search both
**existing local skills** and **online (GitHub / plugin marketplace)** for an existing solution, and give
the three-part conclusion: **what was found → adopt / do not adopt → why**.

- A reason not to adopt must be specific (licence restriction / conflict with existing conventions /
  maintenance cost / context burden); "not suitable" is not acceptable
- When a reusable solution is found, **reuse it or borrow only the method**; do not rewrite
- Basis: reproduced three times (audio round "what open-source options exist", script skill "check whether
  something exists before building", retrospective skill "online and locally")

**② Do not expand scope unprompted** — scope expansions the user did not ask for are **proposed only, never
executed**; a proposal must **state the cost explicitly** (skill table +N rows, +X context per turn, how
many new files), and the user decides.

- Exception: when not expanding scope would cause **a wrong conclusion or a safety risk**, stop and explain
  instead of proceeding unilaterally
- Basis: a round trip where "borrow in body-level lint" was followed by "revert it"

**③ Budget first** — **make a budget → report the budget → plan within the approved amount → keep the
ledger → reconcile**, a five-step loop.
**① and ② unfinished means no work starts; ③ not passed means no paid interface may be called.**

| Step | Action | Hard requirement |
|---|---|---|
| ① **Make a budget** | `python <PIPELINE>/scripts/budget_plan.py --budget <amount> --durations <seconds-per-shot> --plan-out <project>/spend-plan.md` | must include a **rework reserve** (default 25%, cannot be skipped) and a **tier lock** |
| ② **Report the budget** | hand the plan to the user and obtain an **explicitly approved amount** | an empty "approved amount" cell in the plan = **not approved = no work starts** |
| ③ **Plan within the amount** | scope / shot count / duration / tier are **derived backwards from the amount** | not enough → **cut scope** (drop tier → fewer shots → shorter duration), **never exceed the budget** |
| ④ **Keep the ledger** | record every line immediately, **including rejected 0-cost items** | planning without recording actuals is where control fails most (a past run came in far over budget) |
| ⑤ **Reconcile** | settle: planned vs actually paid | more than **10%** over plan → stop and re-request approval; **changing tier = changing the plan = re-approval** |

```powershell
# 1) Make a budget (amount -> plan; the price table is reused from cost_preflight.py, single source of truth)
python <PIPELINE>/scripts/budget_plan.py --budget 6 --durations 4,4,4 --anchors 1 --plan-out spend-plan.md
# 2) When only the amount is known, first solve for the maximum number of shots
python <PIPELINE>/scripts/budget_plan.py --budget 6 --max-shots --duration-each 4
# 3) Record / check / gate (exit code 1 when over budget)
python <PIPELINE>/scripts/cost_ledger.py add --ledger <project>/cost_ledger.json --item "clip_01" --cost 1.20
python <PIPELINE>/scripts/cost_ledger.py check --ledger <project>/cost_ledger.json
```

- Measured: on a small budget, **a single main-tier shot of 4 seconds alone cost over twice the whole
  budget** — not costing the work before planning guarantees rework
- Division of labour between the three tools: `budget_plan.py` amount → plan; `cost_preflight.py`
  storyboard → money; `cost_ledger.py` actuals → ledger
- Basis: **most past projects had no ledger at all** in a past audit; one run paid well over its plan
