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 audio | Enregistrement vidéo (StreamYard 2-up) | |
|---|---|---|
episode.json | aucun (ou {"layout": "background"}) | {"layout": "stacked"} |
| Image | assets/background.png | tuile du locuteur gauche en haut, tuile du locuteur droit en bas, sous-titres dans la bande entre les deux |
| Son | audio.mp3 | extrait 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
- Exécutez
check_env.py(sans argument d'épisode). Il vérifie ffmpeg/libass, l'arrière-plan, la police et queOPENAI_API_KEYest 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
catou negrepjamais 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.
- Ne lisez, n'imprimez, ne
- Créez
episodes/<slug>/(slug ASCII court, ex.ep042). Copiez l'enregistrement sousinput.mp4et la note de spectacle sousshownote.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 dansshownote.txt. Ne déplacez ni ne copiez un enregistrement dont vous n'avez pas reçu le chemin. - S'il s'agit d'un enregistrement vidéo de deux personnes côte à côte, écrivez
episode.json:{"layout": "stacked"}. - 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.
-
Lisez
shownote.txten entier. Listez les noms (animateurs, invités), les produits, les entreprises et les termes techniques qu'il contient. -
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 (制度→精度). -
É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).avantdoit 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
raisonlorsque 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 pastranscript.jsonà la main. - Si rien ne nécessite de correction, écrivez uniquement l'en-tête du tableau.
-
Exécutez
apply_corrections.py <ep>. Il écrittranscript.jsonet exécutevalidate_transcript.py. Si unavantn'est pas trouvé, corrigez cette ligne et ré-exécutez. -
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 modifiantcorrections.mdet en ré-exécutantapply_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.
-
Recherchez :
show_transcript.py <ep> --grep <mot_clé> ... -C 5avec 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. -
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 ; utilisezend_afterpour 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.
-
É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_atetend_aftersont 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'unereasonlorsqu'il y en a plusieurs. (Un brouillon à intervalle unique peut aussi placerstart_id/end_id/... au niveau supérieur.) -
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, écritclip.jsonet le valide. Il liste chaque intervalle avec sa plage temporelle et sa durée. Lisez toutes les ligneswarnqu'il imprime. Les intervalles adjacents (identifiants de segments contigus) sont considérés comme se chevauchant : fusionnez-les en un seul intervalle. -
Enregistrements vidéo uniquement : exécutez
preview_layout.py <ep>et regardezlayout.preview.png(etlayout.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"dansepisode.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.jsonfonctionnel. -
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 aussilayout.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.jsonet ré-exécutezbuild_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.mp4doit correspondre au total quebuild_clip.pya 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 JPn'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ôme | Correction |
|---|---|
OPENAI_API_KEY is not set | L'utilisateur l'ajoute dans $SKILL_DIR/.env (modèle .env.example) ou l'exporte. Ne manipulez jamais la clé vous-même |
before ... not found | before 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 listed | Chaque identifiant de segment modifié doit apparaître dans une ligne de corrections.md |
spans total ... > 180s / exceeds 180s | Supprimez 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 trous | transcribe.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 épisode | Renommez 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 overlap | Deux 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 bordure | Corrigez tiles dans episode.json, vérifiez avec preview_layout.py, puis render.py <ep> |
layout 'stacked' but input.mp4 has no video stream | Il s'agit d'un podcast audio : supprimez episode.json |
| Sous-titres dans une police de secours ou fine | assets/fonts/NotoSansJP-Bold.ttf doit exister (un fichier Bold statique, pas la police variable) ; voir README → Configuration |
Modifications de background.png / paramètres de police | render.py <ep> les détecte et ré- effectue le rendu ; --force pour être sûr |