Communitygithub.com

podcast-digest

Transformez un épisode de podcast japonais – audio (input.mp4 + shownote.txt) ou enregistrement vidéo de type StreamYard – en un court-métrage vertical 9:16 d’au plus 180 secondes avec sous-titres japonais incrustés.

Qu'est-ce que podcast-digest ?

Ce skill permet de créer automatiquement des clips vidéo verticaux (9:16) de type « digest », « highlight », « short », « reel » ou « 切り抜き動画 » à partir d’un épisode de podcast ou d’un enregistrement vidéo en japonais. Il est conçu pour être utilisé avec Claude Code, Cursor ou Codex (agents capables d’exécuter des scripts Python), mais les scripts eux-mêmes sont indépendants du shell et fonctionnent avec `uv` sous macOS/Linux. Le pipeline est déterministe pour les parties techniques (transcription, calcul des timings, rendu). L’humain intervient à deux étapes clés : la correction du transcript (étape 1b) et le choix des extraits (étape 2). Une fois ces choix validés, le rendu final produit une vidéo MP4 1080×1920 à 30 ips avec sous-titres ASS incrustés, audio normalisé à -14 LUFS et fondu audio entre les coupures. Le skill prend en charge les podcasts audio avec une image de fond fixe et les enregistrements vidéo à deux intervenants (disposition empilée). Il gère les longs épisodes (~50 minutes, ~1,5 Go) par téléchargement de l’audio seul et extraction directe des passages dans la vidéo source.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Podcast / enregistrement → vidéo d'extraits

Travail déterministe (audio, appels API, calculs de timing, rendu) effectué par les scripts. Vous effectuez les deux étapes de jugement : corriger le transcript (1b) et choisir les extraits (2). Il y a deux points de contrôle humain. Ne les sautez jamais.

SKILL_DIR ci-dessous désigne le répertoire qui contient ce fichier. Exécutez les scripts avec uv, qui utilise l'environnement verrouillé de ce skill depuis n'importe quel répertoire de travail : uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<nom>.py" <repertoire_episode>. Dans les étapes ci-dessous, <nom>.py <ep> est un raccourci pour cette commande. Chaque script affiche ce qu'il a créé ou ignoré. Une ré-exécution est toujours sans danger : les étapes terminées sont ignorées et les fichiers dérivés sont reconstruits lorsque leurs entrées changent. --force refait une étape.

Structure d'un épisode (un répertoire par épisode)

episodes/<slug>/
  input.mp4                        fourni par l'utilisateur (podcast audio ou enregistrement vidéo)
  shownote.txt                     fourni par l'utilisateur (recommandé : noms, produits, sujets)
  episode.json                     vous l'écrivez pour les enregistrements vidéo : {"layout": "stacked"}
  audio.mp3, transcript.raw.json   étape 1a  (transcribe.py)
  corrections.md                   étape 1b  (vous l'écrivez)
  transcript.json                  étape 1b  (apply_corrections.py)
  clip.draft.json                  étape 2   (vous l'écrivez)
  clip.json                        étape 2   (build_clip.py)
  layout.preview.png               étape 2   (preview_layout.py, enregistrements vidéo)
  digest.ass, digest.mp4           étape 3   (render.py)

Deux types d'épisode, un seul pipeline

Podcast audioEnregistrement vidéo (StreamYard 2-up)
episode.jsonaucun (ou {"layout": "background"}){"layout": "stacked"}
Imageassets/background.pngtuile du locuteur gauche en haut, tuile du locuteur droit en bas, sous-titres dans la bande entre les deux
Sonaudio.mp3extrait de input.mp4, synchronisé avec l'image

Tout le reste (transcription, corrections, sélection des extraits, sous-titres, niveau sonore) est identique. Un enregistrement de 50 minutes, ~1,5 Go, est pris en charge : seul son audio est téléchargé, et le rendu cherche directement chaque extrait.

Étape 0 : Configuration

  1. Exécutez check_env.py (sans argument d'épisode). Il vérifie ffmpeg/libass, l'arrière-plan, la police et que OPENAI_API_KEY est défini dans le shell ou dans $SKILL_DIR/.env. S'il se termine avec le code 1, arrêtez-vous et orientez l'utilisateur vers le README → Configuration.
    • Ne lisez, n'imprimez, ne cat ou ne grep jamais le fichier .env, et ne demandez jamais à l'utilisateur de coller la clé dans le chat. L'utilisateur place la clé lui-même dans .env : cp .env.example .env, puis modifie le fichier.
  2. Créez episodes/<slug>/ (slug ASCII court, ex. ep042). Copiez l'enregistrement sous input.mp4 et la note de spectacle sous shownote.txt. S'il n'y a pas de note de spectacle, demandez à l'utilisateur les noms des intervenants et les termes clés, puis écrivez-les vous-même dans shownote.txt. Ne déplacez ni ne copiez un enregistrement dont vous n'avez pas reçu le chemin.
  3. S'il s'agit d'un enregistrement vidéo de deux personnes côte à côte, écrivez episode.json : {"layout": "stacked"}.
  4. Exécutez run_all.py episodes/<slug>. Il exécute l'étape 1a et s'arrête à l'étape 1b avec le code de sortie 10. Cette pause est attendue. Le code de sortie 1 signifie une erreur : lisez-la.

Étape 1a : Transcrire (script)

run_all.py a déjà exécuté transcribe.py. Il extrait l'audio mono 16 kHz et appelle whisper-1 avec les horodatages des mots, language="ja", et une invite de vocabulaire construite à partir du shownote.txt. Les longs fichiers sont automatiquement divisés en morceaux qui se chevauchent. whisper-1 saute parfois une portion de parole : les longues pauses entre les mots reconnus qui contiennent encore du son sont renvoyées seules et les segments récupérés sont insérés (lignes gap ... recovered). Pour voir l'invite, exécutez transcribe.py <ep> --print-prompt. Pour la remplacer, placez le texte dans <ep>/prompt.txt et ré-exécutez avec --force. La ré-exécution coûte de l'argent via l'API, demandez donc d'abord à l'utilisateur.

Étape 1b : Corriger le transcript (vous)

Objectif : corriger les mots mal reconnus dans le text de chaque segment, en utilisant shownote.txt comme source de vérité pour les noms, les produits et les termes.

  1. Lisez shownote.txt en entier. Listez les noms (animateurs, invités), les produits, les entreprises et les termes techniques qu'il contient.

  2. Lisez le transcript : show_transcript.py <ep> --raw. Pour les épisodes longs, lisez-le par plages (--from-id/--to-id) et utilisez grep pour les éventuelles mauvaises interprétations de chaque terme (--grep クロード --grep claude). Whisper écrit généralement les noms anglais en katakana, les écorche (Wisper), saute des lettres (NTHROPIC) ou choisit le mauvais homophone kanji (制度→精度).

  3. Écrivez corrections.md : un tableau markdown. Chaque ligne remplace une sous-chaîne exacte dans le(s) segment(s) listé(s) :

    | id | avant | après | raison |
    |----|-------|-------|--------|
    | 6 | クロットコード | Claude Code | nom de produit dans le shownote |
    | 7, 8, 12 | エーデント | エージェント | mauvaise reconnaissance de エージェント |
    
    • id : identifiants de segments, séparés par des virgules, plages autorisées (10-12). avant doit apparaître dans le texte actuel de ce segment. Les lignes s'appliquent dans l'ordre.
    • Corrigez uniquement les erreurs de reconnaissance. Ne reformulez pas, ne résumez pas, ne supprimez pas les mots de remplissage et n'"améliorez" pas le japonais. Corrigez un homophone évident d'après le contexte, et mentionnez "(contexte)" dans la colonne raison lorsque le shownote ne le tranche pas.
    • Si un segment est une hallucination de whisper (par exemple 「ご視聴ありがとうございました」 sur du silence), mettez-le à vide : | 41 | ご視聴ありがとうございました | | hallucination |.
    • Ne fusionnez, ne divisez, ne réordonnez et ne re-timez jamais les segments. Les scripts rendent cela impossible tant que vous passez par corrections.md. Ne modifiez pas transcript.json à la main.
    • Si rien ne nécessite de correction, écrivez uniquement l'en-tête du tableau.
  4. Exécutez apply_corrections.py <ep>. Il écrit transcript.json et exécute validate_transcript.py. Si un avant n'est pas trouvé, corrigez cette ligne et ré-exécutez.

  5. POINT DE CONTRÔLE 1. Montrez à l'humain le tableau de corrections (ou un résumé, plus les lignes marquées "(contexte)") et demandez-lui de relire corrections.md. Appliquez ses modifications en modifiant corrections.md et en ré-exécutant apply_corrections.py. Continuez seulement après son approbation.

Étape 2 : Choisir les extraits (vous)

Cette étape nécessite l'instruction de l'humain : soit un sujet (「料金の話をしているところ」) soit une demande pour les meilleurs passages (「大事なポイントをまとめて」). S'il n'en a pas donné, demandez-lui. Ne devinez pas de sujet.

Un court-métrage est constitué d'un ou plusieurs intervalles (parties contiguës de l'épisode) joués dans l'ordre chronologique et reliés par des coupures franches. Total : au maximum 180 s.

  1. Recherchez : show_transcript.py <ep> --grep <mot_clé> ... -C 5 avec les mots-clés de l'instruction ainsi que des synonymes et des orthographes probables. Lisez ensuite généreusement autour de chaque occurrence (--from-id/--to-id) pour voir où chaque discussion commence et se termine réellement. Pour "les points importants", lisez d'abord tout le transcript par plages et listez les moments candidats avec leurs identifiants avant de choisir.

  2. Choisissez les intervalles :

    • Un seul intervalle lorsque l'instruction nomme un seul sujet et que sa discussion tient dans 180 s.
    • Plusieurs intervalles (généralement 2 à 4) lorsque l'instruction demande des extraits, ou lorsqu'un seul sujet est dispersé et que les discussions intermédiaires sont hors sujet. Chaque intervalle doit avoir un sens par lui-même et immédiatement après le précédent : une coupure franche ne donne aucun contexte.
    • Pour chaque intervalle : début là où le point est introduit, pas au milieu d'une pensée ou sur une référence en arrière (「さっきの」「前回」「それで」). Éliminez un mot de remplissage ou de transition (「では本題です。」「で、」) avec start_at. Fin sur une phrase complète ; utilisez end_after pour vous arrêter à l'intérieur d'un segment.
    • Préférez moins d'intervalles, complets, à de nombreux fragments ; un intervalle de moins de ~8 s tient rarement debout tout seul.
    • Si le matériel est plus long que 180 s, gardez les parties les plus fortes et mentionnez-le dans reason.
    • Ajoutez jusqu'à 2 alternatives (autres intervalles forts) avec leurs raisons.
    • Si rien ne correspond, arrêtez-vous et dites à l'humain ce que vous avez cherché et ce que l'épisode couvre réellement. N'écrivez pas de brouillon.
  3. Écrivez clip.draft.json. Vous choisissez les identifiants de segments. Le script fait le timing :

    {
      "instruction": "<l'instruction de l'humain, textuellement>",
      "title": "スキル機能と文字起こしの要点",
      "reason": "Pourquoi ces parties, dans leur ensemble.",
      "spans": [
        { "start_id": 6, "end_id": 8, "start_at": "今日は、Claude Codeのスキル機能について",
          "title": "スキル機能とは", "reason": "Introduit le sujet." },
        { "start_id": 12, "end_id": 12, "title": "分担が大事", "reason": "Le point central." }
      ],
      "alternatives": [
        { "start_id": 120, "end_id": 131, "title": "...", "reason": "..." }
      ]
    }
    

    start_at et end_after sont des sous-chaînes exactes facultatives du texte du premier et du dernier segment d'un intervalle. Les alternatives les acceptent aussi. Chaque intervalle a besoin d'une reason lorsqu'il y en a plusieurs. (Un brouillon à intervalle unique peut aussi placer start_id/end_id/... au niveau supérieur.)

  4. Exécutez build_clip.py <ep>. Il ajuste chaque limite aux horodatages des mots, ajoute environ 0,3 s de marge (sans jamais dépasser un mot voisin), trie les intervalles, vérifie qu'ils ne se chevauchent pas et que le total ≤ 180 s, écrit clip.json et le valide. Il liste chaque intervalle avec sa plage temporelle et sa durée. Lisez toutes les lignes warn qu'il imprime. Les intervalles adjacents (identifiants de segments contigus) sont considérés comme se chevauchant : fusionnez-les en un seul intervalle.

  5. Enregistrements vidéo uniquement : exécutez preview_layout.py <ep> et regardez layout.preview.png (et layout.source.png, où les tuiles sont entourées de rouge = haut, bleu = bas). Chaque panneau doit montrer une personne, centrée, sans bord de cadre ni tuile voisine. Dans le cas contraire, définissez "tiles" dans episode.json (fractions de l'image source) et ré-exécutez :

    {"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}]}
    

    La première tuile va en haut. Échangez les deux entrées pour permuter les locuteurs. Les enregistrements du même studio partagent les mêmes tuiles, réutilisez donc un episode.json fonctionnel.

  6. POINT DE CONTRÔLE 2. Montrez à l'humain le plan : le titre global et la durée totale, puis chaque intervalle dans l'ordre avec titre, mm:ss–mm:ss, durée, raison, et ses première et dernière phrases (show_transcript.py <ep> --from-id A --to-id B), puis les alternatives. Pour la vidéo, montrez aussi layout.preview.png. Demandez-lui d'approuver, d'échanger, d'ajouter ou de supprimer des intervalles.

    • build_clip.py <ep> --promote N : avec un seul intervalle, remplace par l'alternative N ; avec plusieurs, ajoute l'alternative N comme intervalle supplémentaire (surveillez le total de 180 s).
    • Tout autre changement : modifiez clip.draft.json et ré-exécutez build_clip.py.
    • Continuez seulement après son approbation.

Étape 3 : Rendu (script)

Exécutez run_all.py <ep> --render (ou render.py <ep>). Sortie : digest.mp4, 1080×1920, 30 ips, H.264 + AAC, normalisé à -14 LUFS, avec sous-titres incrustés depuis digest.ass. Les intervalles sont reliés par des coupures franches. Chaque coupure reçoit un fondu audio de 50 ms pour éviter un clic.

Vérifiez ensuite le résultat :

  • ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4 doit correspondre au total que build_clip.py a affiché.
  • Extrayez une image de chaque intervalle et regardez-la, pour savoir que les sous-titres s'affichent, ne sont pas des boîtes tofu, et (vidéo) que les panneaux sont bien cadrés : ffmpeg -y -v error -ss 5 -i <ep>/digest.mp4 -frames:v 1 /tmp/frame.png.
  • Si le rendu a averti que la police Noto Sans JP n'a pas été trouvée, informez l'utilisateur (README → Configuration).

Signalez le chemin de digest.mp4, le titre et la plage temporelle source de chaque intervalle.

Dépannage

SymptômeCorrection
OPENAI_API_KEY is not setL'utilisateur l'ajoute dans $SKILL_DIR/.env (modèle .env.example) ou l'exporte. Ne manipulez jamais la clé vous-même
before ... not foundbefore doit être une sous-chaîne exacte du texte actuel de ce segment (les lignes précédentes ont pu le modifier)
segments changed but not listedChaque identifiant de segment modifié doit apparaître dans une ligne de corrections.md
spans total ... > 180s / exceeds 180sSupprimez un intervalle, choisissez-en des plus courts, ou réduisez avec start_at / end_after
Discours manquant dans le transcript (un long saut dans les temps où les gens parlent) dans un transcript créé avant l'existence de la récupération des troustranscribe.py <ep> --fill-gaps renvoie uniquement les trous. Il affiche comment les identifiants se décalent : renumérotez corrections.md / clip.draft.json en conséquence, puis apply_corrections.py <ep>. Mettez à vide tout 「ご視聴ありがとうございました」 récupéré comme une hallucination
Un deuxième court-métrage à partir du même épisodeRenommez le digest.mp4, digest.ass, clip.json, clip.draft.json terminés (ex. digest-points_cles.mp4), puis écrivez un nouveau clip.draft.json
spans overlapDeux intervalles partagent ou touchent des segments : fusionnez-les en un seul intervalle
opens with 'さっき' (warn)Commencez plus tard ou réduisez avec start_at pour que l'intervalle tienne debout tout seul
Les panneaux montrent une zone incorrecte / une bordureCorrigez tiles dans episode.json, vérifiez avec preview_layout.py, puis render.py <ep>
layout 'stacked' but input.mp4 has no video streamIl s'agit d'un podcast audio : supprimez episode.json
Sous-titres dans une police de secours ou fineassets/fonts/NotoSansJP-Bold.ttf doit exister (un fichier Bold statique, pas la police variable) ; voir README → Configuration
Modifications de background.png / paramètres de policerender.py <ep> les détecte et ré- effectue le rendu ; --force pour être sûr

Skills associés