🎙️ HANDOFF — der M4-Renderer (Sprach-Stems)
Was der M4 macht: aus einem Episoden-Skript geklonte Sprach-Stems erzeugen —
offline, lokal, ohne Netz, ohne Key. Das ist der einzige Schritt der Pipeline, der
Apple Silicon braucht (mlx-audio / OmniVoice). Alles danach läuft auf crumbdesktop (Linux).Was der M4 NICHT macht: normalisieren, Betten synthetisieren, mastern, der systemd-Timer.
Das istcrumcast_autorender.shauf dem Linux-Host (siehe AUTORENDER.md).
🧭 Die Pipeline auf einen Blick — wo der M4 sitzt
✍️ episode_<id>.md (Skript, ausformuliert) ← Mensch/Claude, im Repo
│ ⟶ von Hand nach script.json extrahieren (der fehlende Schritt, s.u.)
▼
📄 raw_stems/episode_<id>[_lang]/script.json ← Eingabe für den M4
│
┌────┴─────────────────────────────────────────┐
│ 🍎 M4: tts_stems.py (mlx-audio/OmniVoice) │ ← DU BIST HIER
│ script.json + Referenz-WAVs │
│ ⟶ raw_stems/episode_<id>/*.wav │ (geklonte Sprach-Stems)
└────┬─────────────────────────────────────────┘
│ git push → Linux-Host zieht
▼
🐧 crumbdesktop: crumcast_autorender.sh (systemd-Timer, alle 10 min)
normalize_stems.sh → render_episode.py
▼
🎧 episodes/episode_<id>[_lang]_master.wav
🍎 Voraussetzungen auf dem M4
- Apple Silicon (M4), Python-venv (
$CRUMCAST_PY, z.B.~/workspace/crumcast/venv/bin/python3). - mlx-audio installiert (OmniVoice TTS und STT). Ref: github.com/Blaizzy/mlx-audio.
ffmpeg(für spätere Schritte; der reine TTS braucht ihn nicht zwingend).- Env (Defaults in
tts_stems.py): MLX_TTS_MODEL— Defaultmlx-community/OmniVoice-bfloat16.MIN_REF_SECONDS— Default3.0(kürzere Referenz → Warnung, kein Abbruch).- Zero-shot Cloning, kein Training. Die eingesprochenen Referenz-Takes sind die Stimme.
- OmniVoice braucht zur Referenz auch den Referenz-Text:
tts_stems.pytranskribiert die
Referenz-WAV automatisch per mlx-audio-STT (gecached). Also muss auch das STT-Modell da sein.
📄 Die Eingabe: script.json (Vertrag)
Ein Manifest pro Episode und Sprache, in raw_stems/episode_<id>[_lang]/script.json.
Beispiel (echt, S00-Zyklus — als Formatvorlage): raw_stems/episode_02/script.json.
{
"prefix": "ep6", // Datei-Prefix der Stems
"lang": "", // "" | "_en" | "_fr" → Suffix am Dateinamen
"voices": { // Charakter → Referenz (Ordner ⟶ 1. Clip, oder Einzel-WAV)
"erzaehler": "speaker_roles_stem/SE00EP00/erzaehler",
"deepbit": "speaker_roles_stem/SE00EP00/deepbit",
"kruemel": "speaker_roles_stem/…/kruemel", // ⚠️ existiert noch nicht — s. Blocker 2
"eule": "speaker_roles_stem/SE00EP00/eule"
},
"lines": [
{"seq": 1, "character": "erzaehler", "slug": "grund-ohne-wald", "text": "…"},
{"seq": 2, "character": "kruemel", "slug": "ist-hier-wer", "text": "…"}
]
}
- Dateiname je Zeile:
{prefix}_{seq:02d}_{character}_{slug}{lang}.wav
→ z.B.ep6_02_kruemel_ist-hier-wer.wav - Reihenfolge =
seq. Der Erzähler-Text und jede Figurenzeile werden je zu einem Stem. - Preflight / fail-fast: vor dem ersten Synth prüft
tts_stems.pyALLE Referenzen
(da? lesbar? lang genug?). Fehlt eine, bricht ein echter Lauf ab, bevor etwas erzeugt wird.
▶️ Lauf-Kommandos (auf dem M4)
# 0) Verdrahtung testen OHNE mlx-audio — erzeugt stille Platzhalter-WAVs
python3 tts_stems.py raw_stems/episode_06 --dry-run
# 1) Echt synthetisieren (script.json im Ordner)
python3 tts_stems.py raw_stems/episode_06
# explizites Manifest / alles neu / anderes Modell
python3 tts_stems.py raw_stems/episode_06 mein.json
python3 tts_stems.py raw_stems/episode_06 --force
MLX_TTS_MODEL=… python3 tts_stems.py raw_stems/episode_06
Danach git add raw_stems/episode_06 && git commit && git push — der Linux-Timer zieht und
rendert beim nächsten Lauf von selbst. (Oder auf dem Host einmal bash crumcast_autorender.sh.)
⚠️ Zwei Blocker, bevor S02 (ep06–10) durch den M4 kann
Blocker 1 — die .md-Skripte sind noch kein script.json
Die ausformulierten S02-Folgen liegen als episodes/episode_06…10_*.md (Regie-Skript mit
Timecodes, Sound-Cues, Erzähler + Dialog). Der M4 braucht aber raw_stems/episode_06…10/script.json.
Zu tun: je Folge die gesprochenen Zeilen (Erzähler + Figuren) aus dem .md in ein script.json
übertragen — Regieanweisungen und *(Musik: …)*-Zeilen weglassen, nur Text. seq in Reihenfolge,
slug aus dem Szenen-/Sinn-Kürzel. Achtung Kollision: raw_stems/episode_02 ist der ALTE
Wald-Zyklus (ep2 „Der Schlüssel"), nicht das neue Nullfeld. Die neuen Folgen sind ep06–10 und
haben noch gar keine raw_stems/-Ordner.
Charakter→Referenz-Mapping für S02 (die Rollen kommen alle aus SE00EP00/, bis auf Krümel):
| Charakter | Referenz | S02-Folgen |
|---|---|---|
erzaehler |
speaker_roles_stem/SE00EP00/erzaehler |
alle |
deepbit |
speaker_roles_stem/SE00EP00/deepbit |
ep06, ep10 |
bugsy |
speaker_roles_stem/SE00EP00/bugsy |
ep07, ep09, ep10 |
eule |
speaker_roles_stem/SE00EP00/eule |
ep07, ep09, ep10 |
funkfox (=Vektor) |
speaker_roles_stem/SE00EP00/funkfox (oder /vektor) |
ep08, ep09, ep10 |
kruemel |
— fehlt — | alle ⚠️ |
Blocker 2 — die Stimme „Krümel" existiert noch nicht
Der Krümel ist in S02 zum ersten Mal eine eigene Sprechrolle (in S00 sprach er nicht selbst).
Es gibt keine Referenz-Aufnahme. Zu tun: einen Krümel-Take einsprechen (≥ MIN_REF_SECONDS,
gern 2–3 kurze Emotionen: neugierig, ängstlich, staunend) und unter
speaker_roles_stem/SE00EP00/kruemel/ ablegen — dann greift das Mapping oben.
Bis dahin: --dry-run erzeugt stille Platzhalter, damit man Naming/Timeline schon testen kann.
🐧 Was danach auf dem Linux-Host noch offen ist (nicht M4, aber wissenswert)
render_episode.pyhat pro Episode eine handgebaute Timeline (build_timeline,
aktuell für ep01: Liste aus(dauer_s, voice_stem, bed_fn, bed_gain)). Für ep06–10 muss diese
Timeline neu geschrieben werden — mit den Stem-Namen aus demscript.jsonund den passenden
Betten. Die Betten sind prozedurale Synth-Funktionen (bed_moos,bed_atmo05,
bed_funkfox_code/score…), nicht direkt die Strudel-Snippets.- Neue Betten für S02:
nullfeld_pulseundsternen_chimegibt es als
crumbstrudel/snippets/*.strudel, aber noch nicht alsbed_*-Funktion im Renderer.
Zwei Wege: (a) alsbed_*-Funktion nachbauen (wiebed_atmo05), oder (b) das Strudel-Snippet
zu einer WAV rendern und perbed_from_wav(path)einbinden. - Mehrsprachig: je Sprache ein eigenes
script.json(_en/_fr) und ein eigener Render mit
passendemsuffix— genau wieraw_stems/episode_02_en|fr/es vormachen.
✅ Checkliste „S02 renderbar machen"
- [ ] Krümel-Referenz einsprechen →
speaker_roles_stem/SE00EP00/kruemel/ - [ ]
raw_stems/episode_06…10/script.jsonaus den.md-Skripten extrahieren (Mapping s.o.) - [ ] M4:
tts_stems.py … --dry-run(Verdrahtung), dann echt; push - [ ] Host:
build_timelinefür ep06–10 + Bettennullfeld_pulse/sternen_chime - [ ] Host:
crumcast_autorender.sh→episodes/episode_06…10_master.wav - [ ] EN/FR: eigene
script.jsonje Sprache, Render mit Suffix