Communitygithub.com

srt-whiteboard-workflow

Transforma legendas SRT/roteiros em vídeos animados de lousa branca com mão desenhando, narração, gravação de tela e capturas de tela principais.

O que é srt-whiteboard-workflow?

Esta skill automatiza o fluxo completo de criação de vídeos de lousa branca (whiteboard animation) a partir de roteiros ou legendas SRT, baseado no projeto open-source `srt-whiteboard-animation`. Ela guia o usuário por 7 etapas: storyboard, geração de imagens por IA, anotação pixel-perfect, alinhamento de narração e legendas, renderização com câmera virtual, e queima de legendas. Suporta dois paradigmas de layout (rotas geográficas e linhas do tempo horizontais) e inclui dezenas de regras práticas ("leis de ferro") baseadas em tentativa e erro reais para evitar problemas como sujeira de fundo, tempos de renderização incorretos, sincronia de legenda quebrada e mão fora de posição. O resultado é um vídeo MP4 com hand-drawn animation, narração TTS, legendas sincronizadas e capturas de tela de verificação.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/TaurenRen/srt-whiteboard-workflow/tree/HEAD

Perguntar na sua IA favorita

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

Documentação

Fluxo Completo de Criação de Animação de Lousa Branca (SRT para Animação Final)

Baseado na experiência prática com o projeto open-source srt-whiteboard-animation (github.com/geeklee/srt-whiteboard-animation). Resultado: MP4 de animação de lousa branca "mão desenhando enquanto narra" + capturas de tela principais + gravação da tela.

Leis de Ferro (violação = retrabalho, todas baseadas em erros reais)

  1. Proibido usar imagens de exemplo como entrega. O arquivo scene-01-monkey-mountain-banana.png em examples/ só serve para verificar o pipeline. O usuário pede um tema, deve-se gerar uma imagem daquele tema. Usar imagem de exemplo = falha na entrega.
  2. A imagem fonte DEVE ser limpa. O fundo bege papel gerado por IA tem muitos grãos de textura; o renderizador adaptiveThreshold interpreta grãos como tinta, resultando em linhas fragmentadas e ruído no fundo. Execute scripts/clean_source.py para remover a textura do papel antes de renderizar.
  3. O último elemento NÃO pode usar a região inteira. mask permitida = região − regiões seguintes. Se a região do último elemento for a tela inteira, ela esvazia a allowed de todos os elementos anteriores → as primeiras cenas não desenham nada e perdem todas as cores (confirmado por teste real). A cena final deve escolher uma área pequena real (ex.: uma faixa de nuvem no topo).
  4. O sprite da mão deve ser escalado ao tamanho da tela. O drawing-hand.png original tem 1069×1472. Colocá-lo diretamente em 1280×720 ocuparia 40%+ da tela, obstruindo severamente a visão. Teste real: escalar para 55% (587×809), a largura da mão ~21% da tela, visual normal.
  5. A duração da renderização será MENOR que a anotação. O renderizador determina o número de quadros com base nos pixels realmente desenháveis em cada área; sceneDurationMs é apenas um limite máximo. Teste real: anotação de 8×8000ms=64s renderizou apenas 45.8s. DEVE-SE passar --total-ms e calibrar com a duração real medida pelo ffprobe.
  6. Gravação de tela NÃO pode ser interrompida com kill. Se o ffmpeg for morto à força, perde o moov atom, resultando em erro moov atom not found. Use -t <segundos> para que a gravação termine naturalmente.
  7. Gravação de tela e capturas DEVEM ser entregues. Isto é parte do requisito, não opcional. Capturas devem cobrir: imagem fonte, imagem de verificação da anotação, console de renderização, quadros-chave do vídeo final. Substituir por "documentação escrita" = incompleto.
  8. Uma tela conta APENAS UMA história e DEVE ser um avanço contínuo. Erro real: colocar bandeira vermelha, ponte, montanha nevada, pagoda lado a lado em uma única tela e desenhar tudo de uma vez → o usuário avaliou como "desenho bagunçado". O correto é dispor os elementos ao longo de uma linha principal (rota geográfica / linha do tempo), com a caneta se movendo continuamente na direção da linha principal. O espectador vê "a caneta andando", não "preenchendo detalhes em uma tela".
  9. O layout espacial DEVE obedecer à direção real. Desenhar a Longa Marcha: seguir a geografia real: Ruijin no sudeste → oeste → norte → norte de Shaanxi. Se a composição estiver errada, nem o ritmo mais preciso salva. Antes de desenhar, confirme a posição relativa de cada ponto no mapa.
  10. Ritmo do desenho = ritmo da narração, um elemento por frase. O durationMs de cada elemento DEVE ser igual à duração real da frase correspondente (deduzida da detecção de silêncio com sync_timing.py), nunca distribuir igualmente. A caneta da cena N para no instante em que a "frase N termina".
  11. Cena e narração DEVEM ter correspondência semântica um-para-um. Erro real: a pagoda (Wuqi) foi colocada na cena da frase de conclusão, resultando em "chegada em Wuqi" sendo dita enquanto a cena desenhava outra coisa. A frase de conclusão final ("Vinte e cinco mil lǐ...") não deve ter novos elementos; deve cair no "quadro congelado com tudo desenhado". O renderizador preenche automaticamente quadros vazios até --total-ms para criar o "olhar fixo".
  12. Legendas DEVEM ser queimadas separadamente, não confiando em force_style. SRT puro não tem PlayRes, o libass usa a resolução interna padrão para converter FontSize/MarginV, o que impossibilita o ajuste preciso. Use make_srt.py → srt2ass.py (escrevendo explicitamente PlayRes = tamanho do vídeo e usando {\an2\pos(x,y)} para fixar na borda inferior) para ter controle.
  13. Legendas apenas em uma linha. Duas linhas ocupam duas áreas da tela (superior e inferior) e, em chinês, duas linhas quebram o fluxo de leitura. Se uma linha não couber, divida em múltiplas legendas consecutivas (dividindo o tempo proporcionalmente ao número de caracteres), não use compressão ou redução de fonte. Teste real: fonte 46px + linha única ≤14 caracteres, ~60% da largura da tela em 720p, legível em celular.
  14. A tela DEVE poder se mover; não coloque a imagem inteira em um enquadramento fixo. Feedback do público: "não precisa desenhar tudo em uma única tela" — O correto é renderizar a tela ampliada (2x) e, em cada cena, enquadrar apenas o elemento atual ocupando a tela inteira. Após desenhar, a câmera "pane" para o próximo elemento. Finalmente, zoom out para mostrar a imagem completa. Uma imagem fixa do início ao fim = cada elemento ocupa apenas 10% da tela, impossível de ver.
  15. A ponta da caneta DEVE seguir o conteúdo. O contour-wipe upstream é "apagar tudo de baixo para cima + caneta varrendo horizontalmente aleatoriamente" — os dois estão totalmente desconectados → o usuário vê "a mão se move, mas nada aparece". Mude para pincel serpentina: onde a caneta varre, a cor aparece.
  16. Escreva palavras nos pontos-chave. Apenas desenhar objetos sem escrever nomes faz o espectador não lembrar "qual ano, qual lugar". Incorpore nomes de lugares/anos na imagem de fundo (fonte KaiTi) usando add_handwriting.py, e então faça o renderizador escrevê-los coluna por coluna no modo textwrite — a caneta avança ao longo dos traços, os caracteres aparecem um a um, o mesmo movimento de "escrita à mão".
  17. Após ampliar a tela, duas coisas são OBRIGATÓRIAS, senão fica lento ou borrado: ① Pré-amplie a imagem fonte com INTER_CUBIC para o tamanho da tela (o resize interno do renderizador é INTER_AREA, que é equivalente ao nearest-neighbor ao ampliar, transformando linhas em blocos); ② A aplicação da tinta deve ser processada localmente com bounding box (o upstream cria uma nova máscara de tela inteira para cada segmento, 4x mais lento em tela de 2560).
  18. Deixe 3–7 segundos no final para o zoom out. Não programe a última cena para terminar no último milissegundo da narração, senão não haverá tempo para a ação de "mostrar a imagem completa" como encerramento.
  19. O viewport deve usar o bounding box da tinta real, não a região da anotação. A região é um retângulo desenhado manualmente, geralmente maior que o elemento, podendo abranger 2–3 vizinhos em imagens densas. Usá-la diretamente para o zoom resulta em "espaço vazio no centro do quadro, elemento encolhido no canto, vizinho cortado pela metade". O renderizador já tem embutido: em set_target(element, allowed), calcular o bbox com ink_pixels & allowed e expandir. Exceção: legendas não usam bbox — tags de texto são frequentemente mais largas que a região; usar bbox corta a frase inteira para fora do quadro (teste real: "IA Conversacional" cortou).
  20. Antes de trocar de cena, primeiro mova a câmera, depois desenhe. Deixe ~5 quadros (0.22s) no início de cada cena; caso contrário, a caneta começa a desenhar antes da câmera se posicionar, desenhando fora do quadro, e o espectador vê "mão se move, mas nada aparece".
  21. --zoom-fill deve ser definido separadamente com base na densidade da imagem: 0.86 para imagens esparsas, 0.70 para linhas do tempo densas. Um único valor para todas as situações causa problemas em ambos os extremos.
  22. Elementos verticais longos não preenchem a tela; aceite isso. Em uma tela 16:9, elementos verticais ocupam apenas 60–70% da altura e 25% da largura — isto é uma restrição geométrica. Para preencher mais, desenhe o elemento mais largo ou use saída vertical (9:16).
  23. Gere a narração inteira de uma só vez; proibido gerar frase por frase e depois concatenar. Concatenar = velocidade/pausa inconsistentes entre segmentos + desalinhamento inevitável da linha do tempo, resultando em avaliação do usuário de "fala incoerente". Jimeng pode processar 1500+ caracteres de uma vez (~2 minutos); alimente o texto inteiro de uma só vez.
  24. A linha do tempo vem dos timestamps do ASR da narração, não da detecção de silêncio. Pausa ≠ fronteira de frase; se o número de frases não corresponder ao número de pausas, só resta distribuir igualmente e as legendas ficarão erradas. Use asr_align.py (ffmpeg → faster-whisper word_timestamps → timeline caractere por caractere).
  25. Se a narração for alterada, TODAS as legendas, anotações e durações DEVEM ser recalculadas. Os timestamps das legendas são propriedade do áudio, não da imagem; legendas antigas com novo áudio = trabalho desperdiçado.
  26. Antes da entrega, compare "roteiro" vs "transcrição" com difflib. Similaridade < 0.8, ou frases inteiras faltando/repetidas → problema no áudio, regerar. Erros de homofonia (弘/hóng por 红, 尊/zūn por 遵, 武/wǔ por 吴) são normais do ASR e não contam como erro de leitura.
  27. Após gerar a narração, faça um exame de silêncio. "Texto inteiro enviado de uma vez para TTS" não significa que a plataforma não o divida internamente: Jimeng insere silêncio absoluto de -120dB entre os blocos (teste real: 4.00s / 2.60s). Se a proporção de silêncio >20% ou houver pausas >1.2s → primeiro comprima com tighten_audio.py, depois alinhe a linha do tempo.
  28. O início da legenda DEVE ser ajustado ao quadro de voz real. O ASR, na presença de silêncio longo, marca o start da próxima frase no início do silêncio, fazendo o espectador "ver a legenda e esperar a voz". asr_align.py tem snap_to_voice ativado por padrão; não desligue.

Estrutura de Diretórios (Obrigatória)

D:/04-Projetos-Realizados/<AAMMDD>-<Abreviatura-Projeto>/
  00-Documentos/      Documentos do projeto e notas de código-fonte
  01-Materiais/       Imagens geradas por IA originais, sprite da mão
  02-Legendas-e-Narracao/ SRT, narração mp3, tabela de durações
  03-Anotacoes/       <Nome-da-Imagem>.png + <Nome-da-Imagem>.annotation.json (DEVEM ter o mesmo nome e estar no mesmo diretório, dependência do visualizador de pré-visualização)
  04-Saida-Renderizacao/ silent.mp4 de cada cena, mp4 final
  05-Gravacao-e-Capturas/ gravação da tela mp4 + capturas de tela principais png
  06-Documentacao/    Scripts, registros de operação, lista de verificação

Pré-requisitos

git clone https://github.com/geeklee/srt-whiteboard-animation.git
cd srt-whiteboard-animation && python scripts/prepare_env.py   # Cria .venv e instala opencv/numpy/av

Todos os comandos de renderização subsequentes devem usar srt-whiteboard-animation/.venv/Scripts/python.exe.

Se ocorrer erro _lay_ink() takes 6 positional arguments but 7 were given, é um bug conhecido do repositório: render_stream_whiteboard.py em self._lay_ink(writer, ink_frames, [], set(), None, allowed) remova o None extra (o script scripts/check_bug_fix.py detecta e corrige automaticamente).

Fluxo de Trabalho (7 Etapas: Storyboard → Geração de Imagem → Quadro de Narrativa Espacial → Anotação → Alinhamento da Narração → Renderização → Legendas)

Fase 1 · Roteiro e Storyboard (Confirme antes de prosseguir)

  1. Escreva 6–10 frases de narração, cada uma com 6–10 segundos, total 40–90 segundos.
  2. Analise o storyboard (opcional, também pode ser feito manualmente):
    <ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py legenda.srt --target-sec 30 --min-sec 25 --max-sec 35
    
  3. Saída: tabela de storyboard (Cena / Expressão Central / Sujeito da Cena / Texto da Legenda), pare e aguarde confirmação do usuário.

Fase 2 · Geração da Ilustração Temática por IA

Siga as "Especificações Visuais Uniformes para Geração de Imagens" do projeto e escreva prompts de geração de imagem, cada um DEVE conter:

  • Esboço minimalista à mão livre / rabisco puro a lápis / esboço contido estilo Notion, 16:9, muito espaço vazio, elementos não sobrepostos
  • Fundo bege quente #F5EBD7, linhas cinza escuro #333333, apenas pequenos detalhes em vermelho/laranja/azul
  • Proibido absolutamente qualquer texto (caracteres chineses/letras/números/rótulos/nomes de lugares/títulos/marcas d'água). Proibido foto-realismo/3D/gradientes/alta saturação

Use ImageGen ou Jimeng image para gerar as imagens. Cada imagem corresponde ao significado central de uma cena. Os elementos devem poder ser separados por regiões retangulares.

Após gerar a imagem, três ações OBRIGATÓRIAS:

  1. Remover marca d'água: Imagens geradas por IA geralmente têm AI生成 WORKBUDDY no canto inferior direito. Remova com cv2.inpaint.
  2. Converter para 16:9: Imagens geradas são geralmente 3:2. Adicione fundo bege papel à esquerda e direita e redimensione para 1280×720 (não corte centralmente, pois pode cortar o sujeito).
  3. Limpeza: Remover textura do papel, preservar linhas e detalhes coloridos.
    <ENV_PY> scripts/clean_source.py imagem_original.png 03-Anotacoes/imagem_principal.png
    
    Princípio: papel = filtro mediano de kernel grande, linhas = cinza < papel-18, cor = saturação>70 e cinza<190, manter apenas linhas|cor, preencher o resto com fundo de papel; 8px nas bordas forçam fundo de papel para remover resíduos de borda.

Fase 2.5 · Quadro de Narrativa Espacial (Decide se o vídeo final é "bagunçado" ou não; DEVE ser feito primeiro)

Antes de escrever o prompt de geração de imagem, use texto para fixar a linha principal. Dois tipos:

TemaLinha PrincipalPrincípio de Layout
Rota histórica / Batalha / MigraçãoRota geográfica realPonto de partida na direção real, siga "Oeste→Norte→Leste" fielmente; as coordenadas x/y dos pontos correspondem às direções reais
História da tecnologia / História da empresaUma linha do tempo horizontalTrilho que atravessa a imagem inteira, pontos igualmente espaçados, ícones alternando cima/baixo (cima→baixo→cima), evitar empilhar todos de um lado

Restrições rígidas:

  • Apenas uma linha principal é permitida. Não insira elementos "explicativos" adicionais na linha principal.
  • Elementos não se sobrepõem e cada um deve poder ser delimitado por um retângulo que não cruze a linha principal (caso contrário, as regiões se anulam).
  • A linha principal deve atravessar a imagem inteira: o espaço vazio é distribuído uniformemente em ambos os lados da linha principal. Evite "tudo amontoado no canto inferior direito, canto superior esquerdo vazio". Teste real: o ponto de partida da Longa Marcha (sudeste) no canto inferior direito resultou nos primeiros 20 segundos com tudo empilhado no canto inferior direito, canto superior esquerdo vazio — isso é culpa da composição. Ao gerar a imagem, mova o ponto de partida para o centro e faça a rota preencher a tela.

No prompt de geração de imagem, especifique a posição de cada elemento na imagem ("uma pequena bandeira vermelha no canto inferior direito", "uma ponte de cordas à esquerda do centro"). Não escreva apenas "desenhe uma ponte" — se a posição não for especificada, a composição da IA será inútil.

Fase 2.6 · Dois Paradigmas de Imagem de Fundo (Parâmetros testados, copie diretamente)

Se o vídeo final "preenche a tela" ou "a câmera se move" é decidido na fase de geração de imagem. Dois paradigmas, cada um com seu próprio conjunto de parâmetros; não misture:

A. Tipo Rota Geográfica (História / Batalha / Migração) — Representante: Rota da Longa Marcha do Exército Vermelho

ItemValorJustificativa
ComposiçãoPonto de partida na direção real, rota Oeste→Norte→Leste atravessando a imagemComposição errada, nem o ritmo mais preciso salva
Número de pontos6–8 estaçõesUma estação = duas cenas (desenhar objeto + escrever texto) → 12–16 elementos
Câmera --zoom-fill0.86Muito espaço vazio entre elementos; aproximar para não ficar vazio
DensidadeElementos podem ser grandes ou pequenos (ponte 3x mais larga que bandeira, ok)A rota em si tem coisas de escalas diferentes

B. Tipo Linha do Tempo Horizontal (História da Tecnologia / História da Empresa / Iteração de Produto) — Representante: História do Desenvolvimento de Agentes Inteligentes (A versão que o usuário avaliou como "efeito muito bom", parâmetros copiados diretamente)

ItemValorJustificativa
TrilhoUm eixo principal em forma de S/onda atravessando a imagem, 8 pontos igualmente espaçadosEspaçamento igual → velocidade de pan constante, visual estável
ÍconesAlternando cima/baixo (cima→baixo→cima…), não todos em uma fileiraUma fileira = apenas metade da tela utilizável, desperdiça metade da altura do enquadramento
RótulosPendurados no mesmo lado do ícone, mais para fora: ícone de cima com rótulo acima, ícone de baixo com rótulo abaixoMesma tela que o ícone, a câmera não precisa se mover para escrever
Tamanho da fonteNome principal 36–40px, fonte KaiTi; ano 0.6x abaixoApós zoom, ~60–80px na tela, legível em celular
Câmera --zoom-fill0.70Pontos próximos lateralmente; aproximar demais corta o vizinho ao meio
Proporção de cena por estação (desenhar ícone / escrever)Desenhar ícone 0.60 / Escrever "nome + ano" 0.40Escrever rótulos precisa de tempo suficiente; senão, a escrita termina antes do corte
Eixo da linha do tempoDesenhado entre duas estações adjacentes, processado na mesma cena que o ícone daquela estaçãoCaso contrário, o eixo aparece inteiro de uma vez, quebrando o ritmo

Três avisos importantes para o Tipo B:

  1. Rótulos de texto não usam bounding box de tinta (veja exceção da Lei de Ferro 19). Tags de texto são frequentemente mais largas que a região; usar bbox corta a frase inteira para fora do quadro.
  2. A região do rótulo DEVE ter altura suficiente para "nome principal + ano" em duas linhas; dê 80–90px de altura. Apenas a altura para uma linha do nome principal corta o ano.
  3. O final DEVE dar zoom out para mostrar todo o eixo. O valor narrativo da linha do tempo está na "extensão total"; não dar zoom out nos últimos 3–5 segundos = desperdício.

Linha de base testada (1280×720 / 24fps): 8 pontos × 2 cenas = 16 elementos, vídeo final 43.5s, renderização 33s, --zoom-fill 0.70 --zoom-max 7 --hand-height 350.

Fase 3 · Anotação Pixel-Perfect (A que mais causa erros; DEVE ser verificada)

Primeiro meça as coordenadas, não estime visualmente. Lição real: confundir o tamanho de exibição de 640×360 com as coordenadas reais de 1280×720, resultando em duas rodadas de coordenadas completamente erradas. O correto é usar detecção de cores/linhas para obter o bbox real:

<ENV_PY> scripts/detect_marks.py 03-Anotacoes/imagem_principal.png rotulo   # bbox de componentes conectados de detalhes vermelho/laranja/azul
<ENV_PY> scripts/detect_lines.py 03-Anotacoes/imagem_principal.png rotulo   # bbox de aglomerados de linhas cinza escuro

Para calibração ocular, sobreponha uma régua: scripts/grid_ruler.py imagem_principal.png com_grid.png (grid de 100px, marcando pixels reais).

Auto-verificação da cobertura da região (determina se o vídeo final está completo):

  • Os intervalos x de todas as regiões devem formar uma faixa contínua (sobreposição permitida, sem lacunas).
  • O sujeito contínuo (rota, linha do tempo) deve ser completamente coberto por algumas regiões, caso contrário o vídeo final terá descontinuidades.
  • O último elemento escolhe uma área pequena separadamente (veja Lei de Ferro 3).

Escreva 03-Anotacoes/<nome>.annotation.json:

{
  "sceneId": "scene-01",
  "canvas": { "width": 1280, "height": 720 },
  "storyBasis": "Resumo do evento da legenda desta cena",
  "sceneDurationMs": 56000,
  "elements": [
    {
      "id": "ruijin", "label": "Partida·Ruijin", "sequence": 1,
      "narrativeRole": "Início da Longa Marcha", "subtitle": "Texto da legenda correspondente",
      "type": "point",
      "region": { "x": 265, "y": 515, "width": 145, "height": 100 },
      "reveal": { "direction": "left_to_right", "startMs": 0, "durationMs": 8100,
                  "maskPaddingPx": 16, "protectedRegions": [] },
      "handPath": { "start": [275, 570], "end": [400, 575], "easing": "easeInOut" }
    }
  ]
}
  • canvas DEVE ser igual aos pixels reais da imagem fonte; region DEVE ser em pixels inteiros e dentro da tela.
  • sequence segue a ordem narrativa, não visual da esquerda para a direita.
  • No modo stream, direction/handPath são apenas para exibição no visualizador de pré-visualização; não afetam a caneta do vídeo final.

Gere a imagem de verificação e confirme visualmente quadro a quadro:

<ENV_PY> scripts/region_sheet.py imagem_principal.png anotacao.json 05-Gravacao-e-Capturas/verificacao-region.png

Cada retângulo deve enquadrar exatamente o elemento alvo; se enquadrar espaço vazio ou outros elementos → coordenadas erradas, volte ao passo anterior.

Fase 4 · Narração e Alinhamento da Linha do Tempo

① Narração: Gerar texto inteiro de uma só vez; NUNCA em frases separadas (Lição do retrabalho de 2025-11-05)

"<TTS_PYTHON>  # Qualquer Python 3.9+ com requests
  jimeng-suite/scripts/jimeng_cli.py" audio \
  --text "Narração completa, um parágrafo, separado por pontos finais" \
  --model "SeedAudio 1.0" --submode "全能配音" --voice "狒狒_讲解" \
  --out "xxx-voice.mp3" --outdir "<projeto>/02-Legendas-e-Narracao" --timeout 900
  • Seed TTS + Leitura de Texto foi descontinuado em 2026-10-24, não use mais
  • Alimente o --text com o texto inteiro de uma vez. Pode processar 1500+ caracteres (~2 minutos de áudio) de uma só vez. Não divida mais em pequenos segmentos de 3 segundos: Teste real: dividir um roteiro de 56 segundos em 12 segmentos, gerar separadamente e concatenar resultou em velocidade/pausa/ênfase inconsistentes entre os segmentos, a sensação auditiva era "fala incoerente", e após a concatenação, a linha do tempo e as legendas inevitavelmente se desalinharam — o usuário reprovou diretamente.
  • Divida apenas por parágrafos narrativos (um grupo de frases com uma ideia), 15–40 segundos por parágrafo, vídeo inteiro em 1–3 parágrafos; se possível, use apenas um.
  • A revisão de conteúdo bloqueará: termos militares/violentos/danosos (batalha sangrenta, aniquilar, derrotar, inimigo) retornam JimengContentRejectedError. Reescreva com narrativas neutras ("pagou um preço pesado" "sob bloqueio de fogo"), escrever anos em algarismos chineses (一九三四年) é mais estável.
  • 99 segundos de roteiro geraram áudio em 62 segundos (1.19MB / 99.05s).

② Exame de Silêncio e Compressão de Ritmo (OBRIGATÓRIO, Lição do retrabalho v5)

"Texto inteiro de uma vez para TTS" não significa que a plataforma sintetizou um segmento contínuo: Jimeng divide o texto longo internamente em blocos e os concatena, inserindo silêncio absoluto de -120dB entre os blocos. Teste real da Longa Marcha v4: em 317 caracteres / 99.05s, 31 pausas ≥0.4s totalizam 27.2s (27.5%), incluindo duas pausas de 4.00s @14.05 e 2.60s @70.60 que são amostras de zero digital (pausas naturais têm ruído de fundo, -50-60dB, não chegam a -120dB). A fala efetiva real é de apenas 71.85s (4.41 caracteres/segundo, não é lenta) — a sensação de "ritmo lento, estranho" é causada inteiramente pelos espaços vazios.

# 1) Exame: qualquer silêncio >1.2s deve ser tratado
ffmpeg -hide_banner -i xxx-voice.mp3 -af "silencedetect=noise=-38dB:d=0.35" -f null - 2>&1 | grep silence_start

# 2) Compressão: preserva pausas naturais por nível, sem alterar nenhum conteúdo de fala
<ENV_PY> scripts/tighten_audio.py 02-Legendas-e-Narracao/xxx-voice.mp3 \
  02-Legendas-e-Narracao/xxx-voice-tight.mp3 --report 06-Documentacao/_tighten.json

tighten_audio.py: Janela RMS de 50ms, <-40dB considerado silêncio; pausas ≥1.0s → manter 0.55s, pausas 0.35–1.0s → manter 0.32s, início do arquivo manter 0.12s, reamostrar e concatenar. Teste real v4→v5: 99.05s → 83.15s, proporção de silêncio 27.8% → 14.0%, sem nenhuma pausa >0.7s.

A compressão altera o comprimento do áudio. Posteriormente, ASR, timing, SRT, ASS, durações da anotação, e total-ms da renderização DEVEM ser todos recalculados com base no áudio comprimido (Lei de Ferro 25). --reuse-asr é apenas para ajustar quebra de linha das legendas; se o áudio mudar, NUNCA reutilize os dumps antigos.

③ Linha do Tempo: Use os timestamps do ASR da própria narração, não adivinhe com detecção de silêncio

<ENV_PY> scripts/asr_align.py 02-Legendas-e-Narracao/xxx-voice.mp3 06-Documentacao/roteiro.txt \
  --model small \
  --out-timing 03-Anotacoes/xxx.annotation.timing.json \
  --out-srt 02-Legendas-e-Narracao/xxx.srt --max-chars 18 \
  --dump-asr 06-Documentacao/_asr.json

O que asr_align.py faz: ffmpeg converte para 16k mono → faster-whisper (word_timestamps=True) → cria uma linha do tempo caractere por caractere → localiza cada frase do roteiro no texto transcrito completo (busca exata → ancora com primeiros 8 caracteres → fallback) → gera timing.json (compatível com segments[i/start/end/text]) e SRT de linha única.

Por que não usar silencedetect: O silêncio indica apenas "há uma pausa aqui"; respiração, ênfase também causam pausas. Se o número de frases e o número de pausas não coincidirem, só resta distribuir por número de caracteres, e as legendas inevitavelmente se desalinharão.

asr_align.py também faz, por padrão, ajuste de fronteira de frase (snap_to_voice): o faster-whisper, na presença de silêncio longo, tende a marcar o start da próxima frase no início do silêncio, fazendo a legenda aparecer antes do som, com o espectador olhando para o texto e esperando a voz (v4 adiantou em 4 segundos inteiros). O script encontra o ponto de início real da voz em uma grade de 20ms e corrige; --no-snap desativa.

④ DEVE verificar o conteúdo do áudio (único critério objetivo para julgar "fala incoerente"):

<ENV_PY> -c "..."   # comparação difflib entre roteiro e full do _asr.json

Similaridade deve ser >0.8; o modelo small para nomes próprios chineses geralmente produz apenas erros de homofonia (弘/hóng por 红, 尊/zūn por 遵, 武/wǔ por 吴, 锁/suǒ por 索/suǒ). Se a estrutura (ordem das frases, comprimento das frases) for consistente, a leitura está correta. Se frases inteiras faltarem, repetirem ou pularem → o áudio em si tem problema, regerar.

⑤ Três regras para quebra de linha de legenda (todas em asr_align.py::split_subs):

  • Primeiro divida em orações curtas por pontuação; só combine com a próxima oração se o bloco atual for < limit×0.55 — caso contrário, pode ocorrer colagem indesejada (ex.: "embarcou na famosa jornada da Longa Marcha naquele ano")
  • Após combinação, não ultrapasse o limite (sem margem de +3, 20 caracteres igualmente divididos podem cortar "...caminhar só" com um caractere isolado)
  • Se um bloco único ainda for muito longo, divida igualmente; se a cauda tiver ≤4 caracteres, junte à linha anterior.

⑥ Espaço em branco no final (implementação da Lei de Ferro 18): Adicione "tail": 0.30 à última estação; os últimos 30% daquele segmento não terão elementos, e o renderizador usará esse tempo para zoom out + quadro congelado. Teste real: em um vídeo de 99 segundos, deixar 4 segundos foi o suficiente para ver a imagem completa.

⑦ Distribuição da duração no storyboard: Um elemento corresponde a uma frase (ou uma ação em uma frase); durationMs é diretamente a duração dessa frase. Dentro do segmento, divida a proporção ratio para "desenhar objeto / escrever texto". Veja scripts/make_annotation_v3.py.

Fase 5 · Renderização, Composição, Verificação

<ENV_PY> srt-whiteboard-animation/scripts/render_stream_whiteboard.py \
  03-Anotacoes/imagem_principal.png 03-Anotacoes/imagem_principal.annotation.json 04-Saida-Renderizacao/xxx-silent.mp4 \
  01-Materiais/hand-55pct.png \
  --cap-long-edge 1280 --fps 24 --total-ms <milissegundos da narração>
  • --cap-long-edge igual ao lado maior da imagem fonte → sem redimensionamento; para pré-visualização, pode-se reduzir para acelerar.
  • --total-ms = duração da narração em milissegundos, garante que a imagem não termine antes da narração.
  • Saída: OUTPUT=<caminho>

Composição da faixa de áudio:

ffmpeg -y -i xxx-silent.mp4 -i narracao.mp3 -c:v copy -c:a aac -b:a 192k -shortest video_final.mp4

Três pontos OBRIGATÓRIOS a verificar (um quadro de cada):

  1. Início: fundo bege limpo, sem linhas prematuras.
  2. Meio: linhas contínuas, cores corretas (vermelho/laranja/azul presentes).
  3. Final: imagem completa, sem linhas quebradas, sem grandes áreas vazias.

Se a diferença de duração for >1s, ajuste --total-ms ou os durationMs e renderize novamente.

Fase 5.5 · Câmera e Escrita (Núcleo da v3; decide se "parece estar desenhando")

O renderizador upstream desenha em uma tela fixa do início ao fim. Para fazer a imagem parecer linguagem de câmera, adicione uma subclasse render_camera_whiteboard.py, sem alterar o código do repositório (o script scripts/render_camera_whiteboard.py já está pronto para reuso):

<ENV_PY> scripts/render_camera_whiteboard.py 03-Anotacoes/imagem_principal.png 03-Anotacoes/imagem_principal.annotation.json \
  04-Saida-Renderizacao/xxx-silent.mp4 01-Materiais/hand.png \
  --canvas-long-edge 2560 --frame 1280x720 --fps 24 --grid-edge 20 \
  --zoom-fill 0.86 --zoom-max 7 --hand-height 350 --total-ms 56064

Ele faz seis coisas:

MecanismoImplementaçãoPor quê
Enquadramento da câmeraRenderiza tela 2560×1440, a cada quadro corta e redimensiona para 1280×720 com base no viewportElementos podem "preencher a tela", depois a câmera se move
Enquadramento pelo bounding box da tintaQuadro alvo = bbox da tinta real dentro da máscara allowed da cena, não a regiãoA região geralmente é maior que o elemento, podendo abranger vizinhos; usar a região diretamente → espaço vazio no centro, elemento empurrado para o canto (v3 primeira versão errou aqui)
Seguimento da câmeraSuavização exponencial entre cenas adjacentes: tau 0.20/0.30, cena de escrita pad 0.34, cena de desenho pad 0.05Produz naturalmente o efeito de pan, sem quadros de transição extras
Buffer de transição0.22s (~5 quadros) de tela vazia no início de cada cena antes de começar a desenharA câmera leva 0.3–0.6s para se mover da cena anterior; desenhar durante esse período => "caneta desenhando fora do quadro", principal causa de "mão se move mas nada aparece"
Imagem completa no finalApós a última cena, o target é definido como a tela inteira, preenchendo quadros até --total-ms"Mostrar uma imagem completa no final"
Tamanho fixo da mão na telaA mão não é desenhada na tela, mas sobreposta no quadro de saída (--hand-height 350)Caso contrário, ao aproximar, a mão preencheria a tela; 430 ainda é grande demais, cobrindo o texto sendo escrito

--zoom-fill DEVE ser definido separadamente com base na densidade da imagem (mesmo parâmetro não serve para todas as situações):

Tipo de Imagemzoom-fillMotivo
Esparsa (mapa de rota/diagrama de fluxo, muito espaço vazio entre elementos)0.86Pode aproximar com segurança; a tela não fica vazia
Densa (linha do tempo horizontal, elementos próximos lateralmente)0.70Aproximar demais corta o vizinho ao meio, parecendo bagunçado

Restrição geométrica do 16:9: Elementos verticais longos (tabuleiro de xadrez, celular) são limitados pela altura da tela. Em uma tela cheia, ocupam apenas 60–70% da altura e ~25% da largura — isto é geometria, não bug. Para preencher mais, desenhe o elemento mais largo ou use saída vertical (9:16).

Escrita (escrever nomes de lugares/anos):

# 1) Primeiro, incorpore os rótulos na imagem de fundo (coordenadas baseadas na anotação de 1280×720; o script ajusta automaticamente para duas linhas com base no tamanho da fonte)
<ENV_PY> scripts/add_handwriting.py fundo_2x.png 03-Anotacoes/imagem_principal-v3.png 02-Legendas-e-Narracao/rotulos.json
# 2) Na anotação, para esta cena, defina "inkPath": "textwrite"; a região é a caixa de texto

O modo textwrite avança coluna por coluna da direita para a esquerda, com a ponta da caneta na frente da revelação, garantindo que o texto seja completamente escrito (o rastreamento de esqueleto perde detalhes finos das pinceladas; não use). Os primeiros 30% dos quadros seguram a caneta até a câmera se posicionar; então começa a escrever — caso contrário, o texto será escrito para fora do quadro. Formatação do rótulo: nome principal 34–40px, fonte KaiTi (C:/Windows/Fonts/simkai.ttf) + ano 0.6x abaixo, cor (58,56,52) mesma faixa de cor do esboço, mas ligeiramente mais clara.

Divisão da Cena: Uma estação = duas cenas. Primeiro desenha o objeto (0.6 da duração), em seguida escreve o texto (0.4 da duração). O quadro de enquadramento da cena de escrita é sua própria caixa de texto; assim, a câmera se aproxima ligeiramente, como se "a caneta se move para escrever ao lado".

Fase 6 · Queima de Legendas (SRT → ASS → Queima no Vídeo)

Não crie uma linha do tempo de legendas separada; use diretamente o .timing.json produzido na Fase 4 — ele já está alinhado frase por frase com a narração; o momento em que a legenda muda é o momento em que a caneta para. Isso é "alinhamento de ritmo".

# 1) timing.json -> SRT (**linha única**, linhas longas são automaticamente divididas em múltiplas legendas consecutivas)
<ENV_PY> scripts/make_srt.py 03-Anotacoes/imagem_principal.annotation.timing.json 02-Legendas-e-Narracao/x.srt \
  --max-chars 14

# 2) SRT -> ASS (PlayRes explícito + \pos fixando na borda inferior, tamanho da fonte e posição são pixels reais)
<ENV_PY> scripts/srt2ass.py 02-Legendas-e-Narracao/x.srt 02-Legendas-e-Narracao/x.ass \
  --video-width 1280 --video-height 720 --font-size 46 --margin-bottom 40

O script srt2ass.py atualmente não lida corretamente com caracteres multibyte; use a versão modificada em scripts/srt2ass.py deste projeto (adiciona explícito no final de cada bloco de legenda).

Verificação final: Exiba o vídeo final e a legenda ASS lado a lado. Verifique:

  • A quebra de linha da legenda corresponde exatamente à pausa da narração?
  • O início e o fim da legenda estão alinhados com o início e o fim da fala (erro tolerável <0.15s)?
  • O tamanho da fonte é adequado no celular (46px em 720p, a olho nu, linha única ≤14 caracteres chineses, é o limite superior)?

Se a legenda estiver desalinhada, a causa pode ser: o áudio comprimido não foi realinhado (Lei de Ferro 25), ou o ASR não utilizou snap_to_voice (Lei de Ferro 28). Ajuste e repita o script asr_align.py.

Lista de Verificação Final (24 Itens, Completar Antes da Entrega)

#ItemMétodo de Verificação
1Imagem fonte é original, não a de examples/Abrir a imagem, confirmar visualmente
2Marca d'água removida50px do canto inferior direito, sem pixels AI生成 WORKBUDDY
3Imagem em 16:9 ou redimensionada com preenchimentoffprobe -v error -select_streams v:0 -show_entries stream=width,height
4Imagem limpa (sem ruído de fundo)Comparar clean_source.py antes e depois; a imagem depois não deve ter pontos pretos no fundo
5Regiões da anotação são contínuas sem lacunasregion_sheet.py
6Último elemento não é a tela inteiraConfirmar que a última região não é (0,0,1280,720)
7Narração gerada de uma só vezVerificar comprimento do áudio > 30s; se houver vários arquivos, verificar consistência
8Áudio passou por tight/en e não há silêncio >1.2stighten_audio.py --report
9ASR usa

[continuação truncada por tamanho; o conteúdo completo foi fornecido no original]

Habilidades Relacionadas