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 áudio | Gravação de vídeo (StreamYard 2 pessoas) | |
|---|---|---|
episode.json | nenhum (ou {"layout": "background"}) | {"layout": "stacked"} |
| Imagem | assets/background.png | bloco do falante esquerdo em cima, bloco direito embaixo, legendas na faixa entre eles |
| Som | audio.mp3 | extraí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
- Execute
check_env.py(sem argumento de episódio). Ele verifica ffmpeg/libass, o fundo, a fonte e seOPENAI_API_KEYestá 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,
catougrepo arquivo.env, e nunca peça ao usuário para colar a chave no chat. O usuário coloca a chave no.envpor conta própria:cp .env.example .env, depois edite o arquivo.
- Nunca leia, imprima,
- Crie
episodes/<slug>/(slug ASCII curto, ex.:ep042). Copie a gravação para lá comoinput.mp4e a shownote comoshownote.txt. Se não houver shownote, peça ao usuário os participantes e nomes/termos-chave e escreva-os você mesmo noshownote.txt. Não mova ou copie uma gravação para a qual você não recebeu um caminho. - Se for uma gravação de vídeo de duas pessoas lado a lado, escreva
episode.json:{"layout": "stacked"}. - 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.
-
Leia
shownote.txtpor completo. Liste os nomes (apresentadores, convidados), produtos, empresas e termos técnicos que contém. -
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 (制度→精度). -
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).beforedeve 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
motivoquando 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 editetranscript.jsonmanualmente. - Se nada precisar ser corrigido, escreva apenas a linha do cabeçalho.
-
Execute
apply_corrections.py <ep>. Ele escrevetranscript.jsone executavalidate_transcript.py. Se umbeforenão for encontrado, corrija essa linha e execute novamente. -
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 editandocorrections.mde reexecutandoapply_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.
-
Pesquise:
show_transcript.py <ep> --grep <palavra-chave> ... -C 5com 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. -
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; useend_afterpara 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.
-
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_ateend_aftersão substrings exatas opcionais do texto do primeiro e último segmento de um trecho. Alternativas também os aceitam. Cada trecho precisa de umreasonquando há vários. (Um rascunho de um único trecho também pode colocarstart_id/end_id/... no nível superior.) -
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, escreveclip.jsone o valida. Ele lista cada trecho com seu intervalo de tempo e duração. Leia quaisquer linhaswarnque ele imprimir. Trechos adjacentes (ids de segmento tocantes) são considerados sobrepostos: mescle-os em um trecho. -
Apenas para gravações de vídeo: execute
preview_layout.py <ep>e vejalayout.preview.png(elayout.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"emepisode.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.jsonque funcione. -
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, mostrelayout.preview.pngtambé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.jsone reexecutebuild_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.mp4deve corresponder ao total quebuild_clip.pyimprimiu.- 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 JPnã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
| Sintoma | Correção |
|---|---|
OPENAI_API_KEY is not set | O usuário adiciona em $SKILL_DIR/.env (modelo .env.example) ou exporta. Nunca manuseie a chave você mesmo |
before ... not found | before deve ser uma substring exata do texto atual daquele segmento (linhas anteriores podem tê-lo alterado) |
segments changed but not listed | Cada id de segmento alterado deve aparecer em uma linha de corrections.md |
spans total ... > 180s / exceeds 180s | Remova 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 existir | transcribe.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ódio | Renomeie o digest.mp4, digest.ass, clip.json, clip.draft.json finalizados (ex.: digest-keypoints.mp4), depois escreva um novo clip.draft.json |
spans overlap | Dois 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 borda | Corrija 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 fina | assets/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 fonte | render.py <ep> percebe e re-renderiza; --force para garantir |