Communitygithub.com

podcast-digest

Transforme um episódio de podcast japonês — seja um áudio (input.mp4 + shownote.txt) ou uma gravação de vídeo de duas pessoas (como StreamYard) — em um vídeo vertical 9:16 de até 180 segundos com legendas em japonês queimadas. Destaque partes específicas solicitadas por humanos; vários trechos são unidos com cortes secos em um único clipe.

O que é podcast-digest?

Este skill é uma ferramenta determinística e controlada por scripts para criar clipes de destaque a partir de episódios de podcast ou gravações de vídeo. Ele suporta tanto podcasts de áudio quanto gravações de vídeo com layout empilhado (como StreamYard). O fluxo de trabalho é dividido em etapas: transcrição automática com Whisper-1, correção manual do transcript (com checkpoint humano), seleção de trechos a partir de instruções do usuário (novamente com checkpoint humano) e renderização final. As etapas manuais são essenciais: o usuário deve corrigir erros de reconhecimento de fala no japonês e escolher os trechos de destaque com base no tópico solicitado. O skill garante que as correções sejam aplicadas de forma consistente, valida a duração total (máximo 180 segundos) e produz um vídeo 1080x1920 com legendas, normalização de volume e cortes secos com fade de áudio. Tudo é gerenciado por scripts em Python com suporte a reexecução segura e checkpointing.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Podcast / gravação → vídeo de destaque

Trabalho determinístico (áudio, chamadas de API, cálculos de tempo, renderização) é feito por scripts. Você faz as duas etapas de julgamento: corrigir o transcript (1b) e escolher os destaques (2). Existem dois checkpoints humanos. Nunca os pule.

SKILL_DIR abaixo significa o diretório que contém este arquivo. Execute scripts com uv, que usa o ambiente bloqueado deste skill a partir de qualquer diretório de trabalho: uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<name>.py" <episode_dir>. Nas etapas abaixo, <name>.py <ep> é abreviação para esse comando. Cada script imprime o que criou ou pulou. Reexecutar é sempre seguro: etapas concluídas são puladas e arquivos derivados são reconstruídos quando suas entradas mudam. --force refaz uma etapa.

Layout do episódio (um diretório por episódio)

episodes/<slug>/
  input.mp4                        fornecido pelo usuário (podcast de áudio ou gravação de vídeo)
  shownote.txt                     fornecido pelo usuário (recomendado: nomes, produtos, tópicos)
  episode.json                     você escreve para gravações de vídeo: {"layout": "stacked"}
  audio.mp3, transcript.raw.json   passo 1a  (transcribe.py)
  corrections.md                   passo 1b  (você escreve)
  transcript.json                  passo 1b  (apply_corrections.py)
  clip.draft.json                  passo 2   (você escreve)
  clip.json                        passo 2   (build_clip.py)
  layout.preview.png               passo 2   (preview_layout.py, gravações de vídeo)
  digest.ass, digest.mp4           passo 3   (render.py)

Dois tipos de episódio, um pipeline

Podcast de áudioGravação de vídeo (StreamYard 2 pessoas)
episode.jsonnenhum (ou {"layout": "background"}){"layout": "stacked"}
Imagemassets/background.pngbloco do falante esquerdo em cima, bloco direito embaixo, legendas na faixa entre eles
Somaudio.mp3extraído de input.mp4, sincronizado com a imagem

Todo o resto (transcrição, correções, seleção de destaques, legendas, volume) é o mesmo. Uma gravação de 50 minutos, ~1,5 GB, funciona: apenas seu áudio é enviado, e a renderização busca diretamente cada destaque.

Passo 0: Configuração

  1. Execute check_env.py (sem argumento de episódio). Ele verifica ffmpeg/libass, o fundo, a fonte e se OPENAI_API_KEY está definida no shell ou em $SKILL_DIR/.env. Se sair com código 1, pare e aponte o usuário para README → Configuração.
    • Nunca leia, imprima, cat ou grep o arquivo .env, e nunca peça ao usuário para colar a chave no chat. O usuário coloca a chave no .env por conta própria: cp .env.example .env, depois edite o arquivo.
  2. Crie episodes/<slug>/ (slug ASCII curto, ex.: ep042). Copie a gravação para lá como input.mp4 e a shownote como shownote.txt. Se não houver shownote, peça ao usuário os participantes e nomes/termos-chave e escreva-os você mesmo no shownote.txt. Não mova ou copie uma gravação para a qual você não recebeu um caminho.
  3. Se for uma gravação de vídeo de duas pessoas lado a lado, escreva episode.json: {"layout": "stacked"}.
  4. Execute run_all.py episodes/<slug>. Ele executa o passo 1a e para no passo 1b com código de saída 10. Essa pausa é esperada. Código de saída 1 significa erro: leia-o.

Passo 1a: Transcrever (script)

run_all.py já executou transcribe.py. Ele extrai áudio mono de 16 kHz e chama whisper-1 com timestamps de palavra, language="ja" e um prompt de vocabulário construído a partir da shownote. Arquivos longos são divididos em partes com sobreposição automaticamente. whisper-1 às vezes pula um trecho de fala: longas pausas entre palavras reconhecidas que ainda contêm som são reenviadas separadamente e os segmentos recuperados são inseridos (linhas gap ... recovered). Para ver o prompt, execute transcribe.py <ep> --print-prompt. Para substituí-lo, coloque o texto em <ep>/prompt.txt e reexecute com --force. A reexecução custa dinheiro de API, então pergunte ao usuário primeiro.

Passo 1b: Corrigir o transcript (você)

Objetivo: corrigir palavras mal reconhecidas no texto de cada segmento text, usando shownote.txt como fonte da verdade para nomes, produtos e termos.

  1. Leia shownote.txt por completo. Liste os nomes (apresentadores, convidados), produtos, empresas e termos técnicos que contém.

  2. Leia o transcript: show_transcript.py <ep> --raw. Para episódios longos, leia-o em intervalos (--from-id/--to-id) e pesquise por prováveis erros de reconhecimento de cada termo (--grep クロード --grep claude). Whisper geralmente escreve nomes em inglês em katakana, soletra erroneamente (Wisper), omite letras (NTHROPIC), ou escolhe o homófono kanji errado (制度→精度).

  3. Escreva corrections.md: uma tabela markdown. Cada linha substitui uma substring exata no(s) segmento(s) listado(s):

    | id | before | after | motivo |
    |----|--------|-------|--------|
    | 6 | クロットコード | Claude Code | nome do produto na shownote |
    | 7, 8, 12 | エーデント | エージェント | erro de reconhecimento de エージェント |
    
    • id: ids de segmento, separados por vírgula, intervalos permitidos (10-12). before deve ocorrer no texto atual daquele segmento. As linhas se aplicam em ordem.
    • Corrija apenas erros de reconhecimento. Não reformule, resuma, remova preenchimentos ou "melhore" o japonês. Corrija um homófono óbvio pelo contexto e diga "(contexto)" na coluna motivo quando a shownote não o resolver.
    • Se um segmento for uma alucinação do whisper (por exemplo 「ご視聴ありがとうございました」 sobre silêncio), defina-o como vazio: | 41 | ご視聴ありがとうございました | | alucinação |.
    • Nunca mescle, divida, reordene ou reajuste o tempo dos segmentos. Os scripts tornam isso impossível desde que você passe por corrections.md. Não edite transcript.json manualmente.
    • Se nada precisar ser corrigido, escreva apenas a linha do cabeçalho.
  4. Execute apply_corrections.py <ep>. Ele escreve transcript.json e executa validate_transcript.py. Se um before não for encontrado, corrija essa linha e execute novamente.

  5. CHECKPOINT 1. Mostre ao humano a tabela de correções (ou um resumo dela, mais as linhas marcadas como "(contexto)") e peça para revisar corrections.md. Aplique as alterações editando corrections.md e reexecutando apply_corrections.py. Continue apenas depois que ele aprovar.

Passo 2: Escolher os destaques (você)

Este passo requer a instrução do humano: seja um tópico (「料金の話をしているところ」) ou um pedido pelas melhores partes (「大事なポイントをまとめて」). Se ele não deu uma, peça. Não adivinhe um tópico.

Um clipe curto consiste em um ou mais trechos (partes contíguas do episódio) reproduzidos em ordem cronológica e unidos com cortes secos. Total: no máximo 180 s.

  1. Pesquise: show_transcript.py <ep> --grep <palavra-chave> ... -C 5 com as palavras-chave da instrução mais sinônimos e grafias prováveis. Depois leia generosamente em torno de cada resultado (--from-id/--to-id) para ver onde cada discussão realmente começa e termina. Para "os pontos importantes", leia todo o transcript em intervalos primeiro e liste os momentos candidatos com seus ids antes de escolher.

  2. Escolha os trechos:

    • Um trecho quando a instrução nomeia um tópico e sua discussão cabe em 180 s.
    • Vários trechos (normalmente 2–4) quando a instrução pede destaques, ou quando um tópico é distribuído e a conversa intermediária é off-topic. Cada trecho deve fazer sentido por si só e logo após o anterior: um corte seco não dá contexto.
    • Para cada trecho: inicie onde o ponto é introduzido, não no meio de um pensamento ou com uma referência anterior (「さっきの」「前回」「それで」). Apare um preenchimento ou transição inicial (「では本題です。」「で、」) com start_at. Termine em uma frase completa; use end_after para parar dentro de um segmento.
    • Prefira menos trechos completos a muitos fragmentos; um trecho menor que ~8 s raramente funciona sozinho.
    • Se o material for maior que 180 s, mantenha as partes mais fortes e diga isso em reason.
    • Adicione até 2 alternatives (outros trechos fortes) com motivos.
    • Se nada corresponder, pare e diga ao humano o que você pesquisou e o que o episódio realmente cobre. Não escreva um rascunho.
  3. Escreva clip.draft.json. Você escolhe os ids dos segmentos. O script faz o timing:

    {
      "instruction": "<a instrução do humano, literal>",
      "title": "スキル機能と文字起こしの要点",
      "reason": "Por que essas partes, como um todo.",
      "spans": [
        { "start_id": 6, "end_id": 8, "start_at": "今日は、Claude Codeのスキル機能について",
          "title": "スキル機能とは", "reason": "Introduz o tópico." },
        { "start_id": 12, "end_id": 12, "title": "分担が大事", "reason": "O ponto central." }
      ],
      "alternatives": [
        { "start_id": 120, "end_id": 131, "title": "...", "reason": "..." }
      ]
    }
    

    start_at e end_after são substrings exatas opcionais do texto do primeiro e último segmento de um trecho. Alternativas também os aceitam. Cada trecho precisa de um reason quando há vários. (Um rascunho de um único trecho também pode colocar start_id/end_id/... no nível superior.)

  4. Execute build_clip.py <ep>. Ele ajusta cada limite para timestamps de palavra, adiciona cerca de 0,3 s de margem (nunca passando de uma palavra vizinha), ordena os trechos, verifica se não se sobrepõem e totalizam ≤ 180 s, escreve clip.json e o valida. Ele lista cada trecho com seu intervalo de tempo e duração. Leia quaisquer linhas warn que ele imprimir. Trechos adjacentes (ids de segmento tocantes) são considerados sobrepostos: mescle-os em um trecho.

  5. Apenas para gravações de vídeo: execute preview_layout.py <ep> e veja layout.preview.png (e layout.source.png, onde os blocos são delineados em vermelho = topo, azul = base). Cada painel deve mostrar uma pessoa, centralizada, sem borda do quadro ou bloco vizinho. Se não, defina "tiles" em episode.json (frações do quadro de origem) e reexecute:

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

    O primeiro bloco vai no topo. Troque as duas entradas para trocar os falantes. Gravações do mesmo estúdio compartilham os mesmos blocos, então reutilize um episode.json que funcione.

  6. CHECKPOINT 2. Mostre ao humano o plano: o título geral e a duração total, depois cada trecho em ordem com título, mm:ss–mm:ss, duração, motivo, e sua primeira e última frase (show_transcript.py <ep> --from-id A --to-id B), depois as alternativas. Para vídeo, mostre layout.preview.png também. Peça que ele aprove, troque, adicione ou remova trechos.

    • build_clip.py <ep> --promote N: com um trecho, troca a alternativa N; com vários, adiciona a alternativa N como outro trecho (cuidado com o total de 180 s).
    • Qualquer outra alteração: edite clip.draft.json e reexecute build_clip.py.
    • Continue apenas depois que ele aprovar.

Passo 3: Renderizar (script)

Execute run_all.py <ep> --render (ou render.py <ep>). Saída: digest.mp4, 1080x1920, 30 qps, H.264 + AAC, normalizado para -14 LUFS, com legendas queimadas a partir de digest.ass. Os trechos são unidos com cortes secos. Cada corte recebe um fade de áudio de 50 ms para não estalar.

Depois verifique o resultado:

  • ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4 deve corresponder ao total que build_clip.py imprimiu.
  • Extraia um quadro de cada trecho e veja-os, para saber se as legendas renderizam, não são caixas de tofu, e (vídeo) os painéis estão enquadrados corretamente: ffmpeg -y -v error -ss 5 -i <ep>/digest.mp4 -frames:v 1 /tmp/frame.png.
  • Se a renderização avisar que a fonte Noto Sans JP não foi encontrada, informe o usuário (README → Configuração).

Reporte o caminho para digest.mp4, o título e o intervalo de tempo de origem de cada trecho.

Solução de problemas

SintomaCorreção
OPENAI_API_KEY is not setO usuário adiciona em $SKILL_DIR/.env (modelo .env.example) ou exporta. Nunca manuseie a chave você mesmo
before ... not foundbefore deve ser uma substring exata do texto atual daquele segmento (linhas anteriores podem tê-lo alterado)
segments changed but not listedCada id de segmento alterado deve aparecer em uma linha de corrections.md
spans total ... > 180s / exceeds 180sRemova um trecho, escolha trechos mais curtos ou apare com start_at / end_after
Fala faltando no transcript (um longo salto nos tempos onde as pessoas falam) em um transcript feito antes da recuperação de lacunas existirtranscribe.py <ep> --fill-gaps reenvia apenas as lacunas. Ele imprime como os ids mudam: renumere corrections.md / clip.draft.json para corresponder, depois apply_corrections.py <ep>. Marque qualquer 「ご視聴ありがとうございました」 recuperado como alucinação
Um segundo clipe do mesmo episódioRenomeie o digest.mp4, digest.ass, clip.json, clip.draft.json finalizados (ex.: digest-keypoints.mp4), depois escreva um novo clip.draft.json
spans overlapDois trechos compartilham ou tocam segmentos: mescle-os em um trecho
opens with 'さっき' (aviso)Comece mais tarde ou apare com start_at para que o trecho fique por conta própria
Painéis mostram a área errada / uma bordaCorrija tiles em episode.json, verifique com preview_layout.py, depois render.py <ep>
layout 'stacked' but input.mp4 has no video streamÉ um podcast de áudio: remova episode.json
Legendas em uma fonte fallback ou finaassets/fonts/NotoSansJP-Bold.ttf deve existir (um arquivo Bold estático, não a fonte variável); veja README → Configuração
Mudanças em background.png / configurações de fonterender.py <ep> percebe e re-renderiza; --force para garantir

Habilidades Relacionadas