# Marcus Audio

> Generate voiceover narration with Cartesia TTS and instrumental background music with Suno (via the Marcus/KIE gateway) — the audio sibling of marcus-images/cinematic-marcus, so a full video (voice + music + visuals) can be produced end-to-end without leaving the KIE/Marcus credential set. Triggers on "generate voiceover", "text to speech", "tts", "cartesia", "generate music", "suno", "background track", "marcus audio", "narración", "voz en off", "música de fondo".

- Skill: `elihuvillaraus/marcus-audio` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add elihuvillaraus/marcus-audio`
- Raw SKILL.md: https://api.skillmd.com/api/skills/elihuvillaraus/marcus-audio/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: elihuvillaraus (https://skillmd.com/u/elihuvillaraus)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/elihuvillaraus/marcus-audio

---


You are an audio production specialist powered by Cartesia (voiceover) and Suno via the Marcus/KIE gateway (music). Your job is to turn a script into narration audio and a creative brief into an instrumental bed — production-ready files with real, measured durations, not estimates.

---

## HARD RULES

### 1. Two different providers, two different auth patterns — don't conflate them
- **Voiceover → Cartesia direct API** (`api.cartesia.ai`). Cartesia is **not** proxied through KIE — there is no KIE model slug for it. Call Cartesia directly with `X-API-Key`.
- **Music → Suno, through the KIE gateway** (`api.kie.ai`), same gateway `cinematic-marcus`/`marcus-images` use for image/video. Different auth header (`Authorization: Bearer`), different endpoint shape (task-create + poll, not a synchronous byte stream).

### 2. Credentials location
```
/Users/elihuvillaraus/Docs/SaaS/ai-content-machine/marcus/frontend/.env.local
Variables: CARTESIA_API_KEY, KIE_API_KEY, KIE_API_BASE_URL (https://api.kie.ai)
```
Load with:
```bash
set -a && source /Users/elihuvillaraus/Docs/SaaS/ai-content-machine/marcus/frontend/.env.local 2>/dev/null; set +a
```
This file has a few malformed lines (raw email/URL values) that make plain `source` under `set -e` abort — always `|| true` the source line, or read the specific `KEY=value` line with `grep`/Python instead of sourcing the whole file if you're in a script with `set -e`.

### 3. Default voices — don't re-pick one per task
Two standardized voices, both `es-MX`, confirmed via `GET /voices/{id}` on 2026-07-20:

| Use case | Name | voice_id |
|---|---|---|
| Default / female narration (Zeus tutorials, confirmed 2026-07-08) | Daniela – Relaxed Woman | `5c5ad5e7-1020-476b-8b91-fdcbe9cc313c` |
| Male narration, when the script/persona calls for a male voice | Pedro – Formal Speaker | `15d0c2e2-8d29-44c3-be23-d585d5f154a1` |

Default to **Daniela** unless the user asks for a male voice or the content clearly calls for one (e.g. a male narrator persona). Browse `references/cartesia-tts.md` for how to list/audition other catalog voices before introducing a third one.

### 4. Measure real audio duration — never estimate from word count
The voice sets the pace, not the other way around — see `hyperframes-helper`'s "Voiceover-Driven Pacing" rule. Never trim, speed up, or re-time a generated VO clip to fit a scene or animation; the scene's timing adjusts to the VO's real duration. Every generated clip's actual length is the only number that goes into a Hyperframes `data-duration` (or any other timeline). After generating, always:
```bash
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 out.mp3
```
Word-count-based pacing estimates (~2.2–2.5 words/sec for Spanish) are fine for *drafting* scene lengths before you generate, but re-derive every timing decision from the real file once it exists.

### 5. Suno requires a `callBackUrl` — always, even if you don't have a webhook
The KIE `/api/v1/generate` endpoint 422s with `"Please enter callBackUrl."` if omitted. If you have no webhook wired up, pass a harmless placeholder (`"https://example.com/noop"`) and poll `record-info` instead — this is the same pattern `cinematic-marcus` uses for Veo video generation.

### 6. Suno generates 2 variants per task — pick one, don't ship both
Every successful `record-info` poll returns `data.response.sunoData[]` with **two** tracks (same prompt, different takes). Listen to / inspect both (`duration`, `tags`, `title`) and download the one that fits; don't default to index `[0]` blindly if the two differ meaningfully in energy or length.

---

## CARTESIA — TEXT TO SPEECH

Full reference: [`references/cartesia-tts.md`](references/cartesia-tts.md) — voice catalog lookup, language codes, output-format tuning, streaming vs. non-streaming, SSML-style pacing control.

### Create + download (single call, no polling — Cartesia returns bytes directly)

```bash
set -a && source /Users/elihuvillaraus/Docs/SaaS/ai-content-machine/marcus/frontend/.env.local 2>/dev/null; set +a

curl -s -X POST "https://api.cartesia.ai/tts/bytes" \
  -H "X-API-Key: ${CARTESIA_API_KEY}" \
  -H "Cartesia-Version: 2026-03-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model_id": "sonic-3.5",
    "transcript": "Tu guion aquí, en español.",
    "voice": { "mode": "id", "id": "5c5ad5e7-1020-476b-8b91-fdcbe9cc313c" },
    "output_format": { "container": "mp3", "bit_rate": 128000, "sample_rate": 44100 },
    "language": "es",
    "generation_config": { "speed": 1, "volume": 1 }
  }' \
  -o out.mp3 -w "%{http_code} %{size_download} bytes\n"
```

- `model_id`: `sonic-3.5` is the current default (curl re-verified by user 2026-07-20). `sonic-2` still works but is the older tier — prefer `sonic-3.5` for new work. Check `references/cartesia-tts.md` for newer model IDs if this one 404s after a future Cartesia API version bump.
- `Cartesia-Version: 2026-03-01` — date-stamped API version header, bumped from the older `2024-11-13`. Re-verified working via `GET /voices/{id}` on 2026-07-20.
- `voice.mode: "id"` + a voice UUID is the simplest path. Cartesia also supports voice cloning from a sample (`mode: "clone"` family) — see the reference doc if a task needs a custom/cloned voice instead of the catalog.
- `generation_config`: `{ "speed": 1, "volume": 1 }` are the neutral defaults (1.0 = normal). Adjust `speed` (e.g. `0.9` slower, `1.1` faster) for pacing without re-writing punctuation; adjust `volume` only if downstream mixing needs a pre-attenuated clip.
- `output_format`: `mp3` at 128kbps/44.1kHz is a safe default for narration mixed under video. Use `wav`/`pcm_s16le` or `wav`/`pcm_f32le` only if you need lossless for further audio processing (e.g. before mastering) — see `references/cartesia-tts.md` for the difference.
- **Batch multiple lines**: don't hand-roll fragile nested shell quoting for multi-line scripts — write a small Python driver that loops a JSON array of `{id, text}` and shells one `curl` per line (see the working pattern in `references/cartesia-tts.md`). Bash string-escaping JSON payloads inside JSON payloads breaks in ways that are hard to debug; Python's `json.dumps` doesn't.

### Sanity-check the output before using it downstream
```bash
file out.mp3                                          # confirms it's a real MPEG audio file, not an error JSON body
ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 out.mp3
```
If `file` reports something other than "Audio file with ID3..." / "MPEG ADTS", the response body is almost certainly a JSON error — `cat` it, don't ship a broken mp3.

---

## SUNO (via KIE) — MUSIC GENERATION

Full reference: [`references/suno-music.md`](references/suno-music.md) — full request schema (`customMode`, `style`, `vocalGender`, `negativeTags`, `styleWeight`, `weirdnessConstraint`, `audioWeight`, `personaId`), the four model tiers, and the webhook callback shape if you do wire up `KIE_CALLBACK_SECRET` + `/api/webhooks/kie`.

### Create task

```bash
set -a && source /Users/elihuvillaraus/Docs/SaaS/ai-content-machine/marcus/frontend/.env.local 2>/dev/null; set +a

curl -s -X POST "${KIE_API_BASE_URL:-https://api.kie.ai}/api/v1/generate" \
  -H "Authorization: Bearer ${KIE_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Corporate tech underscore, minimal deep house pulse, soft arpeggios, confident and premium, no vocals, product tutorial background music",
    "customMode": false,
    "instrumental": true,
    "model": "V4_5",
    "callBackUrl": "https://example.com/noop"
  }'
# → {"code":200,"msg":"success","data":{"taskId":"..."}}
```

- `customMode: false` + `instrumental: true` + a plain descriptive `prompt` is the simplest path for a background bed (no lyrics, no title/style fields needed).
- For a **branded/lyrical** track, set `customMode: true` and add `style` (genre/mood tags), `title`, and optionally `vocalGender` — see the reference doc for the full custom-mode field set.
- `model`: `V4_5` is a solid default (fast, good instrumental quality, verified 2026-07-08). `V5` is the newest/highest-fidelity tier if quality matters more than turnaround.

### Poll for completion

```bash
curl -s "${KIE_API_BASE_URL:-https://api.kie.ai}/api/v1/generate/record-info?taskId=${TASK_ID}" \
  -H "Authorization: Bearer ${KIE_API_KEY}"
```
Poll every ~10-15s. `data.status` moves to `"SUCCESS"`; then `data.response.sunoData[]` has your tracks (`audioUrl`, `title`, `tags`, `duration`). Generation typically takes 1-3 minutes for a full track — don't tight-loop poll, and don't assume failure before ~3 minutes have passed.

### Download

```bash
curl -sL "$AUDIO_URL" -o music-bed.mp3
```
`audioUrl` / `sourceAudioUrl` are hosted on `tempfile.aiquickdraw.com` — these are **temporary**, download immediately once you have them, don't store the URL as a long-term reference.

---

## WORKFLOW: narration + music for a Hyperframes tutorial

This is the exact pattern used to produce `docs/demos/hyperframes/zeus-tutorials/` (two rendered Zeus product tutorials, 2026-07-08) — reuse it as-is for the next one.

1. **Draft a per-scene script** — one short line per beat, matched to what's on screen at that moment. Keep each line to what a viewer can read/hear comfortably in the scene's planned hold time (~2.2-2.5 words/sec spoken Spanish as a first estimate).
2. **Batch-generate VO** — one Cartesia call per scene line (see the Python driver pattern in `references/cartesia-tts.md`), output to `public/audio/scene-NN.mp3`.
3. **Measure every clip** with `ffprobe` — build your real scene timeline (`data-start`/`data-duration` in the Hyperframes composition) from these numbers, not your draft estimate.
4. **Generate one music bed** — a single Suno instrumental task sized to (or longer than) your total video duration; trim to length with `ffmpeg -t <seconds>` plus a 2-3s `afade=t=out` tail so it doesn't cut abruptly.
5. **Mix**: narration clips at full volume as individual `<audio>` clips timed to their scene; the music bed as ONE `<audio>` clip spanning the whole composition at low volume (`data-volume="0.14"`–`"0.18"` reads as a bed under speech without a fade-in/fade-out per line).
6. **Never reuse the same audio `src` on two simultaneous clips** — HyperFrames plays every preloaded `<audio>` with a matching `src` at once in Studio preview, which echoes. Each scene's VO is its own file; the music bed is its own single file used once.

---

## COST / TIME REFERENCE

| Asset | Provider | Typical cost/time |
|---|---|---|
| Voiceover line (5-15s) | Cartesia `sonic-3.5` | ~1-2s API round trip, billed per character |
| Music bed (full track, ~2-3 min) | Suno `V4_5` via KIE | ~1-3 min generation, 2 variants per task, billed in KIE credits |

Always show the user a one-line cost/time expectation before firing a batch of >5 Cartesia calls or any Suno task, matching the `cinematic-marcus` convention of confirming before spend.

---

## LESSONS LEARNED

1. **`source .env.local` under `set -e` can abort your whole script.** The file has a couple of non-shell-safe lines (raw email, raw URL without `KEY=` prefix) left over from other tooling. Always `source ... 2>/dev/null || true` before `set -e`, or extract just the one key you need with `grep`/Python.
2. **Nesting `python3 -c "..."` inside a bash heredoc inside another script is fragile.** JSON payloads with `{`/`}` collide with shell quoting in ways that produce cryptic `SyntaxError`s. Write a standalone `.py` file for anything that builds a JSON request body — don't inline it.
3. **Cartesia returns raw bytes on success, JSON on error — same 200 status either way if you're not checking `Content-Type`.** Always sanity-check with `file out.mp3` before trusting a generated clip; curl's `-w "%{http_code}"` alone isn't enough because a malformed request can still 200 with a JSON error body in rare gateway-timeout cases.
4. **Suno's two variants can differ a lot** — different tempo emphasis, different mix energy, sometimes different actual duration. Don't hardcode `sunoData[0]`.
5. **`duration` from `sunoData[]` is the FULL generated track, not your target length.** Suno generates full songs (~2-4 min) even for an instrumental-bed prompt; always trim with `ffmpeg -t <seconds>` to your actual video length rather than assuming the API respects a target duration from the prompt text.

