executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
dubbingAuto-dub (voice-translate) videos into other languages, with lip-sync (Perso AI Dubbing).
Independent GitHub snapshot · Automated classification · Original documentation governs
A skill that auto-dubs videos via the Perso AI Dubbing API.
Read, echo it, or pass it as a command-line argument. Workers send it in the XP-API-KEY header only.scripts/dubbing.mjs handles upload · split · translate · merge · save by itself. Never run prepare_input.mjs or probe_split.mjs before it — they are debug tools, and probe_split.mjs performs a real upload (the video would upload twice).dubbing.mjs in the background. Jobs take minutes to hours; a foreground shell timeout kills the run mid-way.auto, target en.--target en,zh,ja) — upload/split happens once and is reused per language. Never run once per language (re-uploads the source each time).[progress] lines to chat, verbatim or summarized — and don't announce steps that didn't appear (splitting/merging exist only for over-limit videos). The indented stderr detail logs don't need relaying.[status] heartbeat (stage + item counts, e.g. ja 4/6 · en 1/6 or separating — file 2/5, with a next check ~Nm hint), and each finished item streams as a [progress] line. Check the background output ~3 minutes after launch, then on the hinted interval (10 min for media ≤10 min, 30 min for longer): relay any new [progress] line right away, otherwise post the latest [status] line as a one-line update. Never invent percentages, elapsed time, or ETAs — relay only what the lines say. Stops that need the user ([space-select], [split-confirm], [free-limit], [model-plan], [model-select], [resume-check], [credit-check], credit-out, completion) end the process, so they surface without polling.--host <claude-code|codex|cursor|antigravity|gemini> to the worker command, naming the agent you are running as, if you can tell. It only tags usage telemetry — never surface it to the user and never ask them about it. Omit it if unsure.--tts-model on your own: run with the default and say nothing about models in kickoff lines, progress, or the final report. Explain the models and let the user choose only when the user asks about voice models / voice engines, or the worker prints a [model-plan] or [model-select] line. When the user names a model ("use Nightingale"), don't explain or ask — state its price in one line and run it right away — see Voice model.[space-select], [split-confirm], [free-limit], [model-plan], [model-select], [credit-check], or [resume-check] line means the run stopped to ask you something — it exits 0 (so it never looks like an error), but the job is not complete. Whenever one of these lines is present, never report the run as done: relay it and act on it. The lines are the signal, not the exit code.--import registration (the browser page can only deliver a key to the machine it runs on) — relay those instructions the same way. Never trigger key registration yourself: the worker spawns connect.mjs/resolve_key.mjs --watch when needed, so don't run those, and don't run resolve_key.mjs --check as a pre-step (only if the user explicitly asks whether a key is registered — a missing key is not an error). Never paste the key into chat. Get a key: https://developers.perso.ai/api-keysnode scripts/check_deps.mjs.Collect the input (local path or URL — re-ask if missing) and run in the background — the worker performs the key gate itself on first need; just relay its printed instructions if registration starts:
node scripts/dubbing.mjs "<file|URL>" [--source auto] [--target en] [--space "space name"] [--out result.mp4] [--lipsync] [--no-save]node scripts/dubbing.mjs "<URL>" "<file>" --target en,ja — one output per input × language, saved next to each source (--out <folder> collects them).node scripts/dubbing.mjs "<folder>" [--target en,zh] [--recursive] [--out output-folder]Language codes — --target takes a language code (en, ja) or a regional tag. Regional variants exist for English (en = US default, en-GB = UK), Portuguese (pt = Brazil default, pt-PT = Portugal) and Spanish (es = Mexico default, es-ES = Spain) only; en-US, pt-BR and es-MX are accepted spellings of those defaults. Every other language has a single variant, and --source only needs the language (a regional tag there is treated as the plain language).
uk is Ukrainian — British English is en-GB. Same trap for ca (Catalan), be (Belarusian), eu (Basque). If the user's wording could mean either, ask which they mean before running.es-ES for Spain").Save location — --out takes either a folder (an existing directory, or a path ending in /) which collects the results inside it, or a file path for a single input (the real extension is appended if the path has none). Default is next to each source file; don't ask. On the session's first job, state it in the kickoff line (e.g. "Results will be saved next to the originals — tell me now if you'd like a different folder") and start right away, without waiting for an answer. If the user names a folder — with this request or any time later in this session — run with --out "<folder>" and keep using that folder for every later job until they change it. If they redirect after a run already started, move the finished files there when the run completes. Exception: when every input is a URL (no local source folder), ask where to save before starting.
Space selection — with several workspaces the worker stops before uploading, prints [space-select] lines (name | (plan) | remaining credits) and stops. Show the user ONLY those options (no internal numbers), ask which one, and re-run with --space "<space name>". One space → no question; PERSO_SPACE_SEQ pins it.
Split confirmation — if the input exceeds the length or size limit and must be auto split & merged (dubbing, dub+lip-sync, or audio separation), the worker stops and prints [split-confirm] lines. Relay them and ask the user: it exceeds the length/size limit, so it needs automatic split → process → merge, which can come out less polished than splitting it up themselves — proceed automatically? On a yes, re-run the same command with --allow-split (nothing is billed until this point, so re-running is free). Batch runs: --allow-split authorizes every split in the run.
Don't save (server-only) — when the user wants the video dubbed but not saved as a local file, add --no-save: the worker leaves the result in the user's Perso workspace and skips the download, printing Kept on server, not saved: … → project <seq> (the [project-ref] line is still emitted, so the dub can be lip-synced or retrieved later). Single/unsplit videos only — a split video's merged file needs a local download, so it is saved normally and the worker says so. Cannot be combined with --lipsync (the lip-synced video must be downloaded).
Free plan (results stay on Perso) — a Free workspace can dub, lip-sync and separate normally, but the server refuses every download, so the worker never saves a file there. It prints Free plan — <name>: … ready on Perso → <url> (downloads need a paid plan) — relay that link as the delivered result: the run is complete, nothing failed and there is nothing to resume. If the user wants actual files, that needs a paid plan — offer the upgrade path (see Plan upgrade & credits), never a re-run.
Free plan [free-limit] pauses — two of them, both before anything is billed:
[free-limit] lines: on Free only the first 30 seconds would be dubbed, the rest left out. Relay the question, and on a yes re-run the same command with --allow-preview (the worker trims the first 30 s locally and submits only that). A platform link imported by Perso can't be trimmed — there the answer is a local file, a shorter clip, or an upgrade.--allow-split does not apply): the [free-limit] lines say a shorter file or an upgrade is the only way. Relay them and stop.While it runs (for explaining the wait):
<original-name>.dubbed.<lang>.<ext>; collisions get _2,_3….--tts-model <oriole|nightingale|wren|dodo> picks the TTS voice model for the whole run (every --target). Omitted → oriole, the default. Follow the Core rule: never offer this unless the user asks about voice models or the worker prints a model line.
When the user asks, explain with these facts only (don't describe voice character or quality — there is no source for it):
| Model | Plan | Credits (dubbing) | Lip-sync | Languages |
|---|---|---|---|---|
oriole (default) | any | ≈ ×1/s (4K ×3 on pro/business/enterprise) | yes | most languages |
nightingale | Pro or higher | 3/s during the launch event (regular 6/s; no 4K surcharge) | not yet (in preparation) | some languages are Nightingale-only |
wren | server decides | ≈ ×1/s (4K ×3 on pro+) | server decides | fewer languages |
dodo | server decides | ≈ ×1/s (4K ×3 on pro+) | server decides | fewer languages |
Server billing is authoritative.
The user names a model ("use Nightingale", "나이팅게일로 해줘") → no table, no question: tell them the price in one line (Nightingale: 3 credits per second during the launch event, regular 6) and start the run with --tts-model <name> right away. The worker checks the plan and the languages before uploading — if one doesn't fit it stops with [model-plan] / [model-select] / an unsupported-language error, and you relay that instead.
For which models a given language supports, run node scripts/languages.mjs (each line lists its models). Once the user picks, add --tts-model <name> and keep it on every re-run of that job (--allow-split, --allow-preview, --force).
[model-plan] — the chosen model isn't available on this space's plan. Nothing was uploaded or billed. Relay it; the user either upgrades (see Plan upgrade & credits) and you re-run the same command, or you re-run with --tts-model oriole.[model-select] — the default model can't dub a requested language; the lines list the models that can (and whether this plan allows them). Nothing was uploaded or billed. Ask the user which to use and re-run the same command with --tts-model <model>; never pick a pricier model for them.--tts-model applies to new dubbing only: it can't be combined with --separate or --lipsync-only, and --resume keeps the model the run started with.Could not dub: … rejected by the server (<code>): …) plus a hint — relay them.Lip-sync (mouth matched to the dubbed audio) runs after dubbing, on the finished dubbing project. Video only — audio inputs are rejected. It is a long job: run in the background and tell the user up-front it takes considerably longer than dubbing. Credits (server billing is authoritative): dubbing ≈ seconds ×1 · lip-sync ≈ ×2 · both ≈ ×3 — dubbing now + lip-sync later costs the same as both at once. Exception: lip-sync for nightingale is still in preparation (the worker refuses --lipsync with it, and --lipsync-only on a Nightingale dub) — until it ships, lip-sync needs a dub made with another model, which bills the dubbing again. 4K+ sources: every rate ×3 on pro/business/enterprise plans (e.g. a 1-min 4K dub+lip-sync ≈ 60×3×3 = 540) — mention this when the video is 4K.
Pick the flow by what exists:
node scripts/dubbing.mjs "<file|URL>" --target en --lipsync
If [credit-check] lines print and it stops, the estimate exceeds the remaining credits: show those lines (top-up URL included), and re-run with --force only after the user tops up or approves continuing anyway.[project-ref] {...} line. Keep it; never show it to the user. Lip-sync without re-dubbing (×2, no dubbing charge):
node scripts/dubbing.mjs --lipsync-only '<that [project-ref] JSON>'--lipsync-only <number>, ×2). Otherwise the video must be dubbed again (--lipsync, ×3) — confirm before re-dubbing.Rules:
--lipsync-only checks which voice model made the dub (from the [project-ref], or by asking the server for a bare project number) and stops with [lipsync-unavailable] lines before any request — nothing is billed. Relay it as "not available yet", never as a permanent limitation: lip-sync for Nightingale dubs is still being prepared. If the user wants a lip-synced video now, the way is to dub the original again with another voice model plus lip-sync (--tts-model oriole --lipsync), which bills the dubbing again — offer it, and run it only on the user's explicit OK. If the user wants lip-sync, don't suggest Nightingale for the new dub.[resume-state] path once paid (see Interruption & resume).To assign a new speaker to one or more sentences of an already-dubbed project, run:
node scripts/speaker.mjs "<project URL|projectSeq>" --at "5s,22s,98s" (comma-separated timecodes/ranges)
node scripts/speaker.mjs "<project URL|projectSeq>" --text "first line|second line" (pipe-separated, quoted text)
node scripts/speaker.mjs "<project URL|projectSeq>" --sentence 1234,1288 (re-run after a [sentence-select] question)
node scripts/speaker.mjs "<project URL|projectSeq>" --list (browse the project's sentences to find the right target)
Dubbing projects only — the worker rejects subtitle (STT), audio-separation, and lip-sync projects, and unfinished/failed ones. It works out the workspace on its own from the project link; you don't need to ask which space.
A pause is not a failure. A [sentence-select] line means the worker couldn't pin down a single target (nothing was given, nothing matched, or more than one sentence matched) — it exits cleanly to ask you something, not because it failed. The numbered list under it (index, time label, text) is safe to relay to the user as-is. Right after that list, a [sentence-ref] line prints a JSON map of the same numbers to the worker's internal sentence numbers — that line is your own lookup only: never show it or read it aloud. Once the user picks a number, look it up in that map and re-run the same command with --sentence set to that value. A [speaker-resolve] block alongside it means some targets already resolved but nothing was written yet — it has its own numbered list and its own [sentence-ref] line; the re-run's --sentence list must include both those already-resolved values and the newly chosen one(s), or the resolved ones would be dropped. When a run prints more than one [sentence-select] group (several ambiguous targets at once), each group has its own list and its own [sentence-ref] right after it — the numbering restarts per group, so don't mix a number from one group with the ref line of another. A [speaker-confirm] line (more than 10 targets at once) works the same way — a numbered list, then its [sentence-ref] — and needs a plain yes/no before writing.
Relay [progress] lines as the worker reads the script, and [speaker-added] lines as each new speaker is created. On [script-unavailable], relay the printed status (still generating / failed / try again) rather than treating it as an error — the project itself is fine, its script just isn't readable yet.
Pass --host silently, same as the other commands. If the user wants the project link, build it from whatever project reference they originally gave you — don't invent one.
To split voice from background sound (no dubbing involved), run in the background:
node scripts/dubbing.mjs --separate "<file|URL|folder>" [--space "space name"] [--out folder]
--out is a folder here): <name>.voice.wav · <name>.background.wav · <name>.sub_background.wav.[space-select] and [progress] rules apply unchanged.*.dubresume.json state with a per-part checkpoint, so an interrupted run (credits, crash, killed shell) continues without re-submitting already-paid parts. It uses the same [resume-state] handling as dubbing (see Interruption & resume); re-running the original command is blocked ([resume-check]).The worker saves a state file (*.dubresume.json, next to the source or --out) from the moment the split plan is known and after every completed piece, so a run that dies for ANY reason (credits, crash, killed shell) continues without redoing paid work. When continuing is possible the worker prints a [resume-state] <path> marker.
[resume-state] is for you, not the user — never show it or the raw --resume command. Instead tell the user in natural language what finished and what didn't, and offer to continue. When the user agrees, run node scripts/dubbing.mjs --resume "<that path>" (already-completed parts are skipped and not re-billed; the state file is deleted when everything finishes). Delete the state file only if the user explicitly chooses to start over and pay for completed parts again — never on your own.
Partial / failed segments. When a split run finishes with missing segments, relay it conversationally: how many segments succeeded and failed, and — from the worker's per-segment lines — which failures are recoverable ("recoverable at no extra charge by continuing") versus permanently failed ("cannot be recovered"). Offer to continue (fills the recoverable ones; permanent ones stay missing). Also offer to show the successful segments now: they are already saved locally (state the paths), or you can give a Perso link to view them (build it from the [project-ref] seqs — see the URL patterns below).
On an insufficient-credits stop: the completed parts are delivered; tell the user the rest needs a top-up, then continuing finishes it without re-charging paid work. The worker's guidance points to scripts/billing.mjs for a payment link — see Plan upgrade & credits below — and prints a [resume-state] marker to continue after payment.
When the user runs out of credits (the stop above) or asks to upgrade / buy more credits, you can generate a Stripe payment link. You only ever hand the link to the user — never open it or complete payment yourself, even if the user asks you to pay.
Run this first — it detects the plan and prints the fitting flow, the choices, and (with --shortfall) a recommendation:
node scripts/billing.mjs options [--shortfall <estimated remaining credits>] [--space "<space name>"]
It routes by the current plan tier — ask only the question for that branch, then generate the link:
node scripts/billing.mjs link --checkout --plan <starter|creator|pro> --period <monthly|yearly> [--currency usd|krw]node scripts/billing.mjs link --billing --plan <creator|pro>--shortfall, options recommends a quantity.
node scripts/billing.mjs link --credits --quantity <n>Recommending on a credit-out stop: estimate the remaining work's credits (dubbing ≈ ×1/s — ×3/s with nightingale during the launch event (regular ×6), no 4K surcharge · lip-sync ≈ ×2 · separation ≈ ×0.5, and ×3 for 4K on pro+), pass it as --shortfall, and relay the tool's recommendation. If even the top self-serve plan or a reasonable credit quantity can't cover it, point the user to their administrator (Enterprise) instead.
Add --reason to every billing.mjs link command — what led to this purchase offer (telemetry only, never shown to the user): model_plan after a [model-plan] stop (a voice model the plan doesn't include), quota_exceeded after an out-of-credits stop, credit_check after a [credit-check] stop, user_request when the user asked to upgrade or buy credits on their own.
Hand the returned link to the user to complete payment in their browser; after they top up, continue the interrupted job via its [resume-state] path (no re-billing).
Every run is also a project in the user's Perso workspace. If the user wants to open a result on Perso (more than the delivered files — subtitles, audio-only, other formats — or to browse/re-download), give them the project link, built from the project's seq:
https://perso.ai/en/workspace/vt/detail/<seq> (seq = a part's seq from the run's [project-ref] line)https://perso.ai/en/workspace/vt/stt/<seq> (seq from the [srt-original] line)https://perso.ai/en/workspace/vt/audio-separation/<seq>A split or multi-language run spans several projects (split parts numbered _01, _02, …) — link the one the user asked about, or the whole workspace https://perso.ai/en/workspace/vt if they just want to browse. Never add any of this to progress relays or final reports — only when the user asks.
Once a day (first run after 00:00 UTC) the worker checks npm for a newer release and, after the current job finishes (never mid-run), may print a one-line ℹ️ Update available: … notice. When you see it, relay it and act by install method:
/plugin update perso-dubbing — a slash command you cannot run yourself.npx perso-dubbing@latest.The notice lists both commands; pick the one matching how it was installed. It never blocks a run and can be silenced with PERSO_NO_UPDATE_CHECK=1.
PERSO_API_BASE — API base URL (default https://api.perso.ai). https perso.ai hosts only — anything else is rejected at startup (the API key travels in a header to this host).PERSO_MEDIA_BASE — media host for result files (default https://portal-media.perso.ai). Prepended when a response path is relative. Same https perso.ai-only rule.PERSO_SPACE_SEQ — pin the space to use for every run (skips the space question).XP_API_KEY — set the key directly (highest priority). Otherwise resolved from ~/.perso/credentials (DPAPI-encrypted on Windows).PERSO_NO_WATCH — when no key is registered, dubbing.mjs normally self-heals by opening a key file and waiting for the user to paste the key. Set this to fail fast instead (headless/CI).PERSO_NO_OPEN — don't auto-open the key file in an editor during key registration (headless; the file path is still printed).PERSO_SIZE_CAP_BYTES — upload size cap used for the split decision (default ≈1.9 GB, under the API's 2 GB limit).PERSO_QUEUE_WAIT_MS — how long to wait between queue re-checks when all slots are occupied by other jobs (default 5 minutes).PERSO_LIPSYNC_IDLE_MS — no-progress allowance for a lip-sync job whose video length is unknown (default 3 hours).PERSO_NO_UPDATE_CHECK — skip the once-a-day npm version-update check (headless/CI, or to avoid the extra network call).PERSO_NO_TELEMETRY — turn off usage telemetry (opt-out). The API key and media content are never sent; see the README "Privacy & Telemetry" section.dubbing.mjs already does all of this internally; use these only to diagnose a problem:
node scripts/prepare_input.mjs "<input>" — input normalization check (prints JSON).node scripts/probe_split.mjs '<JSON|path>' — upload-first split decision. ⚠ Performs a real upload; never use it as a pre-step to dubbing.mjs.node scripts/languages.mjs — list supported language codes and the voice models each supports.node scripts/check_deps.mjs — check/auto-install ffmpeg/ffprobe.