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)
- Á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.
- Cada quadro é uma função pura:
draw(t, frame) -> Image. Proibido usarrandom/time(), usefilmkit.hash2/hash3/vnoisepara aleatoriedade. O mesmo número de quadro deve sempre produzir a mesma imagem. - Legendas e textos grandes devem estar na zona segura: corpo
y ∈ [58, 974], legenda inferiory ∈ [872, 1030], nunca ultrapassar 1030. - Quadros originais direto para codificação: quadro PIL → pipe
rawvideo→libx264. Não usecv2.VideoWritercomo arquivo intermediário (2x mais lento e com perda secundária). - 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).
- 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". - O som segue a imagem: registre pontos de sinalização no código da cena (
K.CueBus/ hookcues()emscenes.py), não ajuste manualmente depois. Se a imagem mudar (trocar janela de permanência, ajustar ritmo), os pontos de sinalização mudam automaticamente. - Se não entender, adicione pausa; não desacelere o vídeo inteiro:
hold(global) emfilm.jsonoulines[].hold(por frase) adiciona pausa após a fala; usebpmpara 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ário | O que você deve fazer |
|---|---|
| "É esse" / escolheu um | python 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:
- Antes de
propose.py --confirm, o--stage frames/allderender.pyserá bloqueado pelo gate de proposta (aviso padrão,--require-proposalfalha). - 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 < 2será sinalizado. - 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". - Não há modelos internos para reutilizar: todos os campos de
design.jsonestã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). - Registro de deduplicação entre vídeos (ativado por padrão):
--confirmregistrará 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-ledgerverifica o registro,--no-ledgerignora nesta execução, a variável de ambienteVIDEO_STUDIO_LEDGERmuda o caminho ou defineoffpara 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 doswork/eout/gerados pela execução).
<SKILL_DIR>se refere aoresourceBase.pathretornado pela ferramentaskillao carregar esta skill. Quando instalado como plugin, é oskills/video-studio/dentro deste pacote de plugin (node_modules/@dsh-gis/dsh-video-studio-skillno 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, ounode_modules/@dsh-gis/dsh-video-studio-skill/no perfil após instalação), ou seja, dois níveis acima de<SKILL_DIR>.tools/eschemas/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.pypersonalizado, primeiro copie um renderizador (_draw_heroetc.) 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/, nomeadosseg_<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 comoquadros_totais / jobs / 4arredondado para segundos inteiros (jobs=4, 900 quadros → cerca de 8 segundos por bloco), pode ser sobrescrito comchunk_framesemfilm.json.Após alterar falas / alterar scenes.py / alterar design / alterar parâmetros de codificação,
--resumerecusará reutilizar blocos antigos (a impressão digital do plano é armazenada emwork/segments/_plan.json), e sugererm -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)
| Etapa | Saída | Ação chave | Erro comum |
|---|---|---|---|
| P0.5 Definir design | work/design.json + work/style.json | propose.py gera opções → --confirm congela design; --stage style apenas analisa design confirmado e imprime tema/linguagem visual/rotação de layout/legenda | Pular → render.py erra (sem design não há aparência); renderizar sem confirmar gera aviso de proposta |
| P0 Definir base | CONVENTIONS.md | Resolução/framerate/duração/BPM→quadros calculados uma vez, referenciados por todo o projeto | Cada arquivo escreve números mágicos como 150 → bordas de cena desalinhadas |
| P1 Escrever roteiro | film.json | Cada frase separa text (narração) e sub (legenda); cada seção tem um kind; opcional lines[].type bloqueia composição | Um 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 fatos | FACTS.md | Cada 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 TTS | work/tts/*.mp3 + tts.json | edge-tts sintetiza frase por frase, ffmpeg -f f32le mede duração real, cache em disco | Estimar duração por contagem de caracteres → legenda desvia |
| P4 Alinhar linha do tempo | work/timeline.json + sub.srt | dur = lead + fala medida + tail, a fala deve estar dentro desta seção | Seção muito curta → narração cortada pela próxima seção |
| P5 Mixagem | work/audio.wav | Barramento numpy: narração + música + efeitos sobrepostos por ponto de amostra | Múltiplas entradas ffmpeg para mixagem → silêncio não determinístico |
| P6 Mestre | 3 quadros estáticos | Renderize 3 quadros primeiro para ver composição/cor/peso da fonte, depois renderize o vídeo inteiro | Renderizar o vídeo inteiro diretamente → 36000 quadros depois descobre que a fonte é feia |
| P7 Vídeo final | out/film.mp4 | rawvideo→libx264 CRF18 + duas passagens loudnorm para -16 LUFS | Bitrate 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 escrevacrfpara usar o padrão 18; para texto mais nítido, pode usar 17 (volume ~ +30%).
3. Como escolher o motor de renderização
| Cenário | Escolha | Velocidade real | Adequado para |
|---|---|---|---|
| Texto/imagem/dados/ ciencia/estilo UI | PIL (numpy+cv2 auxiliar) — padrão filmkit | 40–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 suave | Navegador 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 pura | manim (se instalado) | Depende da cena | Matemá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 emwork/design.jsoné lida porassets/scenes.py.--engine webexige 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 seguemdesign.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ências | numpy + Pillow (+cv2 para comparação de nitidez) | + playwright + Chromium/Edge |
| Velocidade | 40–80 quadros/segundo | 8–12 quadros/segundo (captura de tela quadro a quadro + codificação, uma ordem de magnitude mais lento) |
| Linguagem de imagem | Composição bitmap, gradiente, geometria, texto | Canvas2D/WebGL: partícula, deformação, shader, animação vetorial |
| Determinismo | Natural (função pura para desenho) | Garantido por "posicionar precisamente a linha do tempo em f/FPS" (window.renderFrame) |
| Depuração | Mude um quadro, execute um quadro | No navegador, index.html?dev=1&t=segundos veja diretamente, bugs fáceis de encontrar |
| Adequado para | Texto/imagem/dados/ ciencia/vídeo promocional | Ví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):
- Todos os tweens devem estar em uma linha do tempo principal pausada, renderizar o quadro f = posicioná-la em
f / FPSsegundos; não pode depender de reprodução. - Conteúdo programático como loops/partículas/ondas deve usar um atualizador que calcula diretamente com o tempo (
update(fn)), não usesetIntervalpara acumular. - 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_linesquebra automaticamente pormax_w=1560 px), 24 caracteres é "limite superior aproximado" não uma validação rígida — a constanteSUB_MAX_CHARS=24atualmente 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
| Camada | Quem faz | Salva em | Descrição |
|---|---|---|---|
| Narração | filmkit.tts (edge-tts) | work/tts/*.mp3 | Única autoridade de duração: linha do tempo deduzida por ela |
| Música | musicgen.py (orquestração parametrizada) | work/music.wav | Gerada no local conforme bpm/key/scale/mood/seed do design + parâmetros de orquestração; não há estilos prontos para escolher |
| Efeitos sonoros | sfxkit.py (modelagem física) | work/sfx.wav | Sintetizados 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 sfxautomaticamente reduz -3 dB em cues de transição que caem dentro de [limite - 2.2 s, limite] (sfx.duck_transitions=3.0ajustá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étrica | Valor alvo | Notas |
|---|---|---|
| LUFS integrado (curto) | -16 LUFS | Não use -23 (é para TV, parece baixo) |
| Pico real | ≤ -1 dBTP | ffmpeg loudnorm com parâmetro tp=-1.0 |
| Margem antes de AAC | 1.4 dB | O codec AAC tem pouca margem acima de -1 dBFS |
| Relação narração/intervalo | ≥ 12 dB | Se 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ível | Executado por | Falha quando |
|---|---|---|
| G1 — Duração | --stage qa | A duração medida da narração desvia da linha do tempo > 200 ms |
| G2 — Proporção de áudio | --stage qa | LUFS fora de [-20, -12] ou pico real > -0.5 dBTP |
| G3 — Gráfico de espectro | --stage qa | Frequência de corte de ruído de fundo ou chiado > -30 dB (verificação de densidade espectral) |
| G4 — Nitidez de texto | --stage qa | Proporçã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 + humana | O humano vê o quadro estático e diz "não gosto" |
| G7 — Lacuna de bloco | --stage mux | Blocos faltando em work/segments/ |
| G8 — Assinatura de entrega | --stage accept | Assinatura 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, escrevaffmpegemfilm.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âmetro | Padrão | Quando mudar |
|---|---|---|
crf | 18 | 17 para texto mais nítido (+30% de volume), 20 para vídeos comuns com imagem em movimento |
preset | slow | veryslow para arquivo final, ultrafast para teste de depuração (qualidade cai, não use para entrega) |
profile | high | high422 ou high444 para vídeos de nível de cinema (se o codec suportar) |
pix_fmt | yuv420p | yuv444p para textura de cor pura (requer codec compatível, volume x1.5) |
Áudio b | 192k | Narraçã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 TTSwork/timeline.json: linha do tempowork/design.json: design confirmadowork/music.wav: músicawork/sfx.wav: efeitos sonoroswork/audio.wav: áudio mixadowork/segments/: blocos de quadroswork/qa/: relatório de QCwork/signoff.json: aprovação de gatework/cues.json: pontos de sinalizaçãowork/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ística | Este | Outros geradores |
|---|---|---|
| Linha do tempo deduzida por áudio | Sim | Maioria estima por contagem de caracteres |
| Renderização de quadro puro determinística | Sim | Maioria permite aleatoriedade |
| Mixagem em nível de amostra | Sim | Maioria usa ffmpeg como bus |
| Gate automático explicitamente configurável | Sim | Maioria não tem |
| Design não como modelo, mas como parâmetros | Sim | Maioria tem modelos internos fixos |
| Proposta primeiro, vídeo depois | Sim | Maioria 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
| Ferramenta | Descrição | Como usar |
|---|---|---|
propose.py | Gera opções de design, confirma/revê | Veja §0.5 |
selfcheck.py | Verifica dependências e ambiente de execução | Execute antes de usar pela primeira vez |
filmcheck.py | Valida o formato de film.json | Execute após escrever o roteiro |
estimate.py | Estima custo de renderização | Use antes de renderização longa |
webengine.py | Motor de renderização de navegador | Use para visual language complexo |
musicgen.py | Geração de música parametrizada | Chamado por --stage score |
sfxkit.py | Geração de efeitos sonoros | Chamado por --stage sfx |
11. Esquemas de referência
| Esquema | Caminho | Descrição |
|---|---|---|
design.schema.json | schemas/design.schema.json | Esquema de design |
film.schema.json | schemas/film.schema.json | Esquema de roteiro |
proposal.md | references/proposal.md | Explicação detalhada do contrato de proposta |
scenes.py | assets/scenes.py | Renderizador interno (exemplo) |
12. Métricas de desempenho
12.1 Velocidade de renderização
| Motor | 1080p30 (quadros/segundo) | Depende de |
|---|---|---|
| PIL (numpy+draw simples) | 60–80 | Uso de CPU, principalmente Pillow e numpy |
| PIL (composição pesada de texto+imagem) | 30–50 | Uso de CPU, cache de fonte, cache de imagem |
| Navegador (Canvas2D) | 8–12 | CPU/GPU, captura de tela PNG, codificação ffmpeg |
| Navegador (WebGL) | 5–8 | Desempenho da GPU, complexidade do shader |
12.2 Espaço em disco
| Tipo de renderização | Espaço aproximado | Notas |
|---|---|---|
| Apenas quadro estático | ~10 MB | Arquivos PNG, 3 quadros |
| Apenas áudio | ~5 MB | Narração + música + efeitos, wav |
| Apenas bloco de quadro | ~50 MB/bloco | MP4 de bloco, 8 segundos |
| Vídeo completo (60s, CRF18) | ~80 MB | MP4 final |
| Vídeo completo (60s, CRF17) | ~100 MB | Texto mais nítido, volume maior |
| Todos os artefatos | ~200 MB | Incluindo 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ão | Data | Principais mudanças |
|---|---|---|
| 1.0.0 | 2024-01-01 | Lançamento inicial |
| 1.1.0 | 2024-03-15 | Adicionado mecanismo de proposta, removido estilo interno |
| 1.2.0 | 2024-06-01 | Adicionado motor web, registro de deduplicação |
| 1.3.0 | 2024-09-01 | Adicionado 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.