Voice Clone
Two capabilities, one skill:
- Clone a narrator's voice with MiniMax voice-cloning → a reusable
voice_id saved in the avatar folder.
- Generate new speech in that cloned voice with MiniMax
speech-2.8-hd,
saved under <avatar>/generated-audios/ with a manifest.
The clone input is a clean voice file — ideally voice_concat.mp3 produced by the
voice-isolate skill.
Requirements
pip3 install -r requirements.txt (replicate client + langid for language
detection).
- A Replicate API token. It is shared with the other Replicate skills
(avatar-video-reel, gpt-image-2, bg-music, …) and discovered automatically.
To set/refresh it:
python3 scripts/setup_key.py YOUR_REPLICATE_API_TOKEN.
- The voice file must be MP3/M4A/WAV, 10s–5min, <20MB.
- A local tunnel —
cloudflared (preferred) or ngrok — installed on
PATH (only for cloning, not for generation). brew install cloudflared
(no account needed) or ngrok config add-authtoken <token> once. See How the
upload works below.
How the upload works
minimax/voice-cloning re-fetches the audio from MiniMax's own servers, so it
needs a public URL with a real extension — a raw file object or Replicate's
auth-protected upload both fail with invalid file ext. To avoid uploading your
voice to a third party, the skill serves the file straight from your machine
over a short-lived tunnel that is torn down as soon as the clone finishes:
- Prefer cloudflared quick tunnel (no account, ephemeral per-run URL), then
fall back to ngrok, then — only if neither is installed — a temporary
public host (
tmpfiles.org / catbox.moe).
- Reachability is verified the way MiniMax sees it: if the local resolver blocks
the tunnel domain (some ISPs filter
*.trycloudflare.com), it re-checks via
public DNS (1.1.1.1 / 8.8.8.8) so a blocked local resolver is not mistaken
for an unreachable URL.
Clone a voice
python3 scripts/clone_voice.py <video>_voice/voice_concat.mp3
That's it — everything else is auto:
- Model:
speech-2.6-hd by default (the current HD model; MiniMax may serve
it on a newer HD engine). Override with --model (speech-2.6-turbo,
speech-02-hd, speech-02-turbo).
- Defaults match the model:
--accuracy 0.7, noise reduction off, volume
normalization off (the input is already clean). Flags: --noise-reduction,
--volume-normalization.
- Where it saves (auto-inferred):
- Source
name: if the file is <stem>_voice/voice_concat.mp3 → <stem>;
otherwise the file's own stem. Override with --name.
- Avatar dir: the folder containing a
videos/ directory (e.g. lolo/).
Override with --avatar-dir.
Output (in <avatar>/voices/)
| File |
What it is |
<name>.json |
The record: voice_id, model, source, preview_url, created_at |
<name>_preview.mp3 |
Preview clip of the cloned voice (skip with --no-preview) |
index.json |
Registry mapping every name → voice_id for this avatar |
The voice_id is what you pass to MiniMax text-to-speech to synthesize new
speech in this cloned voice — which is exactly what generate_speech.py does.
Report the voice_id and the path to <name>.json when done.
Generate speech (TTS)
Synthesize new audio in the avatar's cloned voice with minimax/speech-2.8-hd.
# Reuse the avatar's trained voice (auto-detects language, here Spanish):
python3 scripts/generate_speech.py "Hola, soy Lolo" --avatar-dir lolo
# Train automatically first if the avatar has no voice yet:
python3 scripts/generate_speech.py "Hello!" --source lolo/videos/clip_voice/voice_concat.mp3
# Pick an emotion explicitly:
python3 scripts/generate_speech.py "Great news!" --avatar-dir lolo --emotion happy
Voice resolution (automatic):
--voice-id if given.
- Else the avatar's already-trained voice (
<avatar>/voices/). With one
trained voice it's picked automatically; with several, pass --name.
- Else, if
--source is given, it trains one first (a clean voice file, or
a video whose <stem>_voice/voice_concat.mp3 exists), then generates.
- Else it errors asking for
--source or --voice-id.
Key options:
--emotion (default auto): auto, happy, sad, angry, fearful,
disgusted, surprised, calm, fluent, neutral. The agent should choose
one that fits the line when appropriate.
language_boost defaults to None (no boost) so the cloned voice
keeps its own accent. Boosting a language nudges pronunciation toward a
"standard"/regional accent that can fight the clone — e.g. a neutral or Chilean
voice drifting into Argentinian voseo. Pass --language-boost detect to
auto-detect from the text (Unicode script for CJK/Cyrillic/Arabic/… + langid
for Latin scripts), or a locale (Spanish, English, …) only when you
specifically need that pronunciation help.
- Audio:
--speed, --volume, --pitch, --audio-format (mp3/wav/flac/pcm),
--sample-rate, --bitrate, --channel, --english-normalization.
- Text: pass inline or via
--text-file script.txt.
Expressive interjections & pauses
speech-2.8-hd renders expressive interjections written inline in the text,
so the voice doesn't sound flat. Drop them right where they happen:
python3 scripts/generate_speech.py "Lo logramos (laughs softly)… (sighs) y por fin puedo soltar." --avatar-dir lolo
- Recognized (common, reliably-rendered) set:
(laughs), (laughs softly),
(chuckles), (giggles), (sighs), (gasps), (coughs), (clears throat),
(sneezes), (sniffs), (groans), (yawns), (whistles), (humming),
(hums), (exhales), (inhales), (breathes), (gulps), (crying),
(sobs), (screams), (applause). The model recognizes 20+; run
python3 scripts/generate_speech.py --list-interjections to print them.
- The script logs the interjections it detects and warns about any other
parenthesized text (which may otherwise be read out literally). Detected
interjections are recorded in
manifest.json per clip.
- Manual pauses:
<#x#> inserts x seconds of silence (0.01–99.99), e.g.
"Respira hondo <#0.6#> y continúa." — useful for beats and emphasis.
- Use interjections sparingly — one or two per passage reads as natural;
overusing them sounds theatrical. Pair with
--emotion for the overall tone.
Long narrations: synthesize per sentence
The model degrades on very long single takes (its own docs recommend short
sentences for smoother delivery). For multi-paragraph scripts, synthesize one
sentence at a time and join the clips rather than sending everything in one
call. The avatar-reel-composer skill's narrate.py does exactly this (one
MiniMax call per sentence + a small silence gap); reuse that flow for reels.
Output (in <avatar>/generated-audios/)
| File |
What it is |
<NNN>_<slug>.<ext> |
The generated audio clip (auto-numbered) |
manifest.json |
items[] mapping each file → text, voice_id, voice_name, emotion, language_boost, and synth params |
Report the audio path, the voice_id used, and the detected language_boost.
Notes
- One clone per source recording: re-running with the same
name overwrites its
record and updates index.json.
- The clone quality depends on the input. Use the clean, SFX-free
voice_concat.mp3 from voice-isolate for best results; keep
--noise-reduction off when the audio is already clean.
- Generation never re-uploads the voice sample (it only sends the
voice_id +
text), so it needs no tunnel and is fast.
- A trained
voice_id works across MiniMax speech models, so the voice trained
with speech-2.6-hd is used for speech-2.8-hd generation.
1---2name: voice-clone3description: Clone a narrator's voice from a clean voice audio file using MiniMax voice-cloning on Replicate, then generate new speech (TTS) in that cloned voice with MiniMax speech-2.8-hd. Cloning takes the clean voice MP3/WAV (e.g. voice_concat.mp3 from the voice-isolate skill), trains a voice (default speech-2.6-hd) and saves voice_id + a preview under <avatar>/voices/<name>.json. Generation reuses the avatar's trained voice (or trains one if missing), auto-detects the text language for MiniMax's language_boost, supports per-line emotion and expressive interjections ((laughs), (sighs), …) plus manual <#x#> pauses, and saves audio under <avatar>/generated-audios/ with a manifest.json. Use when the user wants to clone a voice, create/train a TTS voice, get a voice_id, or GENERATE speech / narration / audio in a cloned voice, or mentions "clonar la voz", "voice clone", "voice_id", "entrenar la voz", "generar audio", "text to speech", or "TTS con la voz".4---56# Voice Clone78Two capabilities, one skill:9101. **Clone** a narrator's voice with **MiniMax voice-cloning** → a reusable11 **`voice_id`** saved in the avatar folder.122. **Generate** new speech in that cloned voice with **MiniMax `speech-2.8-hd`**,13 saved under `<avatar>/generated-audios/` with a manifest.1415The clone input is a clean voice file — ideally `voice_concat.mp3` produced by the16`voice-isolate` skill.1718## Requirements1920- `pip3 install -r requirements.txt` (`replicate` client + `langid` for language21 detection).22- A Replicate API token. It is **shared** with the other Replicate skills23 (avatar-video-reel, gpt-image-2, bg-music, …) and discovered automatically.24 To set/refresh it: `python3 scripts/setup_key.py YOUR_REPLICATE_API_TOKEN`.25- The voice file must be **MP3/M4A/WAV, 10s–5min, <20MB**.26- A local tunnel — **`cloudflared`** (preferred) or **`ngrok`** — installed on27 PATH (**only for cloning**, not for generation). `brew install cloudflared`28 (no account needed) or `ngrok config add-authtoken <token>` once. See *How the29 upload works* below.3031## How the upload works3233`minimax/voice-cloning` re-fetches the audio from MiniMax's own servers, so it34needs a **public URL with a real extension** — a raw file object or Replicate's35auth-protected upload both fail with `invalid file ext`. To avoid uploading your36voice to a third party, the skill serves the file **straight from your machine**37over a short-lived tunnel that is torn down as soon as the clone finishes:38391. Prefer **cloudflared** quick tunnel (no account, ephemeral per-run URL), then40 fall back to **ngrok**, then — only if neither is installed — a temporary41 public host (`tmpfiles.org` / `catbox.moe`).422. Reachability is verified the way MiniMax sees it: if the local resolver blocks43 the tunnel domain (some ISPs filter `*.trycloudflare.com`), it re-checks via44 public DNS (1.1.1.1 / 8.8.8.8) so a blocked *local* resolver is not mistaken45 for an unreachable URL.4647## Clone a voice4849```bash50python3 scripts/clone_voice.py <video>_voice/voice_concat.mp351```5253That's it — everything else is auto:5455- **Model**: `speech-2.6-hd` by default (the current HD model; MiniMax may serve56 it on a newer HD engine). Override with `--model` (`speech-2.6-turbo`,57 `speech-02-hd`, `speech-02-turbo`).58- **Defaults** match the model: `--accuracy 0.7`, noise reduction off, volume59 normalization off (the input is already clean). Flags: `--noise-reduction`,60 `--volume-normalization`.61- **Where it saves** (auto-inferred):62 - Source `name`: if the file is `<stem>_voice/voice_concat.mp3` → `<stem>`;63 otherwise the file's own stem. Override with `--name`.64 - Avatar dir: the folder containing a `videos/` directory (e.g. `lolo/`).65 Override with `--avatar-dir`.6667## Output (in `<avatar>/voices/`)6869| File | What it is |70|------|------------|71| `<name>.json` | **The record**: `voice_id`, `model`, `source`, `preview_url`, `created_at` |72| `<name>_preview.mp3` | Preview clip of the cloned voice (skip with `--no-preview`) |73| `index.json` | Registry mapping every `name` → `voice_id` for this avatar |7475The `voice_id` is what you pass to MiniMax text-to-speech to synthesize new76speech in this cloned voice — which is exactly what `generate_speech.py` does.7778Report the `voice_id` and the path to `<name>.json` when done.7980## Generate speech (TTS)8182Synthesize new audio in the avatar's cloned voice with **`minimax/speech-2.8-hd`**.8384```bash85# Reuse the avatar's trained voice (auto-detects language, here Spanish):86python3 scripts/generate_speech.py "Hola, soy Lolo" --avatar-dir lolo8788# Train automatically first if the avatar has no voice yet:89python3 scripts/generate_speech.py "Hello!" --source lolo/videos/clip_voice/voice_concat.mp39091# Pick an emotion explicitly:92python3 scripts/generate_speech.py "Great news!" --avatar-dir lolo --emotion happy93```9495Voice resolution (automatic):96971. `--voice-id` if given.982. Else the avatar's **already-trained** voice (`<avatar>/voices/`). With one99 trained voice it's picked automatically; with several, pass `--name`.1003. Else, if `--source` is given, it **trains one first** (a clean voice file, or101 a video whose `<stem>_voice/voice_concat.mp3` exists), then generates.1024. Else it errors asking for `--source` or `--voice-id`.103104Key options:105106- **`--emotion`** (default `auto`): `auto`, `happy`, `sad`, `angry`, `fearful`,107 `disgusted`, `surprised`, `calm`, `fluent`, `neutral`. The agent should choose108 one that fits the line when appropriate.109- **`language_boost`** defaults to **`None`** (no boost) so the **cloned voice110 keeps its own accent**. Boosting a language nudges pronunciation toward a111 "standard"/regional accent that can fight the clone — e.g. a neutral or Chilean112 voice drifting into Argentinian *voseo*. Pass `--language-boost detect` to113 auto-detect from the text (Unicode script for CJK/Cyrillic/Arabic/… + `langid`114 for Latin scripts), or a locale (`Spanish`, `English`, …) only when you115 specifically need that pronunciation help.116- Audio: `--speed`, `--volume`, `--pitch`, `--audio-format` (mp3/wav/flac/pcm),117 `--sample-rate`, `--bitrate`, `--channel`, `--english-normalization`.118- Text: pass inline or via `--text-file script.txt`.119120### Expressive interjections & pauses121122`speech-2.8-hd` renders **expressive interjections** written inline in the text,123so the voice doesn't sound flat. Drop them right where they happen:124125```bash126python3 scripts/generate_speech.py "Lo logramos (laughs softly)… (sighs) y por fin puedo soltar." --avatar-dir lolo127```128129- Recognized (common, reliably-rendered) set: `(laughs)`, `(laughs softly)`,130 `(chuckles)`, `(giggles)`, `(sighs)`, `(gasps)`, `(coughs)`, `(clears throat)`,131 `(sneezes)`, `(sniffs)`, `(groans)`, `(yawns)`, `(whistles)`, `(humming)`,132 `(hums)`, `(exhales)`, `(inhales)`, `(breathes)`, `(gulps)`, `(crying)`,133 `(sobs)`, `(screams)`, `(applause)`. The model recognizes **20+**; run134 `python3 scripts/generate_speech.py --list-interjections` to print them.135- The script **logs** the interjections it detects and **warns** about any other136 parenthesized text (which may otherwise be read out literally). Detected137 interjections are recorded in `manifest.json` per clip.138- **Manual pauses:** `<#x#>` inserts `x` seconds of silence (0.01–99.99), e.g.139 `"Respira hondo <#0.6#> y continúa."` — useful for beats and emphasis.140- Use interjections **sparingly** — one or two per passage reads as natural;141 overusing them sounds theatrical. Pair with `--emotion` for the overall tone.142143### Long narrations: synthesize per sentence144145The model degrades on very long single takes (its own docs recommend short146sentences for smoother delivery). For multi-paragraph scripts, synthesize **one147sentence at a time and join the clips** rather than sending everything in one148call. The `avatar-reel-composer` skill's `narrate.py` does exactly this (one149MiniMax call per sentence + a small silence gap); reuse that flow for reels.150151### Output (in `<avatar>/generated-audios/`)152153| File | What it is |154|------|------------|155| `<NNN>_<slug>.<ext>` | The generated audio clip (auto-numbered) |156| `manifest.json` | `items[]` mapping each file → `text`, `voice_id`, `voice_name`, `emotion`, `language_boost`, and synth params |157158Report the audio path, the `voice_id` used, and the detected `language_boost`.159160## Notes161162- One clone per source recording: re-running with the same `name` overwrites its163 record and updates `index.json`.164- The clone quality depends on the input. Use the clean, SFX-free165 `voice_concat.mp3` from `voice-isolate` for best results; keep166 `--noise-reduction` off when the audio is already clean.167- Generation never re-uploads the voice sample (it only sends the `voice_id` +168 text), so it needs **no tunnel** and is fast.169- A trained `voice_id` works across MiniMax speech models, so the voice trained170 with `speech-2.6-hd` is used for `speech-2.8-hd` generation.