Communitygithub.com

podcast-digest

Erstellt aus einer japanischen Podcast-Episode oder einer aufgezeichneten Zwei-Personen-Video-Call-Aufnahme (z. B. StreamYard) einen vertikalen 9:16-Kurzclip von bis zu 180 Sekunden mit eingebrannten japanischen Untertiteln.

Was ist podcast-digest?

Mit dem Skill 'podcast-digest' kannst du aus japanischen Podcasts oder Videoaufnahmen (z. B. StreamYard) automatisch kurze Highlight-Clips im vertikalen 9:16-Format erstellen. Der Skill erkennt die wichtigsten Stellen auf Basis einer menschlichen Anweisung – entweder ein konkretes Thema („die Stelle, wo wir über X sprechen“) oder allgemeine Highlights („die wichtigsten Punkte“). Mehrere Ausschnitte werden durch harte Schnitte zu einem Clip von maximal 180 Sekunden verbunden und mit japanischen Untertiteln versehen. Der Workflow ist deterministisch: Skripte übernehmen die Transkription (OpenAI Whisper-1), die Zeitsteuerung und das Rendering (ffmpeg/libass). Du als Benutzer übernimmst zwei wichtige Korrekturschritte: die Transkriptionskorrektur (1b) und die Auswahl der Highlights (2). Der Skill unterstützt sowohl reine Audio-Podcasts als auch Videoaufnahmen mit zwei nebeneinander angezeigten Sprechern (Stacked Layout). Die Ausgabe ist eine MP4-Datei (digest.mp4) mit normalisierter Lautstärke und eingebrannten Untertiteln. Er eignet sich perfekt für die Erstellung von Zusammenfassungen, Reels, Clips oder Kürzungen („切り抜き動画“) aus japanischen Podcasts oder Aufnahmen.

Funktioniert mit✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/tomoima525/podcast-digest-skill/tree/HEAD

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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-PodcastVideoaufnahme (StreamYard 2er-Ansicht)
episode.jsonkeines (oder {"layout": "background"}){"layout": "stacked"}
Bildassets/background.pnglinkes Sprecherfeld oben, rechtes Feld unten, Untertitel im Streifen dazwischen
Tonaudio.mp3aus 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

  1. Führe check_env.py aus (kein Episodenargument). Es prüft ffmpeg/libass, den Hintergrund, die Schriftart und ob OPENAI_API_KEY in der Shell oder in $SKILL_DIR/.env gesetzt ist. Wenn es mit Exit-Code 1 endet, stoppe und weise den Benutzer auf README → Setup hin.
    • Lies, drucke, cat oder grep niemals die .env-Datei und bitte den Benutzer niemals, den Schlüssel in den Chat einzufügen. Der Benutzer legt den Schlüssel selbst in .env ab: cp .env.example .env, dann bearbeite die Datei.
  2. Erstelle episodes/<slug>/ (kurzer ASCII-Slug, z.B. ep042). Kopiere die Aufnahme als input.mp4 und die Shownotes als shownote.txt dorthin. Wenn es keine Shownotes gibt, frage den Benutzer nach den Sprechern und wichtigen Namen/Begriffen und schreibe sie selbst in shownote.txt. Verschiebe oder kopiere keine Aufnahme, deren Pfad dir nicht gegeben wurde.
  3. Wenn es sich um eine Videoaufnahme von zwei Personen nebeneinander handelt, schreibe episode.json: {"layout": "stacked"}.
  4. 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.

  1. Lies shownote.txt vollständig. Liste die darin enthaltenen Namen (Moderatoren, Gäste), Produkte, Unternehmen und technischen Begriffe auf.

  2. 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 (制度→精度).

  3. 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). before muss 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 reason an, 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.md arbeitest. Bearbeite transcript.json nicht von Hand.
    • Wenn nichts zu korrigieren ist, schreibe nur die Kopfzeile.
  4. Führe apply_corrections.py <ep> aus. Es schreibt transcript.json und führt validate_transcript.py aus. Wenn ein before nicht gefunden wird, korrigiere diese Zeile und führe es erneut aus.

  5. PRÜFPUNKT 1. Zeige dem Menschen die Korrekturtabelle (oder eine Zusammenfassung davon, plus die mit "(context)" markierten Zeilen) und bitte ihn, corrections.md zu überprüfen. Wende seine Änderungen an, indem du corrections.md bearbeitest und apply_corrections.py erneut 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.

  1. Suche: show_transcript.py <ep> --grep <keyword> ... -C 5 mit 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.

  2. 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; verwende end_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.
  3. 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_at und end_after sind optionale exakte Teilstrings des Textes des ersten und letzten Segments eines Spans. Alternativen akzeptieren sie ebenfalls. Jeder Span braucht ein reason, wenn es mehrere gibt. (Ein Ein-Span-Entwurf kann start_id/end_id/... auch auf der obersten Ebene setzen.)

  4. 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, schreibt clip.json und validiert es. Es listet jeden Span mit seinem Zeitbereich und seiner Länge auf. Lies alle warn-Zeilen, die es ausgibt. Benachbarte Spans (sich berührende Segment-IDs) gelten als überlappend: Fasse sie zu einem Span zusammen.

  5. Nur bei Videoaufnahmen: Führe preview_layout.py <ep> aus und betrachte layout.preview.png (und layout.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" in episode.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.json wieder.

  6. 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 auch layout.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.json und führe build_clip.py erneut 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.mp4 muss mit der Gesamtlänge übereinstimmen, die build_clip.py ausgegeben 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 JP nicht gefunden wurde, informiere den Benutzer (README → Setup).

Melde den Pfad zu digest.mp4, den Titel und den Quellzeitbereich jedes Spans.

Fehlerbehebung

SymptomLösung
OPENAI_API_KEY is not setDer Benutzer fügt ihn zu $SKILL_DIR/.env hinzu (Vorlage .env.example) oder exportiert ihn. Behandle den Schlüssel niemals selbst
before ... not foundbefore muss ein exakter Teilstring des aktuellen Textes dieses Segments sein (frühere Zeilen können ihn geändert haben)
segments changed but not listedJede geänderte Segment-ID muss in einer Zeile von corrections.md erscheinen
spans total ... > 180s / exceeds 180sEntferne 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 wurdetranscribe.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 EpisodeBenenne 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 overlapZwei 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 RandKorrigiere tiles in episode.json, überprüfe mit preview_layout.py, dann render.py <ep>
layout 'stacked' but input.mp4 has no video streamEs ist ein Audio-Podcast: Entferne episode.json
Untertitel in einer Ersatz- oder dünnen Schriftartassets/fonts/NotoSansJP-Bold.ttf muss existieren (eine statische Bold-Datei, nicht die variable Schriftart); siehe README → Setup
Geänderte background.png / Schriftarteinstellungenrender.py <ep> bemerkt dies und rendert neu; --force zur Sicherheit

Verwandte Skills