voiceover · diff
git:20261014.4e5e7e5 to git:20261015.07749c7
58 added, 102 removed. Audit A to A.
---
name: voiceover
- description: Write and produce voiceover in BashCut — measure the voice's speaking rate and the speech-free windows, size each line to its window, prepare the text so text-to-speech reads it right, synthesize takes with a voice.synthesize provider (for example VieNeu-TTS for Vietnamese, preset or cloned voices), check every take against its text, fit it to the picture, re-take one line in place, and dub or localise existing speech. Use for narration, voiceover, fixing a flubbed line without re-recording, dubbing, or when the user says "lồng tiếng", "thuyết minh", "đọc lời", "giọng AI", "clone giọng", "text to speech", "TTS", "dịch lồng tiếng", "lồng tiếng tiếng Anh".
+ description: Voiceover in BashCut: measured speaking rate and narration windows, lines sized to them, text prepared for TTS, takes from a voice.synthesize provider checked against their text, re-takes in place, dubbing. Triggers: "lồng tiếng", "thuyết minh", "đọc lời", "giọng AI", "clone giọng", "TTS", "dịch lồng tiếng", "voiceover".
---
# Voiceover
- Reply in the user's language. Commands measure and act; the numbers below are ranges with their source, to
- choose from with the measurements in hand. Measure → change → measure again.
+ The numbers below are ranges with their source: choose with the measurements in hand, then measure again.
## Provider and voice
- `voice speak` uses the project's `voice.synthesize` provider. Check with `capabilities get voice.synthesize`:
- `reason: missing` → `plugins search --capability voice.synthesize` and name one whose voices speak the content
- language (`project get` › `contentLanguage`; the footage's language when it is not set) and install it (`bc:edit-workflow`);
- `not_configured` → tell the user what the provider's `detail` says (turn it on, trust it again). Until one serves, finish
- the rest of the edit without voiceover (captions or on-screen text carry the lines) and say so in the final summary;
- `unhealthy` → the failing dependency is in `detail`.
-
- ```sh
- bashcut capabilities get voice.synthesize --voices # per voice kind preset|clone, source, language, region, style, gender, supportsRate, measuredRate; projectVoice
- bashcut voice use <clone-id|provider/voice|default> # the project's voice
- bashcut voice speak "<line>" --voice <clone-id|voice> # one request with another voice
- ```
+ Check `capabilities get voice.synthesize --voices`. `reason: missing` → `plugins search --capability
+ voice.synthesize`, install one speaking the content language (`project get` › `contentLanguage`, else the
+ footage's) (`bc:edit-workflow`); `not_configured` or `unhealthy` → quote its `detail`. Until one serves, finish
+ without voiceover (captions or on-screen text carry the lines) and say so in the final summary. `voice use` sets
+ the project's voice; `voice speak --voice` overrides it once.
- - Only voices with `speaksContentLanguage: true` read the script naturally; none → say so and offer a provider that
- has one (or a recorded voiceover). Pick among them by their facts (region, style, measured rate), then audition 2–3 on the **hardest** line,
+ - Only voices with `speaksContentLanguage: true` read the script naturally; none → say so, offer a provider with
+ one or a recorded voiceover. Pick by region, style, measured rate, then audition 2–3 on the **hardest** line,
not the first; the first voice is rarely the pick (T10 §2).
- - Saved clones (`kind: clone`, source `my-voices`) are the user's own voices from My voices, reusable in every
- project; they carry their consent, so `voice speak --voice clone-…` needs no `--clone-consent`. Prefer the
- user's clone when the video is theirs and they speak in it (a talking head, a vlog with their voice); a preset when
- the voice is a narrator, the clone sounds off on the hardest line, or the language differs from the clone's.
- - Making a clone: only the user's own voice or one whose owner agreed, and only when the user asks. Point them to
- Voice panel › + Clone a voice… (record, a file or the selected clip; it picks a clean 3–8 s and checks it), or
- run `bashcut voice clone-add --name "<name>" --from <media|file> [--at S --to S] --consent own|permission`
- with the consent they gave in this conversation; it waits for their approval in the app. A refusal names the
- reason (`tooShort`, `tooQuiet`, `mostlySilence`, `musicUnder`): pick another part, don't retry the same one.
- - The older plugin option `referenceAudio` still clones without saving; a provider that `clones` refuses it without
- `--clone-consent`; pass it only when the user said yes in this conversation, never by default (T10 §6).
- Never mark AI voiceover on the video (no "AI voice" text or sticker); the voice should simply sound natural.
+ - Saved clones (`kind: clone`, from My voices) carry the user's consent: no `--clone-consent`. Prefer the
+ user's clone when the video is theirs and they speak in it; a preset for a narrator, when the clone sounds off
+ on the hardest line, or another language.
+ - Make a clone only when the user asks, of their own voice or one whose owner agreed: Voice panel › + Clone a
+ voice… (picks and checks a clean 3–8 s), or `voice clone-add --consent own|permission` with the consent they
+ gave in this conversation (the app asks them to approve). A refusal names the reason: pick another part, never
+ retry the same one.
+ - The older plugin option `referenceAudio` clones without saving; pass `--clone-consent` only when the user said
+ yes in this conversation, never by default (T10 §6).
+ - Never mark AI voiceover on the video (no "AI voice" text or sticker); the voice should simply sound natural.
## 1. Measure before writing
There is no normal speaking rate: it varies by voice, language, genre and energy (T10 §3). Measure it in the
- content language's unit (syllables for Vietnamese, characters for Chinese/Japanese/Korean, else words):
-
- ```sh
- bashcut speech rate # each speaker in the transcribed footage: p10/p50/p90, overall, articulation
- bashcut speech rate --voice PROVIDER/VOICE # the rates measured on that voice's earlier takes
- ```
+ content language's unit (syllables for Vietnamese, characters for Chinese/Japanese/Korean, else words) with
+ `speech rate` (each speaker in the footage) or `speech rate --voice PROVIDER/VOICE` (that voice's earlier takes);
+ a voice with no measured rate yet: one test line with `voice speak` (nothing is placed), read its `unitsPerSecond`.
- - A voice with no measured rate yet: synthesize one test line (`voice speak "<line>"`, nothing is placed) and read
- its `unitsPerSecond`.
- When the voiceover should sound like the creator, size lines from the creator's own measured rate.
- Never reuse an English words-per-second figure for a syllable language (T10 §3).
- Published Vietnamese rates are a sanity check for a measurement, never a budget. Pauses removed (compare
`articulation`): read speech 4.7–5.3 syllables a second (Bradlow, Interspeech 2019: 4.74; Pellegrino et al.,
Language 2011: 5.22). Pauses inside phrases kept (compare `overall`): 200–245 a minute, 3.3–4.1 a second
(Vu et al., ACL 2025, PhoAudiobook: audiobooks 201; news, YouTube and conversation sets 224–243). A measurement
far outside these: check the transcript for dropped or merged words before sizing lines from it.
- Then find where voiceover can go and how much text fits:
-
- ```sh
- bashcut narration windows --min-seconds S --rate R # speech-free windows; with rate, a unit budget each
- ```
-
- Each window comes with what covers it (music, footage, silence), the shots under it and anchors (last word
- before it, first cut, first beat). Write each line to its window's budget; the p10–p90 spread of the voice tells
- you how much slack to leave. If a line does not fit, **cut text** first; do not plan to speed the voice
- (T02 §4). TTS length drifts from any plan (a 50 s script came out 40–45 s, T10 §3), so the final timing comes
- from the generated take, not from the script.
+ Then `narration windows --min-seconds S --rate R` gives speech-free windows with a unit budget each. Write each
+ line to its budget, with slack from the voice's p10–p90 spread. A line that does not fit: **cut text**, never
+ plan to speed the voice (T02 §4). TTS length drifts (a 50 s script came out 40–45 s, T10 §3): final timing comes
+ from the take, not the script.
## 2. Prepare the text
Spell out what gets misread:
- Numbers, years, times: `2026` → "hai nghìn không trăm hai mươi sáu", `7h30` → "bảy giờ ba mươi". Captions
keep the digits (T10 §2).
- Units and symbols: km, %, °C, `k` (nghìn đồng).
- Acronyms as letters ("xê en xê" for CNC); show them as letters again in captions.
- English or unusual words: rephrase in plain words rather than respelling ("chill" was read "chiều").
- Very short exclamations (≤ 5 words) with an expressive voice fail often; use a calm voice or a longer line.
When a phrase is misread in every take, **rewording beats regenerating** ("kín xe" → "chật cứng xe"). Change
approved text minimally and tell the user which words changed.
## 3. Synthesize
- **A whole script** (an explainer, a narrated ad, any edit whose picture follows the voice): write the lines into
- `plan.cut.voiceover` [{id, text, section, pause}] and voice them all in one job; `plan apply` then plays each line's
- best take one after another, with captions in the script's own words timed by the take:
+ **A whole script** (an explainer, a narrated ad, any edit whose picture follows the voice): lines in
+ `plan.cut.voiceover` [{id, text, section, pause}], all voiced in one job; `plan apply` plays each line's best take
+ with captions in the script's words:
```sh
- bashcut voice lines --takes 2 --min-similarity 0.9 # job: every line voiced, transcribed and checked; kept
- bashcut jobs wait JOB_ID --done --timeout 600 # per line: best {projectPath, similarity, seconds}, unmatched
+ bashcut voice lines --takes 2 --min-similarity 0.9 # job: every line voiced, transcribed and checked
bashcut voice lines --only l3,l7 # after rewording those lines in plan.cut
```
- A line that already has a passing kept take is not voiced again (`--force` does). Choose `--min-similarity` from
- §4. One line here, one sentence: the take is checked against it and the captions break on it.
+ Choose `--min-similarity` from §4. One line, one sentence: the take is checked against it and the captions break
+ on it.
**One line at a time** (a fix, a dub, a line over a fixed shot):
```sh
- bashcut voice speak "Text of one line" --takes 3 # job; measures every take and keeps its file, places none
- bashcut jobs wait JOB_ID --done --timeout 100 # returns when done; every take's facts and file
- bashcut voice place TAKE_PROJECT_PATH --at-frame F # put the take you picked on the Voiceover track
+ bashcut voice speak "Text of one line" --takes 3 # job; measures and keeps every take, places none
+ bashcut voice place TAKE_PROJECT_PATH --at-frame F # the take you picked
bashcut voice speak "Text" --takes 1 --choose 1 --at-frame F # one take, placed at once
```
- - Each take reports `seconds`, `units`, `unitsPerSecond`, `leadingSilence`, `trailingSilence`, `pauses`, the
- provider's `score` when it gives one, and its `path`/`projectPath`. Pick the take yourself, for example the one
- whose `unitsPerSecond` is closest to the rate you sized the line with. Core holds no pace formula and picks none.
- - One line or short paragraph per call, placed at its window; `voice place` is one undoable edit and the item keeps
- its text and the take's provenance.
+ - Pick the take yourself (core picks none), for example the one whose `unitsPerSecond` is closest to the rate you
+ sized the line with. One line or short paragraph per call, placed at its window.
- Give every call a stable `--request-id` (`vo-<section>-<n>`). A paid provider: `--dry-run` first and show the
user its `estimate`; the job's `usage` reports what it charged.
- Takes: 3–6, more for short or expressive lines (T10 §3).
- A take whose rate is far from the voice's measured rate (video-recap flags beyond about ±30 %, T10 §3) has
likely dropped, repeated or invented words: check it first.
- - Style or emotion directives: compare with a neutral take of the same text. One source treats +25 % duration as
- "took effect" and < 5 % as "not applied" (jianshuo, T10 §3); listen as well.
- - Takes you do not place stay in `voiceover/generated`; place them with `voice place`, not `media import` (that
- loses the voice text and the AI provenance).
+ - Style or emotion directives: compare with a neutral take of the same text: +25 % duration "took effect", < 5 %
+ "not applied" (jianshuo, T10 §3).
+ - Unplaced takes stay in `voiceover/generated`; place them with `voice place`, never `media import` (that loses
+ the voice text and the AI provenance).
## 4. Check every kept take
- ```sh
- bashcut voice check --item ITEM # job: similarity, words matched/substituted/missing, extra words
- bashcut voice check --media MEDIA --text "The text it should say"
- bashcut voice check --items all # job: every voiceover item at once, similarity and unmatched words
- ```
-
- The model misreads, drops or repeats words, mostly **at the start** of a line. Read `words` and `extra`, not
- only `similarity`: a repeated word barely moves it. Pass `--min-similarity` only when you want a pass/fail flag
- (one kit gates at 0.92 for Chinese, T10 §3); the call is yours. Recognition also mishears some regional accents
+ `voice check --item ITEM` (or `--media MEDIA --text "…"`, or `--items all`). The model misreads, drops or repeats
+ words, mostly **at the start** of a line. Read `words` and `extra`, not only `similarity`: a repeated word barely
+ moves it. `--min-similarity` only for a pass/fail flag (one kit gates at 0.92 for Chinese, T10 §3); your call. Recognition also mishears some regional accents
(s→x, final n→ng): count only errors a listener would hear. Fix by rewording or a re-take.
## 5. Fit to picture
Fix in order of naturalness (T10 §4, jianshuo ladder):
1. **Shorten or reword the text**, then re-take.
2. **Provider rate**, when the voice `supportsRate` (jianshuo slows −12…−15 % before stretching, T10 §3).
- 3. **Stretch with pitch kept**, within bounds you pass:
- ```sh
- bashcut voice fit --item ITEM --to-frame F --min-ratio MIN --max-ratio MAX --base-rev N
- ```
- Outside the bounds nothing changes and the error gives the speed it would need. Ranges seen: slowing to
- 0.82–0.95 (floors 0.82 jianshuo, 0.85 digitalsamba; imperceptible above ~0.92); speeding up to about 1.1 and
- only after rewriting failed (T10 §3). How much stretch goes unheard varies with the voice's timbre and how
- much music masks it (T10 §3).
+ 3. **Stretch with pitch kept**: `voice fit --item ITEM --to-frame F --min-ratio MIN --max-ratio MAX`, bounds
+ yours. Ranges seen: slowing to 0.82–0.95 (floors 0.82 jianshuo, 0.85 digitalsamba; imperceptible above
+ ~0.92); speeding up to about 1.1 and only after rewriting failed. How much goes unheard varies with the
+ voice's timbre and the music masking it (T10 §3).
4. **Change the text more**, with the user's consent (it changes the captions).
Silence at a line end is often better than a stretched voice. Ranges seen: tail after the last word 0.1–0.5 s,
longer before a cut or reveal; gaps between sentences 0.2–0.8 s, up to 1–3 s after a key line (T10 §3).
- Never over real speech. Keep a gap from real voices (0.3 s worked in our edits); set the project's
`review.voiceoverMarginSeconds` so `review run` checks it.
- - Give each voiceover section b-roll longer than the line: a too-short source clip made the voiceover run into the
- next spoken cut.
- - Captions for voiceover: write them from your text (`bc:captions-text`); `voice speak` times the item's captions
- from the take.
- - Level: judged against the music, not one number. Measure both (`audio measure --media ID`: loudness and
- `presenceShare`) and set the balance with `bc:audio-mix`, which holds the ranges and sources.
+ - Give each voiceover section b-roll longer than the line: a too-short clip made the voiceover run into the next
+ spoken cut.
+ - Captions come from your text, timed by the take (`bc:captions-text`). Level: against the music, not one
+ number (`bc:audio-mix`).
## 6. Re-take one line
- ```sh
- bashcut voice speak --replace ITEM --takes 1 # same text, a new take in place
- bashcut voice speak "Reworded line" --takes 3 # new text: pick a take, then
- bashcut voice place TAKE_PROJECT_PATH --replace ITEM # put it into the item
- ```
-
- The new take keeps the item's place and its captions are timed again. Check it (`voice check --item ITEM`) and
- fit it again if its length changed.
+ `voice speak --replace ITEM --takes 1` (same text), or `voice speak "Reworded line"`, pick, then `voice place
+ TAKE --replace ITEM`. The item keeps its place; captions are re-timed. Check it, and fit again if its length
+ changed.
## 7. Dubbing and localisation
- 1. **Transcribe** the original (`media transcribe --media ID`, then `media transcript --media ID`) and measure
- both rates: the original speaker (`speech rate --media ID`) and the target voice.
+ 1. **Transcribe** the original (`media transcribe`) and measure both rates: the original speaker (`speech rate
+ --media ID`) and the target voice.
2. **Translate** per cue, keeping cue boundaries at sentence ends. Languages differ in length (Mandarin takes
60–80 % of the time Spanish does, jianshuo, T10 §2), so size each cue from the target voice's measured rate.
3. **Sample one cue first**: the most performance-sensitive one, not the first. Get the user's yes on voice and
pace before batching (OpenMontage, T10 §2).
4. **Per cue, fit ladder**: shorten the text → re-take → `voice fit` within bounds → move the cue
- (`timeline move ITEM --track TRACK --at-frame F --base-rev N`) into nearby silence. Tell the user which cues
- were shortened (it changes the meaning) and which moved.
+ (`timeline move`) into nearby silence. Tell the user which cues were shortened (it changes the meaning) and
+ which moved.
5. **Keep the original audio as a bed** under the dub, not muted: jianshuo keeps it at 0.15–0.25 linear
(≈ −16…−12 dB, T10 §3); set it with `volumeDb` and check with `bc:audio-mix`.
6. Check every cue (`voice check`), then re-time captions from the takes.