Communitygithub.com

znlgis/video-studio

Gere vídeos em chinês do zero com código (documentários, vídeos promocionais, animações curtas): roteiro → narração edge-tts → linha do tempo baseada em duração real medida → renderização quadro a quadro com PIL ou Canvas → codificação ffmpeg → mixagem em nível de amostra com numpy → controle de qualidade automático.

O que é video-studio?

O video-studio é uma skill de agente de IA especializada na produção de vídeos inteiramente por código, sem necessidade de素材 (assets) externos. Ela transforma um roteiro (film.json) em um vídeo finalizado através de um pipeline determinístico e modular: primeiro, mede-se a duração real da narração gerada por TTS (edge-tts); depois, renderiza-se cada quadro como uma função pura (PIL ou Canvas); em seguida, codifica-se o vídeo com ffmpeg e realiza-se a mixagem de áudio em nível de amostra com numpy; por fim, um gate de controle de qualidade automático valida o resultado. A skill impõe regras estritas para evitar problemas comuns como dessincronização de áudio/vídeo e baixa qualidade de imagem, e exige que o agente primeiro proponha opções de design visual e sonoro antes de renderizar o vídeo final, garantindo variedade e controle criativo. Ela é ideal para criar vídeos explicativos, documentários, animações e vídeos promocionais, especialmente em chinês, e é ativada quando o usuário solicita a criação de um vídeo ou menciona ferramentas como ffmpeg e edge-tts.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/znlgis/dsh-video-studio-skill/tree/HEAD/skills/video-studio

Perguntar na sua IA favorita

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

Documentação

Video Studio (video-studio)

Em uma frase: vídeo = roteiro + duração real do áudio + função determinística de quadro a quadro + codificação ffmpeg + controle de qualidade automático. Imagem e som são gerados inteiramente por código, sem素材, reproduzíveis, permitindo pausa e retomada, e aceitação automatizada.

0. Regras de Ferro (violar qualquer uma delas causará problemas no produto final)

  1. Áudio primeiro, imagem depois. A linha do tempo deve ser deduzida a partir da duração real do TTS, nunca estimada por contagem de caracteres. Uma linha do tempo baseada em contagem de caracteres começará a se desviar visivelmente a partir da 20ª frase.
  2. Cada quadro é uma função pura: draw(t, frame) -> Image. Proibido usar random/time(), use filmkit.hash2/hash3/vnoise para aleatoriedade. O mesmo número de quadro deve sempre produzir a mesma imagem.
  3. Legendas e textos grandes devem estar na zona segura: corpo y ∈ [58, 974], legenda inferior y ∈ [872, 1030], nunca ultrapassar 1030.
  4. Quadros originais direto para codificação: quadro PIL → pipe rawvideo → libx264. Não use cv2.VideoWriter como arquivo intermediário (2x mais lento e com perda secundária).
  5. Mixagem com numpy em nível de amostra, não use um comando ffmpeg com centenas de entradas (causará silêncio não determinístico no Windows).
  6. Antes da entrega, execute o script de controle de qualidade + veja pessoalmente 3 quadros estáticos (use read_image). "Consegue gerar imagem" não é igual a "está correto".
  7. O som segue a imagem: registre pontos de sinalização no código da cena (K.CueBus / hook cues() em scenes.py), não ajuste manualmente depois. Se a imagem mudar (trocar janela de permanência, ajustar ritmo), os pontos de sinalização mudam automaticamente.
  8. Se não entender, adicione pausa; não desacelere o vídeo inteiro: hold (global) em film.json ou lines[].hold (por frase) adiciona pausa após a fala; use bpm para alinhar a música de fundo.

0.5 [Regra de Ferro] Proponha primeiro, produza depois (independentemente de o usuário solicitar)

Se o usuário não pedir, você ainda deve apresentar um plano primeiro. Esta é a regra de produto mais importante desta skill, não uma sugestão. Motivo: se a aparência é escolhida pelo programa, o usuário não participa da decisão do início ao fim, resultando em "todo vídeo parece igual". Portanto, o fluxo é fixo: amostragem no local para gerar algumas opções → usuário escolhe uma → congela o design → depois produz o vídeo.

Ação fixa (a primeira coisa em qualquer tarefa de novo vídeo):

python propose.py --brief "<tema/público-alvo/duração/tom>" --script film.json --n 3

Ele produzirá N opções candidatas (cada uma = um design + 4 quadros estáticos + custo + distância de design para outras opções), salva em work/proposal.json, e imprime "Confirme uma / se não gostar, use --revise". Totalmente offline, em segundos.

Reação do usuárioO que você deve fazer
"É esse" / escolheu umpython propose.py --confirm --candidate B, só então permite renderizar
"Muito frio/muito chamativo/mude a voz/mais rápido"python propose.py --revise "<texto original>" — nova rev, rev antiga mantida para diff
"Nenhum presta, comece de novo"Mesmo acima, escreva os requisitos específicos (o texto de --revise será salvo em proposal.feedback)
Disse explicitamente "você decide"Ainda assim, primeiro apresente opções, escolha uma que você recomenda e justifique, peça a aprovação do usuário

Restrições rígidas:

  1. Antes de propose.py --confirm, o --stage frames/all de render.py será bloqueado pelo gate de proposta (aviso padrão, --require-proposal falha).
  2. As opções candidatas devem diferir em ≥2 dimensões de design (família de cores/motivo/transição/easing/numeração/música de fundo/tom de voz). Apenas mudar a cor não conta como múltiplas opções, diversity.vs[].n < 2 será sinalizado.
  3. Cada opção candidata deve ter uma intenção em chinês (pitch), explicando "o que esta opção quer alcançar" — não "Opção A/Opção B".
  4. Não há modelos internos para reutilizar: todos os campos de design.json estão abertos (35 campos de tema + cinco conjuntos de assinatura de linguagem visual + som/música/ritmo), a cor é qualquer ponto no espaço contínuo de matiz, não uma seleção de algumas aparências prontas. Cada proposta é gerada no local — o mesmo brief executado duas vezes resultará em designs diferentes (para reproduzir, passe --seed).
  5. Registro de deduplicação entre vídeos (ativado por padrão): --confirm registrará o design congelado em ~/.dsh/video-studio/design-ledger.json (máx. 500 registros), e na próxima proposta evitará ativamente designs entregues recentemente — se a distância de design entre a opção candidata e qualquer vídeo no registro for < 2 dimensões, ela automaticamente trocará nonce e reamostrará com alerta. Este é o mecanismo para "vídeos diferentes não se repetirem" evoluir de "aleatoriedade" para "com memória". --show-ledger verifica o registro, --no-ledger ignora nesta execução, a variável de ambiente VIDEO_STUDIO_LEDGER muda o caminho ou define off para desligar. Falhas de leitura/escrita do registro sempre tratam como tabela vazia, nunca bloqueiam a produção do vídeo.

Apenas um caso pode pular a proposta: reparo puro / re-renderização pura (sem mudança de conteúdo, sem mudança de design, apenas corrigir dessincronização de áudio/vídeo ou re-exportar). Neste caso, execute de forma diferente de --require-proposal e explique na resposta "Esta execução não teve proposta, porque…".

0.5.1 De onde vem a aparência: apenas dois caminhos

Não há "estilo interno" para escolher. Escrever qualquer valor style (auto / legacy / algum id) em film.json será explicitamente rejeitado com duas etapas de orientação — o roteiro não determina a aparência, a aparência é determinada por work/design.json. Da mesma forma, a skill não possui tabelas THEMES/STYLES/VISUAL_LANGUAGES, nem presets/; a música de fundo é gerada no local por musicgen.py conforme parâmetros, e scenes.draw_frame não tem branch legado.

Para obter uma aparência específica, há apenas dois caminhos:

# ① Requisitos em linguagem natural (reconhece palavras-chave em must_have/avoid)
python propose.py --brief "Lançamento de tecnologia, azul escuro, widescreen" --script film.json

# ② Escrever design.json diretamente (todos os campos abertos, cor é qualquer RGB)
python render.py --script film.json --stage all     # Pré-requisito: work/design.json já existe

Contrato detalhado em references/proposal.md e schemas/design.schema.json.


1. Cinco passos para começar (sem cópia: não copie scripts, não copie modelos)

Este caminho não tem nenhuma ação de "cópia". Os scripts da skill permanecem no diretório da skill e são executados diretamente; seu diretório de projeto só precisa de um film.json (além dos work/ e out/ gerados pela execução).

<SKILL_DIR> se refere ao resourceBase.path retornado pela ferramenta skill ao carregar esta skill. Quando instalado como plugin, é o skills/video-studio/ dentro deste pacote de plugin (node_modules/@dsh-gis/dsh-video-studio-skill no perfil é um link). Se não for encontrado, localize pelo nome: find ~ -path '*/skills/video-studio/scripts/filmkit.py'.

<REPO> se refere à raiz do pacote (raiz do repositório, ou node_modules/@dsh-gis/dsh-video-studio-skill/ no perfil após instalação), ou seja, dois níveis acima de <SKILL_DIR>. tools/ e schemas/ estão em <REPO>, não em <SKILL_DIR>.

# 1) Apenas escreva um film.json (o único arquivo que você precisa escrever manualmente no diretório do projeto)
mkdir -p myfilm && cd myfilm
cat > film.json <<'JSON'
{
  "title": "Meu Vídeo",
  "lines": [
    {"id": "l01", "kind": "hook",  "chapter": "01 · Abertura",
     "text": "Esta é uma imagem e som gerados do zero com código.", "sub": "Sem素材, todo código", "big": "Sem素材, todo código"},
    {"id": "l02", "kind": "body",  "chapter": "02 · Corpo", "kicker": "PIPELINE",
     "text": "Todo o pipeline tem apenas cinco etapas: escrever roteiro, gerar narração, alinhar linha do tempo, renderizar quadro a quadro, mixar e sintetizar.",
     "sub": "Cinco etapas, cada uma salva", "big": "Cinco etapas",
     "items": [{"text": "Roteiro film.json"}, {"text": "Narração"}, {"text": "Linha do tempo"}, {"text": "Renderização quadro a quadro"}, {"text": "Mixagem"}]},
    {"id": "l03", "kind": "outro", "chapter": "99 · Encerramento",
     "text": "Primeiro defina o roteiro, depois o som, por último a imagem.", "sub": "Ordem é mais importante que esforço", "big": "Ordem é mais importante que esforço"}
  ]
}
JSON
#    lines[].text = texto para narração (oral natural); sub = legenda na tela (curta); kind = hook/body/quote/outro/emphasis/calm
#    Estilo visual/música/tom/ritmo são todos decididos pelo design, film.json **não** tem campo style.
#    Validação de contrato (opcional, alguns segundos):
python <SKILL_DIR>/scripts/filmcheck.py film.json

# 2) Verifique o ambiente primeiro (opcional; 69 barreiras de regressão, cerca de 10–20 segundos, sem internet; módulos opcionais ausentes marcam SKIP)
python <SKILL_DIR>/scripts/selfcheck.py

# 3) [Regra de Ferro] Primeiro proponha, depois produza — o mesmo brief sempre dá resultados diferentes, então o usuário deve ver primeiro
python <SKILL_DIR>/scripts/propose.py \
       --brief "Três anos de grandes modelos de IA, 30 segundos de科普, azul escuro" \
       --script film.json --work work
#    Produz 3 candidatos: cada um com design + contato de quadros estáticos (work/proposals/rev1/_compare.png) + distância de design,
#    resultado em 2 segundos, totalmente offline. Veja a imagem lado a lado, escolha um.

# 4) Confirme o candidato, congele o design (work/design.json) — a partir daqui, a renderização só reconhece este design
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
#    Se não gostar, escolha outro: --revise "Mais frio, menos decoração, troque para voz feminina" (nova rev, rev antiga mantida para diff)
#    Para reproduzir uma execução: adicione --seed 42 na primeira proposta (sem seed = novo nonce a cada vez, esta é a fonte de "cada vídeo diferente")

# 5) Primeiro veja quadros estáticos, depois produza o vídeo completo; se --scenes não for usado, usa o renderizador interno da skill
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills \
       --stills "l01:3,l02:5,l03:2.5"
#    → work/stills/_sheet.png: veja pessoalmente com read_image, não pule

python <SKILL_DIR>/scripts/render.py --script film.json --work work \
       --out out/film.mp4 --stage all
#    TTS→Tipografia→Música→Efeitos sonoros→Mixagem→Quadro a quadro→Codificação→Controle de qualidade integrado; todos os artefatos intermediários em work/, permitindo pausa e retomada

Sobre --scenes (a chave para não copiar): Quando render.py não usa --scenes, a ordem de localização é ① scenes.py no mesmo diretório do roteiro → ② assets/scenes.py interno da skill. Portanto, por padrão, você não precisa escrever nada, nem copiar nada; o renderizador interno produz o vídeo diretamente; apenas se quiser modificar a imagem coloque seu próprio scenes.py no mesmo diretório do roteiro (implementando draw_frame abaixo e opcionalmente cues()), ele será encontrado automaticamente — também pode usar explicitamente --scenes my_scenes.py. O código-fonte do renderizador interno é o melhor exemplo: <SKILL_DIR>/assets/scenes.py.

# Assinatura real (seis parâmetros, faltar um quebra): `scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
    # scene    : dicionário desta seção na linha do tempo (id / kind / scene_type / sub / big / items / stats ...)
    # t_local  : tempo dentro da seção (segundos, 0..dur) — animações de entrada/contínuas avançam por ele dentro da seção
    # dur      : duração total desta seção (segundos)
    # t_global : tempo total do vídeo (segundos) — animações contínuas entre seções (push lento de câmera, respiração) usam ele
    # script   : dicionário do roteiro/plano completo (plan, cor do tema, palavras, etc.; tem cache, não modifique in-loco)
    # frame    : número de quadro absoluto (inteiro) — use para ruído determinístico/fase, não use time()
    return img   # PIL.Image, deve ser 1920x1080 RGB (use K.new_frame() como ponto de partida mais estável)

Ao escrever scenes.py personalizado, primeiro copie um renderizador (_draw_hero etc.) de <SKILL_DIR>/assets/scenes.py, que já contorna as armadilhas de função pura, cache de tipografia, templates pré-renderizados, etc.

Por que propor primeiro: não há "estilo interno" para escolher, a aparência é determinada por design.json. O mesmo brief executado duas vezes resultará em designs diferentes (cor/motivo/transição/música/tom de voz podem ser diferentes), então o fluxo deve ser "gerar opções → humano confirmar → congelar → produzir"; se renderizar sem confirmar, render.py mostrará um aviso de proposta (com --require-proposal, falha diretamente). Contrato de design em references/proposal.md.

Execução por etapas (cada etapa faz apenas sua tarefa, pode ser reexecutada separadamente após salvar):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style   # Analisa design confirmado, imprime tema/linguagem visual/rotação de layout/estilo de legenda
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts      # Apenas sintetiza narração + mede duração real (cache com validação de entrada)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout   # Apenas alinha linha do tempo + seleciona composição, salva timeline.json / sub.srt / storyboard.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues     # Apenas gera pontos de sinalização -> work/cues.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score    # Apenas gera música de fundo -> work/music.wav (musicgen orquestração no local)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx      # Apenas gera trilha de efeitos sonoros (sfxkit, conforme cues.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio    # Apenas mixa (narração+música+efeitos, narração prioritária)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames   # Apenas renderiza quadros (--engine pil|web, --jobs N paralelo por bloco)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux      # Apenas sintetiza (cópia do fluxo de vídeo, duas passagens loudnorm)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa       # Apenas controle de qualidade (somente leitura, apenas escreve work/qa/)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills   # Apenas renderiza quadros estáticos mestres + folha de contato
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills   # Assinatura humana de um gate (escreve work/signoff.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain  # Apenas relata "quais etapas estão desatualizadas, por quê, o que executar", sem executar nenhuma etapa

Ordem completa de --stage all: style → tts → layout → cues → score → sfx → audio → frames → mux → qa. Qualquer problema em qualquer etapa interromperá ali, não continuará com entrada ruim.

Calcule o custo antes de renderizar (opcional; estimate.py é uma ferramenta do repositório, chamada por <REPO>):

python <REPO>/tools/estimate.py --script film.json --jobs 4

Ele fornece contagem de quadros/tempo por etapa/disco/sugestão de divisão (--json para leitura por máquina), é limite superior de intervalo não uma promessa; se work/timeline.json existir, usa duração real, caso contrário estima por contagem de caracteres. Para economizar VMAF do vídeo inteiro, escreva "qa": {"vmaf": {"mode": "off"}} em film.json (estimate e render leem este campo, G11 registra skipped).

Veja o comprovante de entrega antes de aprovar:

cat out/QC-REPORT.md     # Matriz de gate dividida em pass / fail / skipped; skipped = não executado nesta execução, não significa aprovado
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release

A aprovação está vinculada à impressão digital do conteúdo: se o roteiro / scenes.py / design mudarem, a aprovação antiga é automaticamente invalidada, exigindo nova visualização e nova aprovação. Para forçar antes da entrega (skipped também conta como falha): adicione --strict-gates.

Renderização de vídeos longos (paralelização por bloco + pausa e retomada):

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4 --resume

Blocos salvos em work/segments/, nomeados seg_<quadro_inicial>_<quadro_final>.mp4; antes da concatenação, verifica explicitamente lacunas, bloco faltando causa erro, não permite "produzir vídeo com segmento faltando". Bloco padrão calculado como quadros_totais / jobs / 4 arredondado para segundos inteiros (jobs=4, 900 quadros → cerca de 8 segundos por bloco), pode ser sobrescrito com chunk_frames em film.json.

Após alterar falas / alterar scenes.py / alterar design / alterar parâmetros de codificação, --resume recusará reutilizar blocos antigos (a impressão digital do plano é armazenada em work/segments/_plan.json), e sugere rm -rf work/segments — intencional: blocos antigos com nova linha do tempo resultariam em um vídeo com "imagem e narração dessincronizadas", e o gate de duração não detectaria.

2. Oito etapas padrão (P0–P7)

EtapaSaídaAção chaveErro comum
P0.5 Definir designwork/design.json + work/style.jsonpropose.py gera opções → --confirm congela design; --stage style apenas analisa design confirmado e imprime tema/linguagem visual/rotação de layout/legendaPular → render.py erra (sem design não há aparência); renderizar sem confirmar gera aviso de proposta
P0 Definir baseCONVENTIONS.mdResolução/framerate/duração/BPM→quadros calculados uma vez, referenciados por todo o projetoCada arquivo escreve números mágicos como 150 → bordas de cena desalinhadas
P1 Escrever roteirofilm.jsonCada frase separa text (narração) e sub (legenda); cada seção tem um kind; opcional lines[].type bloqueia composiçãoUm campo usado para dois fins → legenda muito longa ou leitura estranha; não escreva style (não afeta mais a imagem, aparência determinada por work/design.json)
P2 Verificação de fatosFACTS.mdCada número/ano na tela tem número de fonte; criar lista negra "não pode ir para a tela"Pular esta etapa → números na imagem e na narração conflitam, primeiro comentário aponta seu erro
P3 TTSwork/tts/*.mp3 + tts.jsonedge-tts sintetiza frase por frase, ffmpeg -f f32le mede duração real, cache em discoEstimar duração por contagem de caracteres → legenda desvia
P4 Alinhar linha do tempowork/timeline.json + sub.srtdur = lead + fala medida + tail, a fala deve estar dentro desta seçãoSeção muito curta → narração cortada pela próxima seção
P5 Mixagemwork/audio.wavBarramento numpy: narração + música + efeitos sobrepostos por ponto de amostraMúltiplas entradas ffmpeg para mixagem → silêncio não determinístico
P6 Mestre3 quadros estáticosRenderize 3 quadros primeiro para ver composição/cor/peso da fonte, depois renderize o vídeo inteiroRenderizar o vídeo inteiro diretamente → 36000 quadros depois descobre que a fonte é feia
P7 Vídeo finalout/film.mp4rawvideo→libx264 CRF18 + duas passagens loudnorm para -16 LUFSBitrate muito baixo/loudness -25 LUFS → "parece rascunho"

Nota: O padrão em filmkit / render.py é crf=18; assets/film.json é apenas um fixture de teste (para testar o pipeline completo e regressão de quadro dourado/gate, não um modelo para você copiar), ele usa 17. Ao escrever seu próprio roteiro, não escreva crf para usar o padrão 18; para texto mais nítido, pode usar 17 (volume ~ +30%).

3. Como escolher o motor de renderização

CenárioEscolhaVelocidade realAdequado para
Texto/imagem/dados/ ciencia/estilo UIPIL (numpy+cv2 auxiliar) — padrão filmkit40–80 fps @1080p30 (exemplo 75)A maioria dos vídeos科普 chineses, relatório para vídeo
Precisa de vetor/partícula/3D/textura de papel suaveNavegador headless Canvas2D/WebGL + captura de tela CDP → image2pipe (webengine.py)80–130 ms/quadro (placa gráfica integrada, captura PNG)Vídeos longos de nível documentário, precisa de linguagem visual complexa
Animação de dados puramanim (se instalado)Depende da cenaMatemática/derivação de fórmulas

A velocidade na tabela acima é medida em máquina única sem carga (muda com máquina diferente, imagem pesada; consulte §12). Não considere o intervalo como garantia.

O design cobre apenas o motor PIL (engine: pil, padrão): a cor/fonte/layout/motivo em work/design.json é lida por assets/scenes.py. --engine web exige que a página seja escrita por você (assets/web/ é apenas uma página de exemplo funcional, não um modelo de produção de vídeo), a imagem não segue o design; mas a música e a narração ainda seguem design.json (não dependem do motor de renderização).

Padrão é PIL: menos dependências (numpy+Pillow suficiente), inicialização rápida, determinismo forte, baixo custo de depuração. Só use a solução de navegador para "deformação de partícula em um único take / textura de shader em tempo real":

cp -r <SKILL_DIR>/assets/web ./web                     # Modelo de página (Canvas2D nativo, zero bibliotecas de terceiros)
pip install playwright && python -m playwright install chromium   # Apenas necessário ao usar motor web
python render.py ... --engine web --stage cues        # Exporta pontos de sinalização da página
python render.py ... --engine web --stage frames --jobs 3 --resume
python webengine.py textqa ./web/index.html            # Verifica sobreposição de texto / texto fora da tela / texto que passa rápido / texto que nunca aparece

Compensação entre os dois caminhos (pense bem antes de escolher, trocar de motor exige refazer a imagem):

PIL (engine: pil)Navegador (engine: web)
Dependênciasnumpy + Pillow (+cv2 para comparação de nitidez)+ playwright + Chromium/Edge
Velocidade40–80 quadros/segundo8–12 quadros/segundo (captura de tela quadro a quadro + codificação, uma ordem de magnitude mais lento)
Linguagem de imagemComposição bitmap, gradiente, geometria, textoCanvas2D/WebGL: partícula, deformação, shader, animação vetorial
DeterminismoNatural (função pura para desenho)Garantido por "posicionar precisamente a linha do tempo em f/FPS" (window.renderFrame)
DepuraçãoMude um quadro, execute um quadroNo navegador, index.html?dev=1&t=segundos veja diretamente, bugs fáceis de encontrar
Adequado paraTexto/imagem/dados/ ciencia/vídeo promocionalVídeos longos de nível documentário, precisa de linguagem visual em um único take

Três restrições rígidas do motor de navegador (já implementadas em webengine.py, siga-as ao escrever sua própria página):

  1. Todos os tweens devem estar em uma linha do tempo principal pausada, renderizar o quadro f = posicioná-la em f / FPS segundos; não pode depender de reprodução.
  2. Conteúdo programático como loops/partículas/ondas deve usar um atualizador que calcula diretamente com o tempo (update(fn)), não use setInterval para acumular.
  3. A página deve expor window.ready / DURATION / renderFrame(f) / getCues(); não use CDN, não faça requisições de rede.

Para reprodutibilidade em nível de byte entre sessões de navegador (reinicialização, renderizações múltiplas para diff, controle de qualidade para comparar duas imagens), ative o modo determinístico: --deterministic ou variável de ambiente WEBENGINE_DETERMINISTIC=1 (desliga rasterização GPU, 8/8 sessões consistentes em teste, custo ~ +22%/quadro). Desligado por padrão: dentro da mesma sessão, já é consistente; entre sessões, há diferenças de antialiasing de ≤4 pixels, ±1 nível.

4. Lista de verificação de qualidade de imagem (estes itens decidem "se parece profissional")

  • Brilho e profundidade: fundo escuro (#0a1420) + acento claro no topo; não use preto puro ou branco puro, a tela do usuário vai comer o gradiente.
  • Peso e tamanho da fonte: texto grande 84–96px, subtítulo 34–38px, legenda 46px; chinês use msyhbd.ttc (Microsoft YaHei Bold).
  • Legenda: ≤ 2 linhas, cerca de 24 caracteres chineses por linha, com fundo semitransparente, y ≥ 872. A quebra de linha real é baseada na largura em pixels (fit_sub_lines quebra automaticamente por max_w=1560 px), 24 caracteres é "limite superior aproximado" não uma validação rígida — a constante SUB_MAX_CHARS=24 atualmente não é referenciada por nenhum código, não confie que ela vai te barrar.
  • Ritmo: a cada 3–6 segundos deve haver uma mudança visual (novo cartão / deslocamento / rolagem de número); mais de 6 segundos parado, o espectador vai embora.
  • Câmera: apenas push lento de 3–15%, sem grandes zooms para longe ou perto.
  • Cor: um tom principal + um tom de destaque no vídeo inteiro, não mais que 4 matizes.
  • Áudio: master -16 LUFS / pico real ≤ -1 dBTP; narração 12 dB acima do intervalo; deixe 1.4 dB de margem antes de AAC.

5. Som: música + efeitos sonoros + pontos de sinalização

A ideia desta seção vem de uma retrospectiva de um vídeo real (ai-history-film): som ruim não é "mixagem não alta o suficiente", mas "som não conhece a imagem". A ordem de solução é sempre: primeiro faça o som saber o que está acontecendo na imagem (pontos de sinalização), depois fale de timbre, por último o master.

5.1 Três camadas de som

CamadaQuem fazSalva emDescrição
Narraçãofilmkit.tts (edge-tts)work/tts/*.mp3Única autoridade de duração: linha do tempo deduzida por ela
Músicamusicgen.py (orquestração parametrizada)work/music.wavGerada no local conforme bpm/key/scale/mood/seed do design + parâmetros de orquestração; não há estilos prontos para escolher
Efeitos sonorossfxkit.py (modelagem física)work/sfx.wavSintetizados um a um conforme pontos de sinalização registrados em cues.json

As três barras são mixadas em --stage audio: quando o efeito sonoro começa, a música cede automaticamente (duck_db padrão 4 dB, cinema/epic 5.5 dB — com 3 dB de cedência, na transição entre parágrafos, a música quase iguala o efeito, o som fica uma bagunça), finalmente passado para ffmpeg duas passagens loudnorm para -16 LUFS / pico real ≤ -1 dBTP. Não faça a normalização final de loudness no numpy — você tem loudnorm, ele é muito mais preciso que normalização manual, e o pico real é o gate.

Separação dentro do vídeo: a música já tem transições nas fronteiras dos parágrafos (swell / riser / sub_boom escritos pelo orquestrador), a trilha sfx não repete whoosh/riser no mesmo limite — --stage sfx automaticamente reduz -3 dB em cues de transição que caem dentro de [limite - 2.2 s, limite] (sfx.duck_transitions=3.0 ajustável, padrão 3).

5.2 Pontos de sinalização: o que o som precisa saber

// work/cues.json — gerado por `--stage cues`, consumido por `--stage sfx` e `--stage score`
{
  "cues": [
    {"time_s": 0.0,  "type": "transition_in",  "pitch": "E3", "intensity": 0.4},
    {"time_s": 12.5, "type": "emphasis",        "pitch": "A4", "intensity": 0.8},
    {"time_s": 25.0, "type": "transition_out",  "pitch": "E3", "intensity": 0.6},
    {"time_s": 30.0, "type": "finish",          "pitch": "E2", "intensity": 1.0}
  ]
}

soundkit integrado (gerar pontos de sinalização + efeitos sonoros):

from filmkit import K
import numpy as np

# Registrar ponto de sinalização
K.cue(time=12.5, type="emphasis", pitch="A4", intensity=0.8)

# Gerar forma de onda de efeito sonoro (síntese)
wave = K.sfx.whoosh(duration=0.6, pitch_start=400, pitch_end=200, stereo=True)
# wave é um array numpy float32, (amostras, 2)

whoosh é um gerador de efeito sonoro integrado, não um arquivo de som pré-gravado.

5.3 Mixagem: por que numpy em vez de ffmpeg como bus

ffmpeg com múltiplas entradas para mixagem falha no Windows (veja §0, regra 5), e mesmo no Linux, com > 30 entradas, ffmpeg -filter_complex amix pode falhar silenciosamente — o pico real ultrapassa 0 dBFS sem aviso, ou um dos canais aparece mais baixo.

A solução: --stage audio usa numpy para sobreposição ponto a ponto, normaliza para -1 dBTP (reserva de 1.4 dB), depois passa para ffmpeg apenas para loudnorm.

# Lógica central da mixagem (simplificada)
buses = [narration, music, sfx]  # lista de numpy arrays
master = np.sum(buses, axis=0)    # sobreposição ponto a ponto
master /= np.max(np.abs(master))  # normalizar para pico real = 1.0
master *= 10**(-1.4 / 20)         # reserva de 1.4 dB → pico real ≈ -1.4 dB

5.4 Normas de loudness

MétricaValor alvoNotas
LUFS integrado (curto)-16 LUFSNão use -23 (é para TV, parece baixo)
Pico real≤ -1 dBTPffmpeg loudnorm com parâmetro tp=-1.0
Margem antes de AAC1.4 dBO codec AAC tem pouca margem acima de -1 dBFS
Relação narração/intervalo≥ 12 dBSe a narração estiver apenas 6 dB acima do intervalo, parece "sussurro"

Ouça com fones de ouvido antes de entregar: o QC só detecta "pico real ultrapassado" e "LUFS baixo demais", não "som parece preso em uma caixa" ou "grave sumiu".

6. Controle de qualidade: gate automático + inspeção humana

6.1 Níveis de gate

NívelExecutado porFalha quando
G1 — Duração--stage qaA duração medida da narração desvia da linha do tempo > 200 ms
G2 — Proporção de áudio--stage qaLUFS fora de [-20, -12] ou pico real > -0.5 dBTP
G3 — Gráfico de espectro--stage qaFrequência de corte de ruído de fundo ou chiado > -30 dB (verificação de densidade espectral)
G4 — Nitidez de texto--stage qaProporção de pontuação de nitidez de texto < 4.0 (comparação contra referência de texto borrado)
G5 — Quadro congelado--stage qa> 50% dos quadros consecutivos têm diferença < 0.5% (SSIM)
G6 — Semelhança de quadros--stage stills + humanaO humano vê o quadro estático e diz "não gosto"
G7 — Lacuna de bloco--stage muxBlocos faltando em work/segments/
G8 — Assinatura de entrega--stage acceptAssinatura antiga inválida (conteúdo mudou)

6.2 Relatório de QC

cat out/QC-REPORT.md
# Matriz de gate:
# [PASS] G1 — Duração (desvio de 14 ms)
# [FAIL] G2 — Proporção de áudio (LUFS = -11.2)
# [SKIP] G3 — Gráfico de espectro (não configurado)

Gates opcionais (pode desligar em film.json.qa): vmaf / contagem de cenas / detecção de quadro congelado.

6.3 Por que inspeção humana

O QC automático só pode detectar problemas numéricos, não "fonte feia" ou "essa composição parece estranha". Por isso, a regra 6 de ferro exige "veja pessoalmente 3 quadros estáticos" — é a única maneira de detectar que a imagem finalizada tem um problema que nenhum número capturou.

7. ffmpeg: guia rápido de parâmetros

Os parâmetros padrão estão em filmkit.mux. Se precisar sobrescrever, escreva ffmpeg em film.json:

{"ffmpeg": {"video": {"codec": "libx264", "crf": 18, "preset": "slow"}, "audio": {"codec": "aac", "b": "192k"}}}

Sempre use duas passagens loudnorm (loudnorm no filtro de áudio, não compressão dinâmica manual).

ParâmetroPadrãoQuando mudar
crf1817 para texto mais nítido (+30% de volume), 20 para vídeos comuns com imagem em movimento
presetslowveryslow para arquivo final, ultrafast para teste de depuração (qualidade cai, não use para entrega)
profilehighhigh422 ou high444 para vídeos de nível de cinema (se o codec suportar)
pix_fmtyuv420pyuv444p para textura de cor pura (requer codec compatível, volume x1.5)
Áudio b192kNarração com alto detalhe pode ir para 256k, música pode ir para 320k

8. Debug: verificação de linha do tempo, inspeção de quadro estático

8.1 Verificar se a linha do tempo corresponde à narração

from filmkit import K
timeline = K.load_timeline("work/timeline.json")
# Verificar se a duração de cada seção corresponde à duração real do TTS
for seg in timeline["segments"]:
    tts_file = f"work/tts/{seg['id']}.mp3"
    tts_dur = K.audio_duration(tts_file)
    if abs(seg["dur"] - tts_dur) > 0.2:  # 200 ms de tolerância
        print(f"Aviso: seção {seg['id']} linha do tempo {seg['dur']:.2f}s ≠ TTS {tts_dur:.2f}s")

8.2 Inspeção de quadro estático

from filmkit import K
frame = K.read_image("work/stills/l01_3.png")
# Inspecionar posição do texto
import numpy as np
# Verificar se o texto da legenda está na zona segura
if np.any(frame[872:, :, :] == 0):  # pixels pretos na zona da legenda
    print("Aviso: texto na zona da legenda pode estar fora")

9. Perguntas frequentes

9.1 "O que é work/?"

Diretório de artefatos intermediários, incluindo:

  • work/tts/: áudio TTS
  • work/timeline.json: linha do tempo
  • work/design.json: design confirmado
  • work/music.wav: música
  • work/sfx.wav: efeitos sonoros
  • work/audio.wav: áudio mixado
  • work/segments/: blocos de quadros
  • work/qa/: relatório de QC
  • work/signoff.json: aprovação de gate
  • work/cues.json: pontos de sinalização
  • work/stills/: quadros estáticos

Não faça commit de work/ no git (inclua no .gitignore). Se o pipeline interromper no meio, você pode deletar work/ e começar de novo.

9.2 "Qual é a diferença entre este e outros geradores de vídeo?"

CaracterísticaEsteOutros geradores
Linha do tempo deduzida por áudioSimMaioria estima por contagem de caracteres
Renderização de quadro puro determinísticaSimMaioria permite aleatoriedade
Mixagem em nível de amostraSimMaioria usa ffmpeg como bus
Gate automático explicitamente configurávelSimMaioria não tem
Design não como modelo, mas como parâmetrosSimMaioria tem modelos internos fixos
Proposta primeiro, vídeo depoisSimMaioria precisa de instruções humanas

9.3 "Posso integrar com outras ferramentas?"

Sim, você pode escrever scripts para chamar as etapas do pipeline:

# Combinar com ChatGPT para gerar roteiro
python generate_script.py | python <SKILL_DIR>/scripts/filmcheck.py -

# Configurar CI/CD
if python <SKILL_DIR>/scripts/render.py --script film.json --stage qa --strict-gates; then
    echo "QC passou, pronto para entrega"
else
    echo "QC falhou, verifique work/qa/"
fi

10. Ferramentas complementares

FerramentaDescriçãoComo usar
propose.pyGera opções de design, confirma/revêVeja §0.5
selfcheck.pyVerifica dependências e ambiente de execuçãoExecute antes de usar pela primeira vez
filmcheck.pyValida o formato de film.jsonExecute após escrever o roteiro
estimate.pyEstima custo de renderizaçãoUse antes de renderização longa
webengine.pyMotor de renderização de navegadorUse para visual language complexo
musicgen.pyGeração de música parametrizadaChamado por --stage score
sfxkit.pyGeração de efeitos sonorosChamado por --stage sfx

11. Esquemas de referência

EsquemaCaminhoDescrição
design.schema.jsonschemas/design.schema.jsonEsquema de design
film.schema.jsonschemas/film.schema.jsonEsquema de roteiro
proposal.mdreferences/proposal.mdExplicação detalhada do contrato de proposta
scenes.pyassets/scenes.pyRenderizador interno (exemplo)

12. Métricas de desempenho

12.1 Velocidade de renderização

Motor1080p30 (quadros/segundo)Depende de
PIL (numpy+draw simples)60–80Uso de CPU, principalmente Pillow e numpy
PIL (composição pesada de texto+imagem)30–50Uso de CPU, cache de fonte, cache de imagem
Navegador (Canvas2D)8–12CPU/GPU, captura de tela PNG, codificação ffmpeg
Navegador (WebGL)5–8Desempenho da GPU, complexidade do shader

12.2 Espaço em disco

Tipo de renderizaçãoEspaço aproximadoNotas
Apenas quadro estático~10 MBArquivos PNG, 3 quadros
Apenas áudio~5 MBNarração + música + efeitos, wav
Apenas bloco de quadro~50 MB/blocoMP4 de bloco, 8 segundos
Vídeo completo (60s, CRF18)~80 MBMP4 final
Vídeo completo (60s, CRF17)~100 MBTexto mais nítido, volume maior
Todos os artefatos~200 MBIncluindo tudo acima

O espaço real varia com o conteúdo do vídeo. Os dados acima são para referência.

13. Histórico de versões

VersãoDataPrincipais mudanças
1.0.02024-01-01Lançamento inicial
1.1.02024-03-15Adicionado mecanismo de proposta, removido estilo interno
1.2.02024-06-01Adicionado motor web, registro de deduplicação
1.3.02024-09-01Adicionado separação de áudio em três camadas, sistema de gate de QC

Aviso: Esta skill e seu conteúdo são disponibilizados "como estão", sem garantia de qualquer tipo, seja expressa ou implícita. O usuário é responsável por cumprir todas as leis e regulamentos aplicáveis. O autor não se responsabiliza por quaisquer danos ou consequências decorrentes do uso desta skill.

Habilidades Relacionadas