Podcast/Aufnahme → Highlight-Video
Deterministische Arbeit (Audio, API-Aufrufe, Timing-Mathematik, Rendering) wird von Skripten erledigt. Du übernimmst die beiden Beurteilungsschritte: Korrektur des Transkripts (1b) und Auswahl der Highlights (2). Es gibt zwei menschliche Prüfpunkte. Überspringe sie niemals.
SKILL_DIR unten bezeichnet das Verzeichnis, das diese Datei enthält. Führe Skripte mit uv aus, das die eigene gesperrte Umgebung dieses Skills von jedem Arbeitsverzeichnis aus verwendet:
uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<name>.py" <episode_dir>.
In den folgenden Schritten steht <name>.py <ep> kurz für diesen Befehl. Jedes Skript gibt aus, was es erstellt oder übersprungen hat. Erneutes Ausführen ist immer sicher: Abgeschlossene Schritte werden übersprungen und abgeleitete Dateien werden neu erstellt, wenn sich ihre Eingaben ändern. --force wiederholt einen Schritt.
Episodenlayout (ein Verzeichnis pro Episode)
episodes/<slug>/
input.mp4 vom Benutzer bereitgestellt (Audio-Podcast oder Videoaufnahme)
shownote.txt vom Benutzer bereitgestellt (empfohlen: Namen, Produkte, Themen)
episode.json du schreibst es für Videoaufnahmen: {"layout": "stacked"}
audio.mp3, transcript.raw.json Schritt 1a (transcribe.py)
corrections.md Schritt 1b (du schreibst es)
transcript.json Schritt 1b (apply_corrections.py)
clip.draft.json Schritt 2 (du schreibst es)
clip.json Schritt 2 (build_clip.py)
layout.preview.png Schritt 2 (preview_layout.py, Videoaufnahmen)
digest.ass, digest.mp4 Schritt 3 (render.py)
Zwei Arten von Episoden, eine Pipeline
| Audio-Podcast | Videoaufnahme (StreamYard 2er-Ansicht) | |
|---|---|---|
episode.json | keines (oder {"layout": "background"}) | {"layout": "stacked"} |
| Bild | assets/background.png | linkes Sprecherfeld oben, rechtes Feld unten, Untertitel im Streifen dazwischen |
| Ton | audio.mp3 | aus input.mp4 geschnitten, synchron mit dem Bild |
Alles andere (Transkription, Korrekturen, Highlight-Auswahl, Untertitel, Lautstärke) ist gleich. Eine 50-minütige, ~1,5 GB große Aufnahme ist in Ordnung: Nur ihr Audio wird hochgeladen und das Rendering springt direkt zu jedem Highlight.
Schritt 0: Einrichtung
- Führe
check_env.pyaus (kein Episodenargument). Es prüft ffmpeg/libass, den Hintergrund, die Schriftart und obOPENAI_API_KEYin der Shell oder in$SKILL_DIR/.envgesetzt ist. Wenn es mit Exit-Code 1 endet, stoppe und weise den Benutzer auf README → Setup hin.- Lies, drucke,
catodergrepniemals die.env-Datei und bitte den Benutzer niemals, den Schlüssel in den Chat einzufügen. Der Benutzer legt den Schlüssel selbst in.envab:cp .env.example .env, dann bearbeite die Datei.
- Lies, drucke,
- Erstelle
episodes/<slug>/(kurzer ASCII-Slug, z.B.ep042). Kopiere die Aufnahme alsinput.mp4und die Shownotes alsshownote.txtdorthin. Wenn es keine Shownotes gibt, frage den Benutzer nach den Sprechern und wichtigen Namen/Begriffen und schreibe sie selbst inshownote.txt. Verschiebe oder kopiere keine Aufnahme, deren Pfad dir nicht gegeben wurde. - Wenn es sich um eine Videoaufnahme von zwei Personen nebeneinander handelt, schreibe
episode.json:{"layout": "stacked"}. - Führe
run_all.py episodes/<slug>aus. Es führt Schritt 1a aus und stoppt bei Schritt 1b mit Exit-Code 10. Diese Pause ist beabsichtigt. Exit-Code 1 bedeutet einen Fehler: lies ihn.
Schritt 1a: Transkribieren (Skript)
run_all.py hat bereits transcribe.py ausgeführt. Es extrahiert Mono-16-kHz-Audio und ruft whisper-1 mit Wort-Zeitstempeln, language="ja" und einem aus der Shownotes erstellten Vokabel-Prompt auf. Lange Dateien werden automatisch in überlappende Teile aufgeteilt. whisper-1 überspringt manchmal eine Sprachpassage: Lange Pausen zwischen erkannten Wörtern, die noch Ton enthalten, werden einzeln erneut gesendet und die wiederhergestellten Segmente eingefügt (gap ... recovered Zeilen). Um den Prompt zu sehen, führe transcribe.py <ep> --print-prompt aus. Um ihn zu überschreiben, füge Text in <ep>/prompt.txt ein und führe es mit --force erneut aus. Der erneute Durchlauf kostet API-Geld, frage also zuerst den Benutzer.
Schritt 1b: Korrigiere das Transkript (du)
Ziel: Korrigiere falsch erkannte Wörter im text jedes Segments, wobei shownote.txt als Quelle der Wahrheit für Namen, Produkte und Begriffe dient.
-
Lies
shownote.txtvollständig. Liste die darin enthaltenen Namen (Moderatoren, Gäste), Produkte, Unternehmen und technischen Begriffe auf. -
Lies das Transkript:
show_transcript.py <ep> --raw. Lies es bei langen Episoden in Bereichen (--from-id/--to-id) und durchsuche es mit grep nach wahrscheinlichen Fehlhörern jedes Begriffs (--grep クロード --grep claude). Whisper schreibt englische Namen normalerweise in Katakana, buchstabiert sie falsch (Wisper), lässt Buchstaben weg (NTHROPIC) oder wählt das falsche Kanji-Homophon (制度→精度). -
Schreibe
corrections.md: eine Markdown-Tabelle. Jede Zeile ersetzt einen genauen Teilstring in den aufgelisteten Segment(en):| id | before | after | reason | |----|--------|-------|--------| | 6 | クロットコード | Claude Code | Produktname in Shownotes | | 7, 8, 12 | エーデント | エージェント | Fehlerkennung von エージェント |id: Segment-IDs, durch Kommas getrennt, Bereiche erlaubt (10-12).beforemuss im aktuellen Text dieses Segments vorkommen. Zeilen werden der Reihe nach angewendet.- Korrigiere nur Erkennungsfehler. Formuliere nicht um, fasse nicht zusammen, entferne keine Füllwörter und "verbessere" nicht das Japanische. Korrigiere ein offensichtliches Homophon aus dem Kontext und gib "(context)" im
reasonan, wenn die Shownotes es nicht klären. - Wenn ein Segment eine Whisper-Halluzination ist (z.B.「ご視聴ありがとうございました」über Stille), setze es auf leer:
| 41 | ご視聴ありがとうございました | | hallucination |. - Vereine, teile, sortiere oder ändere die Zeitsteuerung von Segmenten niemals. Die Skripte machen dies unmöglich, solange du über
corrections.mdarbeitest. Bearbeitetranscript.jsonnicht von Hand. - Wenn nichts zu korrigieren ist, schreibe nur die Kopfzeile.
-
Führe
apply_corrections.py <ep>aus. Es schreibttranscript.jsonund führtvalidate_transcript.pyaus. Wenn einbeforenicht gefunden wird, korrigiere diese Zeile und führe es erneut aus. -
PRÜFPUNKT 1. Zeige dem Menschen die Korrekturtabelle (oder eine Zusammenfassung davon, plus die mit "(context)" markierten Zeilen) und bitte ihn,
corrections.mdzu überprüfen. Wende seine Änderungen an, indem ducorrections.mdbearbeitest undapply_corrections.pyerneut ausführst. Fahre erst fort, nachdem er zugestimmt hat.
Schritt 2: Wähle die Highlights (du)
Dieser Schritt benötigt die Anweisung des Menschen: entweder ein Thema (「料金の話をしているところ」) oder eine Anfrage nach den besten Teilen (「大事なポイントをまとめて」). Wenn er keines gegeben hat, frage danach. Rate kein Thema.
Ein Kurzclip besteht aus einem oder mehreren Spans (zusammenhängende Teile der Episode), die in chronologischer Reihenfolge abgespielt und mit harten Schnitten verbunden werden. Gesamt: maximal 180 s.
-
Suche:
show_transcript.py <ep> --grep <keyword> ... -C 5mit den Schlüsselwörtern der Anweisung sowie Synonymen und wahrscheinlichen Schreibweisen. Lies dann großzügig um jeden Treffer herum (--from-id/--to-id), um zu sehen, wo jede Diskussion wirklich beginnt und endet. Bei "den wichtigen Punkten" lies zuerst das gesamte Transkript in Bereichen durch und liste die Kandidaten-Momente mit ihren IDs auf, bevor du auswählst. -
Wähle die Spans:
- Ein Span, wenn die Anweisung ein Thema nennt und dessen Diskussion in 180 s passt.
- Mehrere Spans (normalerweise 2–4), wenn die Anweisung nach Highlights fragt oder wenn ein Thema verteilt ist und das dazwischenliegende Gespräch vom Thema abweicht. Jeder Span muss für sich allein und direkt nach dem vorherigen Sinn ergeben: Ein harter Schnitt liefert keinen Kontext.
- Für jeden Span: Beginne dort, wo der Punkt eingeführt wird, nicht mitten im Gedanken oder bei einem Rückbezug (「さっきの」「前回」「それで」). Kürze einleitende Füllwörter oder Übergänge (「では本題です。」「で、」) mit
start_at. Ende mit einem vollständigen Satz; verwendeend_after, um innerhalb eines Segments zu stoppen. - Bevorzuge weniger, vollständige Spans gegenüber vielen Fragmenten; ein Span kürzer als ~8 s steht selten allein.
- Wenn das Material länger als 180 s ist, behalte die stärksten Teile und sage dies im
reason. - Füge bis zu 2
alternatives(andere starke Spans) mit Begründungen hinzu. - Wenn nichts passt, stoppe und teile dem Menschen mit, wonach du gesucht hast und was die Episode stattdessen behandelt. Schreibe keinen Entwurf.
-
Schreibe
clip.draft.json. Du wählst die Segment-IDs. Das Skript erledigt das Timing:{ "instruction": "<die Anweisung des Menschen, wörtlich>", "title": "スキル機能と文字起こしの要点", "reason": "Warum diese Teile als Ganzes.", "spans": [ { "start_id": 6, "end_id": 8, "start_at": "今日は、Claude Codeのスキル機能について", "title": "スキル機能とは", "reason": "Führt das Thema ein." }, { "start_id": 12, "end_id": 12, "title": "分担が大事", "reason": "Der Kernpunkt." } ], "alternatives": [ { "start_id": 120, "end_id": 131, "title": "...", "reason": "..." } ] }start_atundend_aftersind optionale exakte Teilstrings des Textes des ersten und letzten Segments eines Spans. Alternativen akzeptieren sie ebenfalls. Jeder Span braucht einreason, wenn es mehrere gibt. (Ein Ein-Span-Entwurf kannstart_id/end_id/... auch auf der obersten Ebene setzen.) -
Führe
build_clip.py <ep>aus. Es setzt jeden Grenzwert auf Wort-Zeitstempel, fügt etwa 0,3 s Polsterung hinzu (niemals über ein benachbartes Wort hinaus), sortiert die Spans, prüft, dass sie sich nicht überlappen und die Gesamtlänge ≤ 180 s beträgt, schreibtclip.jsonund validiert es. Es listet jeden Span mit seinem Zeitbereich und seiner Länge auf. Lies allewarn-Zeilen, die es ausgibt. Benachbarte Spans (sich berührende Segment-IDs) gelten als überlappend: Fasse sie zu einem Span zusammen. -
Nur bei Videoaufnahmen: Führe
preview_layout.py <ep>aus und betrachtelayout.preview.png(undlayout.source.png, wo die Felder rot = oben, blau = unten umrandet sind). Jedes Panel muss eine Person zeigen, zentriert, ohne Bildrand oder benachbartes Feld. Wenn nicht, setze"tiles"inepisode.json(Bruchteile des Quellframes) und führe es erneut aus:{"layout": "stacked", "tiles": [{"x": 0.0, "y": 0.14, "w": 0.497, "h": 0.70}, {"x": 0.504, "y": 0.14, "w": 0.496, "h": 0.70}]}Das erste Feld kommt nach oben. Vertausche die beiden Einträge, um die Sprecher zu tauschen. Aufnahmen aus demselben Studio-Setup teilen dieselben Felder, also verwende ein funktionierendes
episode.jsonwieder. -
PRÜFPUNKT 2. Zeige dem Menschen den Plan: den Gesamttitel und die Gesamtlänge, dann jeden Span in der Reihenfolge mit Titel,
mm:ss–mm:ss, Dauer, Grund und seinen ersten und letzten Sätzen (show_transcript.py <ep> --from-id A --to-id B), dann die Alternativen. Zeige bei Video auchlayout.preview.png. Bitte ihn, die Spans zu genehmigen, zu tauschen, hinzuzufügen oder zu entfernen.build_clip.py <ep> --promote N: Bei einem Span wird Alternative N eingefügt; bei mehreren wird Alternative N als weiterer Span hinzugefügt (beachte die 180 s Gesamtlänge).- Jede andere Änderung: Bearbeite
clip.draft.jsonund führebuild_clip.pyerneut aus. - Fahre erst fort, nachdem er zugestimmt hat.
Schritt 3: Rendern (Skript)
Führe run_all.py <ep> --render (oder render.py <ep>) aus. Ausgabe: digest.mp4, 1080x1920, 30 fps, H.264 + AAC, normalisiert auf -14 LUFS, mit eingebrannten Untertiteln aus digest.ass. Die Spans werden mit harten Schnitten verbunden. Jeder Schnitt erhält eine 50 ms Audio-Überblendung, damit es nicht knackt.
Überprüfe dann das Ergebnis:
ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4muss mit der Gesamtlänge übereinstimmen, diebuild_clip.pyausgegeben hat.- Extrahiere einen Frame aus jedem Span und betrachte ihn, damit du weißt, dass die Untertitel gerendert werden, keine Tofu-Boxen sind und (bei Video) die Panels richtig eingerahmt sind:
ffmpeg -y -v error -ss 5 -i <ep>/digest.mp4 -frames:v 1 /tmp/frame.png. - Wenn das Rendering gewarnt hat, dass die Schriftart
Noto Sans JPnicht gefunden wurde, informiere den Benutzer (README → Setup).
Melde den Pfad zu digest.mp4, den Titel und den Quellzeitbereich jedes Spans.
Fehlerbehebung
| Symptom | Lösung |
|---|---|
OPENAI_API_KEY is not set | Der Benutzer fügt ihn zu $SKILL_DIR/.env hinzu (Vorlage .env.example) oder exportiert ihn. Behandle den Schlüssel niemals selbst |
before ... not found | before muss ein exakter Teilstring des aktuellen Textes dieses Segments sein (frühere Zeilen können ihn geändert haben) |
segments changed but not listed | Jede geänderte Segment-ID muss in einer Zeile von corrections.md erscheinen |
spans total ... > 180s / exceeds 180s | Entferne einen Span, wähle kürzere oder kürze mit start_at / end_after |
| Sprachpassagen fehlen im Transkript (ein großer Zeitsprung, wo Leute sprechen) in einem Transkript, das vor der Lückenwiederherstellung erstellt wurde | transcribe.py <ep> --fill-gaps sendet nur die Lücken erneut. Es gibt aus, wie sich IDs verschieben: Nummeriere corrections.md / clip.draft.json entsprechend um, dann apply_corrections.py <ep>. Leere wiederhergestellte 「ご視聴ありがとうございました」 als Halluzination |
| Ein zweiter Kurzclip aus derselben Episode | Benenne die fertige digest.mp4, digest.ass, clip.json, clip.draft.json um (z.B. digest-keypoints.mp4), schreibe dann einen neuen clip.draft.json |
spans overlap | Zwei Spans teilen oder berühren Segmente: Fasse sie zu einem Span zusammen |
opens with 'さっき' (warn) | Beginne später oder kürze mit start_at, damit der Span für sich allein steht |
| Panels zeigen den falschen Bereich / einen Rand | Korrigiere tiles in episode.json, überprüfe mit preview_layout.py, dann render.py <ep> |
layout 'stacked' but input.mp4 has no video stream | Es ist ein Audio-Podcast: Entferne episode.json |
| Untertitel in einer Ersatz- oder dünnen Schriftart | assets/fonts/NotoSansJP-Bold.ttf muss existieren (eine statische Bold-Datei, nicht die variable Schriftart); siehe README → Setup |
Geänderte background.png / Schriftarteinstellungen | render.py <ep> bemerkt dies und rendert neu; --force zur Sicherheit |