primocode 10.15.2 → 10.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +74 -9
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +38 -8
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +7 -1
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +305 -0
- package/lib/estudio/index.js +12 -1
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +70 -12
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +64 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +285 -7
- package/lib/local/motor.js +5 -2
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +33 -2
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +23 -4
- package/lib/ui.js +2 -2
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/__main__.py +55 -40
- package/voz/primo_voz/app.py +500 -4
- package/voz/primo_voz/chat.html +1773 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/painel_dados.py +134 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Cenas sob medida
|
|
2
|
+
|
|
3
|
+
Quando nenhuma peça do catálogo serve, a cena é escrita à mão, como nos vídeos que ficaram melhores: um
|
|
4
|
+
arquivo por cena nesta pasta (`compositions/cenas/s03_orcamento.html`), cada um uma sub-composição do
|
|
5
|
+
HyperFrames. A pasta fica dentro de `compositions/` porque o `python3 build.py hf lint` só confere o que está
|
|
6
|
+
ali (uma cena em outra pasta passaria sem lint). O `build.py` encaixa a cena no tempo certo do corte, numa faixa
|
|
7
|
+
própria da linha do tempo do estúdio, tanto na `amostra` quanto no `completo`. Os arquivos soltos em
|
|
8
|
+
`compositions/` (fora de `cenas/`) são as peças do catálogo que o build gera e apaga a cada build: não escreva lá.
|
|
9
|
+
|
|
10
|
+
## No projeto.json
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
"cenas_proprias": [
|
|
14
|
+
{"arquivo": "compositions/cenas/s03_orcamento.html", "de": 12.3, "ate": "cortou", "nome": "Cena · orçamento",
|
|
15
|
+
"faixa": 31, "esconde_legenda": true}
|
|
16
|
+
]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| campo | o que é |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `arquivo` | o arquivo da cena, a partir da pasta do projeto |
|
|
22
|
+
| `de`, `ate` | segundos do `cut.mp4` ou a palavra (a cena começa ou termina quando a palavra começa); `de_depois_de` e `ate_depois_de` escolhem qual repetição da palavra |
|
|
23
|
+
| `nome` | nome da faixa no estúdio (padrão: "Cena · s03 orcamento", tirado do arquivo) |
|
|
24
|
+
| `faixa` | número da faixa no estúdio (padrão 30, 31, 32...; só organiza a tela, o render não lê) |
|
|
25
|
+
| `esconde_legenda` | `true` quando a cena já escreve a fala (a legenda some nesse trecho) |
|
|
26
|
+
| `sobre_a_pessoa` | `true` quando a cena é um cartão pequeno acima da cabeça, com a pessoa em tela cheia (`area: "acima_do_rosto"` nas cenas `k2d_medidor`, `k2d_comando`, `k2d_busca` e `k2d_notas`): a câmera viva fica em zoom 1,0 e não aproxima nesse trecho. O build avisa se um `tela_cheia` do trecho ainda aproxima |
|
|
27
|
+
|
|
28
|
+
Na `amostra`, só entram as cenas que cruzam o trecho. Se a amostra começa no meio de uma cena, o build põe
|
|
29
|
+
`data-playback-start` e a cena continua do ponto certo (não recomeça).
|
|
30
|
+
|
|
31
|
+
## O que a cena recebe
|
|
32
|
+
|
|
33
|
+
O `index.html` traz, antes de tudo, `window.__cenas["<id da cena>"]`:
|
|
34
|
+
|
|
35
|
+
| chave | o que é |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `de`, `ate`, `duracao` | onde a cena fica no corte, em segundos. Com `segura_fim`, a cena cujo `ate` cai no fim do corte (até 0,25 s antes) vai até o fim do quadro parado, e o `ate` e a `duracao` já vêm com isso |
|
|
38
|
+
| `palavras` | a fala do trecho: `[{"texto": "cortou", "t": 3.68, "fim": 4.04}]`, com `t` e `fim` em segundos **da cena** (0 = o `de`). O `t` é o começo medido na onda quando a pasta `edit` tem o `palavras_onda.json` (`helpers/palavras_na_onda.py`); sem ele, o tempo do Whisper, que adianta até 0,3 s |
|
|
39
|
+
| `tempo` | de onde vem o `t` das palavras: `"onda"` ou `"whisper"`. Com `"whisper"`, uma batida que precisa cair na palavra pode chegar até 0,3 s cedo: rode o helper e o build de novo |
|
|
40
|
+
| `rosto` | a caixa do rosto na tela no começo da cena, em pixels: `{x0, y0, x1, y1, topo}` (`topo` = topo da cabeça estimado); `null` sem rosto |
|
|
41
|
+
| `moldura` | quando a cena cruza uma moldura: `{caixa, area_livre, fechada_em}` (`fechada_em` em segundos da cena: antes disso o quadro ainda está fechando e o que aparecer passa na frente do rosto); `null` sem moldura. Cruzar é dividir pelo menos um quadro do render, com os dois trechos meio-abertos, [de, ate): a cena que começa no quadro em que a moldura acaba não recebe `moldura` (senão fica transparente em cima do vídeo em tela cheia), nem quando o tempo foi escrito com 3 casas (`4.933` contra o corte de verdade, 4,93333) ou quando o bloco encostou no corte e a cena ficou no tempo que você escreveu |
|
|
42
|
+
| `seguro` | a zona segura do formato `[x0, y0, x1, y1]` (9:16: 140, 230, 940, 1530) |
|
|
43
|
+
| `largura`, `altura` | 1080 x 1920, 1920 x 1080, 1080 x 1350 (4:5) ou 1080 x 1080 (1:1) |
|
|
44
|
+
| `cores` | as cores do `estilo` do projeto: `papel`, `linha`, `base`, `destaque`, `tinta`, `verde`, `vermelho`. Em estilo escuro (`laranja`) a `tinta` é clara e o `papel` é escuro: letra sobre uma cor clara (o laranja) leva o `papel`, e não a `tinta` (as cenas do kit já escolhem pela luz do fundo) |
|
|
45
|
+
| `fundo_css` | o fundo do estilo em CSS (grade e brilho), só nos estilos escuros com fundo próprio (`laranja`); `null` em `papelaria` e `noite`. A cena que pinta o próprio fundo em tela cheia usa ele quando existe (as cenas do kit 2D já usam), para a grade e o brilho não mudarem de um trecho para o outro |
|
|
46
|
+
|
|
47
|
+
Aberta sozinha no painel Compositions do estúdio, a cena não tem esses dados: use sempre um valor padrão (o
|
|
48
|
+
exemplo abaixo faz isso).
|
|
49
|
+
|
|
50
|
+
A lista `palavras` vem da transcrição do corte e pode mudar quando o corte é conferido de novo (o Whisper junta
|
|
51
|
+
ou separa palavras, "o tudo" vira "o" e "tudo", "ééé" vira "éééé"). Nunca conte com um número fixo de palavras:
|
|
52
|
+
procure pela palavra (a função `em` do exemplo) e deixe o desenho aguentar uma a mais (linha que quebra sozinha,
|
|
53
|
+
`flex-wrap`), senão a cena estoura o painel e o `check` reprova.
|
|
54
|
+
|
|
55
|
+
## O contrato (o que não pode faltar)
|
|
56
|
+
|
|
57
|
+
1. Tudo dentro de `<template id="<id>-template">`: o `<style>`, a raiz e o `<script>`. O que fica fora do
|
|
58
|
+
template é jogado fora.
|
|
59
|
+
2. A raiz é `<div id="root" data-composition-id="<id>" data-width="1080" data-height="1920" data-duration="...">`
|
|
60
|
+
no tamanho do vídeo do projeto (1920x1080 no 16:9, 1080x1350 no 4:5, 1080x1080 no 1:1; veja "O tamanho da
|
|
61
|
+
raiz") e o estilo dela vai em `#root {...}` (nunca numa classe da raiz). O `<id>` é o nome do arquivo sem
|
|
62
|
+
`.html` (`s03_orcamento`); `main` e `peca-...` são do build.
|
|
63
|
+
3. Uma timeline só, pausada, registrada com o mesmo id escrito por extenso:
|
|
64
|
+
`window.__timelines["s03_orcamento"] = tl;` dentro de uma função `(function () { ... })();`.
|
|
65
|
+
4. Tempo da cena: 0 é o `de`. Nunca tempo negativo.
|
|
66
|
+
5. Determinística e sem depender do seek: `fromTo` com os dois lados, `immediateRender: false` a partir do
|
|
67
|
+
segundo tween do mesmo elemento, nada de `Math.random`, `Date.now`, `repeat: -1` ou timer. Valor que muda a
|
|
68
|
+
cada quadro vai como lista de dados + laço de `tl.set`.
|
|
69
|
+
6. Todo elemento nasce com `opacity: 0` no CSS e entra por tween. Anime `opacity`, `x`, `y`, `rotation` e
|
|
70
|
+
`clip-path`; nunca `left/top/width/height`, e nada de `transform` no CSS de quem o GSAP move. Nada de escala
|
|
71
|
+
(`scale`, `scaleX`): depois de um tween de escala o Chrome guarda o desenho numa escala que depende do que a
|
|
72
|
+
página já tocou e da máquina, e dois renders do mesmo projeto saem diferentes. Para "crescer", use `clip-path`
|
|
73
|
+
(`inset(...)` ou `circle(...)`) ou um deslocamento maior. Imagem do tamanho em que aparece: maior que a
|
|
74
|
+
caixa, reduzida pelo navegador, também muda a borda de um render para outro.
|
|
75
|
+
7. Texto que muda nunca nasce vazio (escreva o primeiro valor no HTML); número que começa com zero leva o
|
|
76
|
+
espaço de largura zero na frente (`"01"`).
|
|
77
|
+
8. Ids com o prefixo da cena (`#s03-card`), para não bater com as peças do build.
|
|
78
|
+
9. Caminho de arquivo sempre a partir da pasta do projeto (`assets/fontes/Montserrat.ttf`,
|
|
79
|
+
`assets/print.png`), nunca `../`. Fonte com `@font-face` dentro do `<style>` da cena.
|
|
80
|
+
10. `<video>` dentro da cena: `muted playsinline`, com `id`, `data-start` em tempo da cena, sem `crossorigin`.
|
|
81
|
+
Som vai num `<audio id>` à parte.
|
|
82
|
+
11. Espaço entre palavras em elementos separados vai como margem em px, nunca como texto: o HyperFrames
|
|
83
|
+
descarta o texto que é só espaço entre dois elementos (`<span>Não</span> <span>fui</span>` sai "NÃOFUI").
|
|
84
|
+
Use `margin-left: 14px` (em px: `.3em` num span sem `font-size` próprio mede pelos 16 px da raiz e some).
|
|
85
|
+
12. Risco em cima de texto reprova o `check` (`text_occluded`), mesmo com `data-layout-allow-occlusion`. Ponha o
|
|
86
|
+
risco por baixo das letras: filho do item, com o item em `position:relative; z-index:0` e o risco em
|
|
87
|
+
`z-index:-1`. Continua parecendo riscado e o check passa.
|
|
88
|
+
13. Troca de estado com os dois textos no mesmo lugar ao mesmo tempo reprova (`content_overlap`): o texto velho
|
|
89
|
+
termina de sair antes do novo entrar (ou um sai por um lado e o outro entra pelo outro, sem cruzar).
|
|
90
|
+
14. Na moldura, o primeiro elemento entra depois do quadro fechar (`C.moldura.fechada_em`, uns 0,7 s depois
|
|
91
|
+
da troca). O build avisa quando a cena começa antes disso. A cena que começa quando a moldura acaba não é da
|
|
92
|
+
moldura (`moldura: null`): desenhe com fundo próprio, porque o vídeo está em tela cheia.
|
|
93
|
+
|
|
94
|
+
## O tamanho da raiz
|
|
95
|
+
|
|
96
|
+
O HyperFrames monta a cena com o tamanho que a raiz declara, não com o do vídeo: uma raiz 1080x1920 num projeto
|
|
97
|
+
16:9, 4:5 ou 1:1 sai cortada. O build compara a raiz de cada cena com o vídeo:
|
|
98
|
+
|
|
99
|
+
- a cena se desenha pelo `largura` e `altura` que recebe (lê os dois do `window.__cenas`, como as cenas do kit 2D
|
|
100
|
+
e 3D): o build põe a raiz no tamanho do vídeo no `index.html` (uma regra com `!important` no host e no filho
|
|
101
|
+
dele, que vence o tamanho que o HyperFrames escreve, no estúdio e no render) e diz numa linha só quais cenas
|
|
102
|
+
ajustou. O arquivo da cena não muda; aberta sozinha no painel Compositions, ela fica no tamanho que declara.
|
|
103
|
+
- a cena tem posições fixas (não lê `largura` nem `altura`): o build não mexe e avisa `a raiz declara 1080x1920 e o
|
|
104
|
+
vídeo é 1920x1080: a cena sai cortada`. Troque `data-width` e `data-height` da raiz e refaça as posições para o
|
|
105
|
+
formato (melhor: calcule pelo `largura`, `altura` e `seguro` que a cena recebe).
|
|
106
|
+
|
|
107
|
+
## Exemplo mínimo (passa no `python3 build.py hf lint` e no `hf check`)
|
|
108
|
+
|
|
109
|
+
`compositions/cenas/s01_titulo.html`, com `{"arquivo": "compositions/cenas/s01_titulo.html", "de": 0.7, "ate": 5.5}` no
|
|
110
|
+
projeto:
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<!doctype html>
|
|
114
|
+
<html lang="pt-BR"><head><meta charset="UTF-8"/><title>s01 · título</title></head>
|
|
115
|
+
<body>
|
|
116
|
+
<template id="s01_titulo-template">
|
|
117
|
+
<style>
|
|
118
|
+
@font-face{font-family:Montserrat;src:url("assets/fontes/Montserrat.ttf") format("truetype");font-weight:100 900;font-display:block}
|
|
119
|
+
#root{position:absolute;inset:0;font-family:Montserrat}
|
|
120
|
+
#s01-card{position:absolute;left:190px;top:300px;width:700px;height:200px;border-radius:32px;background:#fffdfd;
|
|
121
|
+
display:flex;align-items:center;justify-content:center;opacity:0;box-shadow:0 12px 0 -2px #c7b7ef,0 22px 44px rgba(53,44,64,.35)}
|
|
122
|
+
#s01-txt{position:relative;z-index:1;font-weight:900;font-size:64px;color:#352c40;white-space:nowrap}
|
|
123
|
+
#s01-risco{position:absolute;left:60px;right:60px;top:50%;height:10px;margin-top:-5px;border-radius:5px;background:#e5484d;opacity:0}
|
|
124
|
+
</style>
|
|
125
|
+
<div id="root" data-composition-id="s01_titulo" data-width="1080" data-height="1920" data-duration="4.8">
|
|
126
|
+
<div id="s01-card"><span id="s01-txt">UMA VEZ SÓ</span><i id="s01-risco"></i></div> <!-- risco por baixo das letras -->
|
|
127
|
+
</div>
|
|
128
|
+
<script>
|
|
129
|
+
(function () {
|
|
130
|
+
// palavras do trecho em segundos da cena; aberta sozinha no estúdio a cena usa os valores padrão
|
|
131
|
+
const C = (window.__cenas || {})["s01_titulo"] || { palavras: [], duracao: 4.8 };
|
|
132
|
+
const sem = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
133
|
+
const em = (texto, depois = 0, padrao = depois) => {
|
|
134
|
+
const p = C.palavras.find((w) => w.t >= depois - 1e-3 && sem(w.texto).startsWith(sem(texto)));
|
|
135
|
+
return p ? p.t : padrao;
|
|
136
|
+
};
|
|
137
|
+
const entra = em("vez", 0, 0.8), risca = em("errei", entra, 1.5);
|
|
138
|
+
const tl = gsap.timeline({ paused: true });
|
|
139
|
+
tl.fromTo("#s01-card", { opacity: 0, y: 60 }, { opacity: 1, y: 0, duration: 0.35, ease: "back.out(2)" }, Math.max(0, entra - 0.1));
|
|
140
|
+
// o risco cresce da esquerda com clip-path (escala deixaria dois renders diferentes)
|
|
141
|
+
tl.fromTo("#s01-risco", { opacity: 1, clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.25, ease: "power2.out", immediateRender: false }, risca);
|
|
142
|
+
tl.to("#s01-card", { opacity: 0, y: -20, duration: 0.25, ease: "power3.in", immediateRender: false }, Math.max(risca + 0.8, (C.duracao || 4.8) - 0.3));
|
|
143
|
+
window.__timelines["s01_titulo"] = tl;
|
|
144
|
+
})();
|
|
145
|
+
</script>
|
|
146
|
+
</template>
|
|
147
|
+
</body></html>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Como conferir
|
|
151
|
+
|
|
152
|
+
1. `python3 build.py amostra <de> <ate>` com o trecho da cena (uns 15 s com motion).
|
|
153
|
+
2. `python3 build.py hf lint` e `python3 build.py hf check` sem erro (o `hf` usa a versão do `package.json`).
|
|
154
|
+
3. `python3 build.py estudio`: a cena aparece numa faixa com o nome dela; arraste a agulha pela entrada e pela
|
|
155
|
+
saída.
|
|
156
|
+
4. Render só no fim (`python3 build.py hf render --quality draft --output renders/amostra.mp4`) e olhe o
|
|
157
|
+
arquivo: ele é a referência, não a prévia.
|
|
158
|
+
5. Anote em `eventos.json` (na pasta do projeto) o quadro em que cada mudança de estado pousa, com o tipo do
|
|
159
|
+
som (`[{"t": 12.4, "tipo": "pop"}]`): o `plano_sfx.py --projeto` junta com os sons das peças.
|
|
160
|
+
|
|
161
|
+
A cena é sua: o build nunca reescreve os arquivos desta pasta. O estúdio, sim: quando abre o projeto, carimba
|
|
162
|
+
um `data-hf-id` em cada elemento das cenas (e o arquivo muda no disco). É normal. Ao editar a cena depois disso,
|
|
163
|
+
procure o trecho sem contar com o texto exato das tags (uma troca de texto exata falha calada e deixa o elemento
|
|
164
|
+
velho): leia o arquivo de novo antes de mudar, ou troque por expressão que ignora o `data-hf-id`.
|
|
165
|
+
|
|
166
|
+
## Outras cenas prontas para adaptar
|
|
167
|
+
|
|
168
|
+
- **Kit 2D**: `compositions/cenas/2d/` com oito cenas `k2d_*.html`: o arquivo de vídeo que muda de estado sem
|
|
169
|
+
sair do lugar, a conversa no celular que vira pedido confirmado, o esquema que cresce com a fala (etapas,
|
|
170
|
+
grupos ou causa e efeito), o antes e depois desenhado, a barra que digita o pedido para a IA, o medidor com
|
|
171
|
+
agulha, a barra de busca com o resultado e a chamada e o bloco de notas com a lista que se conclui. Parâmetros no topo de cada uma; quando usar: `references/animacao.md` da skill, seção Kit 2D.
|
|
172
|
+
- **3D de verdade**: `compositions/cenas/3d/` (o motor `motor-three.js` e quatro cenas `k3d_*.html`, com o
|
|
173
|
+
three.js r181 em `assets/three`). Como usar e o teste de 5 s: `references/3d.md` da skill.
|
|
174
|
+
- **Legenda à mão (papelaria), manchete, legenda que se escreve**: não são estilos da legenda do modelo (que
|
|
175
|
+
tem `padrao` e `discreta`). Escreva como cena própria com `"esconde_legenda": true`, usando as `palavras`.
|
|
176
|
+
- **Moldura com o vídeo desfocado atrás**: não precisa de cena, é `"fundo": "desfocado"` no bloco `moldura`.
|
|
177
|
+
Cena própria só quando o arranjo é outro (vidro fosco nos painéis, dois quadros).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Largura de cada letra das fontes do modelo, medida no próprio arquivo da fonte e no peso que vai para a tela.
|
|
3
|
+
|
|
4
|
+
O build.py usa esta tabela (assets/fontes/larguras.json) para encaixar o lettering antes de montar a página: quebra
|
|
5
|
+
por sentido, dois momentos e, só por último, letra menor. A Montserrat do modelo é variável e, sem escolher o peso,
|
|
6
|
+
é medida no 100 (bem mais fina que a 900 da tela); aqui cada peso é medido de verdade.
|
|
7
|
+
|
|
8
|
+
Rode com o Python da skill (tem o Pillow) quando trocar ou acrescentar uma fonte em assets/fontes:
|
|
9
|
+
uv run --directory '<SKILL>' python '<PROJETO>/ferramentas/medir_fontes.py'
|
|
10
|
+
|
|
11
|
+
Saída: {"fontes": {"Montserrat": {"arquivo": "...", "pesos": {"900": {"A": 780, ...}}}}, "em": 1000}: a largura de
|
|
12
|
+
cada caractere com a letra de 1000 px (sem kerning; a página com kerning fica igual ou um pouco mais estreita).
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
FONTES = os.path.join(os.path.dirname(AQUI), 'assets', 'fontes')
|
|
20
|
+
EM = 1000
|
|
21
|
+
# arquivo, pesos medidos (o que o modelo usa na tela)
|
|
22
|
+
MEDIR = {'Montserrat': ('Montserrat.ttf', (700, 800, 900)), 'Caveat': ('Caveat.ttf', (700,))}
|
|
23
|
+
EXTRAS = '–—‘’“”…•€✓→×'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def caracteres():
|
|
27
|
+
cs = [chr(c) for c in range(32, 127)] + [chr(c) for c in range(160, 256)]
|
|
28
|
+
return cs + list(EXTRAS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
if any(a in ('-h', '--help') for a in sys.argv[1:]):
|
|
33
|
+
print(__doc__)
|
|
34
|
+
return 0
|
|
35
|
+
try:
|
|
36
|
+
from PIL import ImageFont
|
|
37
|
+
except ImportError:
|
|
38
|
+
print('erro: falta o Pillow. Rode com o Python da skill: uv run --directory <SKILL> python ...', file=sys.stderr)
|
|
39
|
+
return 2
|
|
40
|
+
saida = {'em': EM, 'fontes': {}}
|
|
41
|
+
for nome, (arq, pesos) in MEDIR.items():
|
|
42
|
+
caminho = os.path.join(FONTES, arq)
|
|
43
|
+
if not os.path.exists(caminho):
|
|
44
|
+
print(f'aviso: não achei {caminho}', file=sys.stderr)
|
|
45
|
+
continue
|
|
46
|
+
por_peso = {}
|
|
47
|
+
for peso in pesos:
|
|
48
|
+
f = ImageFont.truetype(caminho, EM)
|
|
49
|
+
try:
|
|
50
|
+
eixos = f.get_variation_axes()
|
|
51
|
+
except OSError:
|
|
52
|
+
eixos = []
|
|
53
|
+
if eixos:
|
|
54
|
+
f.set_variation_by_axes([max(eixos[0]['minimum'], min(eixos[0]['maximum'], peso))])
|
|
55
|
+
por_peso[str(peso)] = {c: round(f.getlength(c)) for c in caracteres()}
|
|
56
|
+
saida['fontes'][nome] = {'arquivo': f'assets/fontes/{arq}', 'pesos': por_peso}
|
|
57
|
+
destino = os.path.join(FONTES, 'larguras.json')
|
|
58
|
+
with open(destino, 'w', encoding='utf-8') as fh:
|
|
59
|
+
json.dump(saida, fh, ensure_ascii=False, separators=(',', ':'))
|
|
60
|
+
print(f'ok: {destino}')
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == '__main__':
|
|
65
|
+
sys.exit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pessoa recortada (fundo transparente) no mesmo tempo do cut.mp4, para a tela verde.
|
|
3
|
+
|
|
4
|
+
Usa o recorte da skill editor-video (helpers/matte.py: modelo selfie_multiclass, fluxo óptico e filtro
|
|
5
|
+
guiado). O recorte é feito trecho por trecho do cut_timeline.json, então o filtro recomeça em cada corte e
|
|
6
|
+
a forma da tomada anterior não vaza para a próxima. Não use o `hyperframes remove-background` para isso:
|
|
7
|
+
ele deixa halo no cabelo e carrega a cor do fundo antigo na borda.
|
|
8
|
+
|
|
9
|
+
python3 ferramentas/pessoa_recortada.py --edit '<EDIT>' [--de 28 --ate 32.3]
|
|
10
|
+
[--skill PASTA_DA_SKILL] [--saida assets/pessoa.webm]
|
|
11
|
+
|
|
12
|
+
--skill: a pasta da skill editor-video. Sem ele, usa a skill anotada no projeto.json ("skill", que o
|
|
13
|
+
`build.py novo` escreve), depois a skill onde este modelo está (se rodar dentro dela), EDITOR_VIDEO_DIR (se
|
|
14
|
+
existir) e por fim ~/.claude/skills/editor-video e ~/.codex/skills/editor-video, nessa ordem. A primeira linha da
|
|
15
|
+
saída diz qual skill foi usada e de onde ela veio: confira que é a versão certa.
|
|
16
|
+
|
|
17
|
+
--de/--ate montam o vídeo só desse trecho (bem mais rápido; o recorte em si é feito no corte todo e fica
|
|
18
|
+
guardado). O pessoa.json anota o "inicio", e o build.py usa isso para alinhar com o cut.mp4.
|
|
19
|
+
|
|
20
|
+
Saídas: assets/pessoa.webm (VP9 com alfa, mesmo tamanho, fps e duração do cut.mp4) e assets/pessoa.json
|
|
21
|
+
(por trecho: onde a pessoa encosta na borda, usado pelo build.py para o lado cortado encostar na tela).
|
|
22
|
+
Leva uns 3 minutos por minuto de vídeo na primeira vez; depois o recorte fica guardado em <EDIT>/renders/matte.
|
|
23
|
+
Precisa do uv e da skill editor-video instalada. Só a biblioteca padrão do Python aqui.
|
|
24
|
+
"""
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
|
|
32
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
PROJETO = os.path.dirname(AQUI)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def erro(msg, cod=2):
|
|
37
|
+
print(f'erro: {msg}', file=sys.stderr)
|
|
38
|
+
sys.exit(cod)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def contar_quadros(caminho):
|
|
42
|
+
r = subprocess.run(['ffprobe', '-v', 'error', '-select_streams', 'v:0', '-count_packets', '-show_entries',
|
|
43
|
+
'stream=nb_read_packets', '-of', 'csv=p=0', caminho], capture_output=True, text=True)
|
|
44
|
+
try:
|
|
45
|
+
return int(r.stdout.strip().split(',')[0])
|
|
46
|
+
except ValueError:
|
|
47
|
+
return -1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def skill_padrao():
|
|
51
|
+
"""A skill editor-video instalada: EDITOR_VIDEO_DIR, depois Claude Code, depois Codex."""
|
|
52
|
+
candidatas = [os.environ.get('EDITOR_VIDEO_DIR') or '',
|
|
53
|
+
os.path.join('~', '.claude', 'skills', 'editor-video'),
|
|
54
|
+
os.path.join('~', '.codex', 'skills', 'editor-video')]
|
|
55
|
+
for c in candidatas:
|
|
56
|
+
if c and os.path.exists(os.path.join(os.path.expanduser(c), 'helpers', 'matte.py')):
|
|
57
|
+
return os.path.expanduser(c)
|
|
58
|
+
return os.path.expanduser(candidatas[1])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def skill_do_projeto():
|
|
62
|
+
"""a skill anotada no projeto.json ("skill"), ou None"""
|
|
63
|
+
try:
|
|
64
|
+
with open(os.path.join(PROJETO, 'projeto.json'), encoding='utf-8') as f:
|
|
65
|
+
s = str(json.load(f).get('skill') or '').strip()
|
|
66
|
+
except (OSError, ValueError, AttributeError):
|
|
67
|
+
return None
|
|
68
|
+
return os.path.expanduser(s) if s else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def achar_skill(explicita=None):
|
|
72
|
+
"""(pasta da skill, de onde veio): --skill, projeto.json, a skill onde o modelo está, a instalada"""
|
|
73
|
+
if explicita:
|
|
74
|
+
return os.path.abspath(os.path.expanduser(explicita)), '--skill'
|
|
75
|
+
s = skill_do_projeto()
|
|
76
|
+
if s:
|
|
77
|
+
return os.path.abspath(s), 'projeto.json ("skill")'
|
|
78
|
+
pai = os.path.dirname(PROJETO)
|
|
79
|
+
if os.path.basename(PROJETO) == 'modelo-projeto' and os.path.exists(os.path.join(pai, 'helpers', 'matte.py')):
|
|
80
|
+
return pai, 'a skill onde o modelo está'
|
|
81
|
+
return os.path.abspath(skill_padrao()), 'a skill instalada (o projeto.json não diz qual; anote em "skill")'
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
ap = argparse.ArgumentParser(description='pessoa recortada para a tela verde')
|
|
86
|
+
ap.add_argument('--edit', required=True, help='pasta edit da editor-video (com cut.mp4 e cut_timeline.json)')
|
|
87
|
+
ap.add_argument('--skill', default=None,
|
|
88
|
+
help='pasta da skill editor-video (padrão: o "skill" do projeto.json, a skill onde o modelo está, '
|
|
89
|
+
'EDITOR_VIDEO_DIR, ~/.claude/skills/editor-video ou ~/.codex/skills/editor-video)')
|
|
90
|
+
ap.add_argument('--saida', default=os.path.join(PROJETO, 'assets', 'pessoa.webm'))
|
|
91
|
+
ap.add_argument('--crf', type=int, default=20)
|
|
92
|
+
ap.add_argument('--de', type=float, default=None, help='início do trecho (s do corte)')
|
|
93
|
+
ap.add_argument('--ate', type=float, default=None, help='fim do trecho (s do corte)')
|
|
94
|
+
a = ap.parse_args()
|
|
95
|
+
edit = os.path.abspath(os.path.expanduser(a.edit))
|
|
96
|
+
skill, origem = achar_skill(a.skill)
|
|
97
|
+
print(f'skill: {skill} ({origem})', flush=True)
|
|
98
|
+
cut = os.path.join(edit, 'cut.mp4')
|
|
99
|
+
tl_p = os.path.join(edit, 'cut_timeline.json')
|
|
100
|
+
for p in (cut, tl_p):
|
|
101
|
+
if not os.path.exists(p):
|
|
102
|
+
erro(f'falta {p}')
|
|
103
|
+
if os.path.islink(cut):
|
|
104
|
+
# o matte.py resolve o atalho, deixa de reconhecer o cut_timeline.json e recorta tudo como um trecho só
|
|
105
|
+
# (o filtro não recomeça nos cortes e a tomada anterior vaza na seguinte)
|
|
106
|
+
erro('o cut.mp4 da pasta edit é um atalho. Deixe o arquivo de verdade lá (a editor-video grava assim).')
|
|
107
|
+
if not os.path.exists(os.path.join(skill, 'helpers', 'matte.py')):
|
|
108
|
+
erro(f'não achei a skill editor-video em {skill} (use --skill, ou "skill" no projeto.json)')
|
|
109
|
+
with open(tl_p, encoding='utf-8') as f:
|
|
110
|
+
tl = json.load(f)
|
|
111
|
+
fps, W, H = tl['fps'], tl['width'], tl['height']
|
|
112
|
+
|
|
113
|
+
print('recortando a pessoa com a editor-video (a primeira vez leva uns 3 min por minuto de vídeo)...', flush=True)
|
|
114
|
+
# o progresso sai na hora (a pessoa não fica olhando uma tela parada); o resumo JSON vem no fim
|
|
115
|
+
# PrimoCode: o Python do editor (venv próprio) vem em PRIMOCODE_EDITOR_PYTHON; sem ele, o uv da skill
|
|
116
|
+
py = os.environ.get('PRIMOCODE_EDITOR_PYTHON')
|
|
117
|
+
base = [py] if py and os.path.isfile(py) else ['uv', 'run', '--directory', skill, 'python']
|
|
118
|
+
proc = subprocess.Popen(base + ['helpers/matte.py', '--edit-dir', edit, '--json'], cwd=skill,
|
|
119
|
+
stdout=subprocess.PIPE, text=True, bufsize=1, env=dict(os.environ, PYTHONUNBUFFERED='1'))
|
|
120
|
+
corpo = []
|
|
121
|
+
for linha in proc.stdout:
|
|
122
|
+
if corpo or linha.rstrip('\n') == '{':
|
|
123
|
+
corpo.append(linha)
|
|
124
|
+
else:
|
|
125
|
+
print(' ' + linha.rstrip('\n'), flush=True)
|
|
126
|
+
if proc.wait() != 0:
|
|
127
|
+
erro('o recorte da editor-video falhou (veja a mensagem acima)', proc.returncode)
|
|
128
|
+
if not corpo:
|
|
129
|
+
erro('não entendi a resposta do matte.py')
|
|
130
|
+
rep = json.loads(''.join(corpo))
|
|
131
|
+
trechos = sorted(rep['ranges'], key=lambda x: x['start_frame'])
|
|
132
|
+
for x, y in zip(trechos, trechos[1:]):
|
|
133
|
+
if x['end_frame'] != y['start_frame']:
|
|
134
|
+
erro(f'os trechos do recorte não emendam ({x["id"]} termina em {x["end_frame"]}, {y["id"]} começa em {y["start_frame"]})')
|
|
135
|
+
|
|
136
|
+
total = trechos[-1]['end_frame'] - trechos[0]['start_frame']
|
|
137
|
+
f_ini = max(0, int(round((a.de or 0) * fps)))
|
|
138
|
+
f_fim = min(total, int(round(a.ate * fps)) if a.ate is not None else total)
|
|
139
|
+
if f_fim <= f_ini:
|
|
140
|
+
erro('--ate tem que ser maior que --de')
|
|
141
|
+
os.makedirs(os.path.dirname(os.path.abspath(a.saida)), exist_ok=True)
|
|
142
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
143
|
+
lista = os.path.join(tmp, 'mattes.txt')
|
|
144
|
+
with open(lista, 'w', encoding='utf-8') as f:
|
|
145
|
+
for x in trechos:
|
|
146
|
+
p = x['file'] if os.path.isabs(x['file']) else os.path.join(edit, x['file'])
|
|
147
|
+
f.write("file '" + p.replace("'", "'\\''") + "'\n")
|
|
148
|
+
# cor do cut.mp4 + alfa do recorte, quadro a quadro na mesma ordem (setpts alinha os dois pela contagem)
|
|
149
|
+
corte = ''
|
|
150
|
+
if a.de is not None or a.ate is not None:
|
|
151
|
+
corte = f',trim=start_frame={f_ini}:end_frame={f_fim},setpts=N/({fps}*TB)'
|
|
152
|
+
filtro = (f'[0:v]setpts=N/({fps}*TB){corte},format=rgba[c];'
|
|
153
|
+
f'[1:v]setpts=N/({fps}*TB){corte},format=gray,scale={W}:{H}:flags=area[m];'
|
|
154
|
+
f'[c][m]alphamerge,format=yuva420p[v]')
|
|
155
|
+
cmd = ['ffmpeg', '-v', 'error', '-y', '-i', cut, '-f', 'concat', '-safe', '0', '-i', lista,
|
|
156
|
+
'-filter_complex', filtro, '-map', '[v]', '-r', str(fps), '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p',
|
|
157
|
+
'-b:v', '0', '-crf', str(a.crf), '-deadline', 'good', '-cpu-used', '4', '-row-mt', '1',
|
|
158
|
+
'-auto-alt-ref', '0', '-metadata:s:v:0', 'alpha_mode=1', '-an', a.saida]
|
|
159
|
+
print('montando o vídeo com fundo transparente...', flush=True)
|
|
160
|
+
if subprocess.run(cmd).returncode != 0:
|
|
161
|
+
erro('o ffmpeg falhou ao montar o pessoa.webm', 1)
|
|
162
|
+
esperado = f_fim - f_ini
|
|
163
|
+
feito = contar_quadros(a.saida)
|
|
164
|
+
if abs(feito - esperado) > 1:
|
|
165
|
+
print(f'aviso: o pessoa.webm tem {feito} quadros e o corte tem {esperado}', file=sys.stderr)
|
|
166
|
+
info = dict(video=os.path.basename(a.saida), fps=fps, largura=W, altura=H, quadros=feito,
|
|
167
|
+
inicio=round(f_ini / fps, 4), fim=round(f_fim / fps, 4),
|
|
168
|
+
trechos=[dict(id=x['id'], de=round(x['start_frame'] / fps, 4), ate=round(x['end_frame'] / fps, 4),
|
|
169
|
+
stats=x.get('stats') or {}) for x in trechos])
|
|
170
|
+
with open(os.path.splitext(a.saida)[0] + '.json', 'w', encoding='utf-8') as f:
|
|
171
|
+
json.dump(info, f, ensure_ascii=False, indent=1)
|
|
172
|
+
print(f'ok: {a.saida} ({feito} quadros, {len(trechos)} trechos)')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == '__main__':
|
|
176
|
+
main()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rastreio das mãos no cut.mp4 (MediaPipe Hand Landmarker) -> assets/maos.json, para o "objeto preso na mão".
|
|
3
|
+
|
|
4
|
+
uv run --python 3.12 --with mediapipe==0.10.21 --with numpy python ferramentas/rastrear_maos.py \
|
|
5
|
+
--edit '<EDIT>' --de 8 --ate 13
|
|
6
|
+
|
|
7
|
+
Versão fixa de propósito: o mediapipe 1.x (e o Python 3.13) aborta com "Service is unavailable". A primeira vez
|
|
8
|
+
o uv baixa o Python 3.12 e o mediapipe (uns minutos, centenas de MB); avise a pessoa antes. Depois fica guardado.
|
|
9
|
+
|
|
10
|
+
--de/--ate limitam ao trecho que vai ter o objeto (mais rápido). O modelo hand_landmarker.task (Apache 2.0,
|
|
11
|
+
do Google) é procurado na skill do projeto (o "skill" do projeto.json, em assets/models), depois em
|
|
12
|
+
~/.editor-video/modelos (e na pasta antiga, ~/.sala-de-corte/modelos, sem baixar de novo); se não tiver, é baixado
|
|
13
|
+
na primeira vez para ~/.editor-video/modelos (uns 8 MB). Ou use --modelo.
|
|
14
|
+
|
|
15
|
+
Regras que já custaram retrabalho:
|
|
16
|
+
- Em cada corte do cut_timeline.json o rastreador recomeça: a mão de uma tomada não pode "pular" para a outra.
|
|
17
|
+
- O rótulo Left/Right do MediaPipe engana (depende de o vídeo estar espelhado). Aqui a mão é nomeada pelo lado
|
|
18
|
+
da IMAGEM onde ela nasce e segue a trilha: "esquerda_da_imagem" ou "direita_da_imagem".
|
|
19
|
+
- A palma muitas vezes fica perto da borda de baixo. Por isso sai também "nos" (média dos nós dos dedos,
|
|
20
|
+
pontos 5, 9, 13 e 17): o objeto fica acima deles.
|
|
21
|
+
- Suavização de 3 quadros centrada (atraso zero), sem atravessar corte.
|
|
22
|
+
|
|
23
|
+
Saída: {fps, largura, altura, cortes, quadros: [{t, maos: [{lado, palma, nos, pts}]}]}, coordenadas de 0 a 1.
|
|
24
|
+
"""
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import urllib.request
|
|
31
|
+
|
|
32
|
+
MODELO_URL = 'https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task'
|
|
33
|
+
AQUI = os.path.dirname(os.path.abspath(__file__))
|
|
34
|
+
PROJETO = os.path.dirname(AQUI)
|
|
35
|
+
PALMA = (0, 5, 9, 13, 17)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
COMANDO = ("uv run --python 3.12 --with mediapipe==0.10.21 --with numpy python ferramentas/rastrear_maos.py "
|
|
39
|
+
"--edit '<EDIT>' --de 8 --ate 13")
|
|
40
|
+
MEDIAPIPE = '0.10.21'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def skill_do_projeto():
|
|
44
|
+
"""(pasta da skill, de onde veio) pelo "skill" do projeto.json; senão a skill onde o modelo está; senão None"""
|
|
45
|
+
try:
|
|
46
|
+
with open(os.path.join(PROJETO, 'projeto.json'), encoding='utf-8') as f:
|
|
47
|
+
s = str(json.load(f).get('skill') or '').strip()
|
|
48
|
+
if s:
|
|
49
|
+
return os.path.abspath(os.path.expanduser(s)), 'projeto.json ("skill")'
|
|
50
|
+
except (OSError, ValueError, AttributeError):
|
|
51
|
+
pass
|
|
52
|
+
pai = os.path.dirname(PROJETO)
|
|
53
|
+
if os.path.basename(PROJETO) == 'modelo-projeto' and os.path.exists(os.path.join(pai, 'helpers', 'matte.py')):
|
|
54
|
+
return pai, 'a skill onde o modelo está'
|
|
55
|
+
return None, ''
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def modelo_padrao(skill=None):
|
|
59
|
+
"""a skill do projeto (assets/models), depois ~/.editor-video/modelos; o arquivo baixado antes na pasta antiga
|
|
60
|
+
(~/.sala-de-corte/modelos) continua valendo"""
|
|
61
|
+
if skill:
|
|
62
|
+
na_skill = os.path.join(skill, 'assets', 'models', 'hand_landmarker.task')
|
|
63
|
+
if os.path.exists(na_skill):
|
|
64
|
+
return na_skill
|
|
65
|
+
novo = os.path.join(os.path.expanduser('~'), '.editor-video', 'modelos', 'hand_landmarker.task')
|
|
66
|
+
antigo = os.path.join(os.path.expanduser('~'), '.sala-de-corte', 'modelos', 'hand_landmarker.task')
|
|
67
|
+
if not os.path.exists(novo) and os.path.exists(antigo):
|
|
68
|
+
return antigo
|
|
69
|
+
return novo
|
|
70
|
+
NOS = (5, 9, 13, 17)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def media(pts, idx):
|
|
74
|
+
return [sum(pts[i][0] for i in idx) / len(idx), sum(pts[i][1] for i in idx) / len(idx)]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
ap = argparse.ArgumentParser(description='rastreio das mãos para prender um objeto na mão')
|
|
79
|
+
ap.add_argument('--edit', required=True)
|
|
80
|
+
ap.add_argument('--de', type=float, default=0.0)
|
|
81
|
+
ap.add_argument('--ate', type=float, default=None)
|
|
82
|
+
ap.add_argument('--modelo', default=None, help='hand_landmarker.task (padrão: a skill do projeto, ~/.editor-video/modelos)')
|
|
83
|
+
ap.add_argument('--saida', default=os.path.join(PROJETO, 'assets', 'maos.json'))
|
|
84
|
+
a = ap.parse_args()
|
|
85
|
+
skill, origem = skill_do_projeto()
|
|
86
|
+
print(f'skill: {skill} ({origem})' if skill else 'skill: nenhuma anotada no projeto.json ("skill"); o modelo de mãos '
|
|
87
|
+
'vem de ~/.editor-video/modelos', flush=True)
|
|
88
|
+
a.modelo = a.modelo or modelo_padrao(skill)
|
|
89
|
+
try:
|
|
90
|
+
import numpy as np
|
|
91
|
+
import mediapipe as mp
|
|
92
|
+
from mediapipe.tasks.python import BaseOptions, vision
|
|
93
|
+
except ImportError:
|
|
94
|
+
print(f'erro: falta o mediapipe. Rode assim (a primeira vez baixa uns minutos de pacotes):\n {COMANDO}',
|
|
95
|
+
file=sys.stderr)
|
|
96
|
+
return 2
|
|
97
|
+
versao = getattr(mp, '__version__', '?')
|
|
98
|
+
if versao != MEDIAPIPE or sys.version_info[:2] != (3, 12):
|
|
99
|
+
print(f'aviso: esta ferramenta foi conferida com o mediapipe {MEDIAPIPE} no Python 3.12 (aqui: mediapipe '
|
|
100
|
+
f'{versao}, Python {sys.version_info[0]}.{sys.version_info[1]}). O mediapipe 1.x aborta com "Service is '
|
|
101
|
+
f'unavailable". Se der erro, rode:\n {COMANDO}', file=sys.stderr, flush=True)
|
|
102
|
+
edit = os.path.abspath(os.path.expanduser(a.edit))
|
|
103
|
+
with open(os.path.join(edit, 'cut_timeline.json'), encoding='utf-8') as f:
|
|
104
|
+
tl = json.load(f)
|
|
105
|
+
fps, W, H, dur = tl['fps'], tl['width'], tl['height'], tl['duration']
|
|
106
|
+
ate = min(a.ate if a.ate is not None else dur, dur)
|
|
107
|
+
f0, f1 = int(round(a.de * fps)), int(round(ate * fps))
|
|
108
|
+
cortes = sorted(int(s['out_start_frame']) if 'out_start_frame' in s else int(round(s['out_start'] * fps))
|
|
109
|
+
for s in tl['segments'] if s['out_start'] > 0.01)
|
|
110
|
+
if not os.path.exists(a.modelo):
|
|
111
|
+
os.makedirs(os.path.dirname(a.modelo), exist_ok=True)
|
|
112
|
+
print('baixando o modelo de mãos do MediaPipe (uns 8 MB)...', flush=True)
|
|
113
|
+
urllib.request.urlretrieve(MODELO_URL, a.modelo)
|
|
114
|
+
|
|
115
|
+
def novo():
|
|
116
|
+
op = vision.HandLandmarkerOptions(base_options=BaseOptions(model_asset_path=a.modelo), num_hands=2,
|
|
117
|
+
running_mode=vision.RunningMode.VIDEO, min_hand_detection_confidence=0.5,
|
|
118
|
+
min_hand_presence_confidence=0.5, min_tracking_confidence=0.5)
|
|
119
|
+
return vision.HandLandmarker.create_from_options(op)
|
|
120
|
+
|
|
121
|
+
# quadros exatos, em sequência, por pipe (menor lado 540 basta para as mãos; as coordenadas saem de 0 a 1)
|
|
122
|
+
esc = 540 / min(W, H)
|
|
123
|
+
w2, h2 = int(round(W * esc / 2)) * 2, int(round(H * esc / 2)) * 2
|
|
124
|
+
cmd = ['ffmpeg', '-v', 'error', '-ss', f'{f0 / fps:.4f}', '-i', os.path.join(edit, 'cut.mp4'), '-frames:v', str(f1 - f0),
|
|
125
|
+
'-vf', f'scale={w2}:{h2}', '-f', 'rawvideo', '-pix_fmt', 'rgb24', '-']
|
|
126
|
+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
|
127
|
+
lm, cruas = novo(), {}
|
|
128
|
+
n = f0
|
|
129
|
+
while n < f1:
|
|
130
|
+
buf = proc.stdout.read(w2 * h2 * 3)
|
|
131
|
+
if len(buf) < w2 * h2 * 3:
|
|
132
|
+
break
|
|
133
|
+
if n in cortes and n != f0:
|
|
134
|
+
lm.close()
|
|
135
|
+
lm = novo()
|
|
136
|
+
img = mp.Image(image_format=mp.ImageFormat.SRGB, data=np.frombuffer(buf, np.uint8).reshape(h2, w2, 3))
|
|
137
|
+
res = lm.detect_for_video(img, int(round(n * 1000 / fps)))
|
|
138
|
+
cruas[n] = [[[p.x, p.y] for p in hand] for hand in res.hand_landmarks]
|
|
139
|
+
n += 1
|
|
140
|
+
proc.stdout.close()
|
|
141
|
+
proc.wait()
|
|
142
|
+
lm.close()
|
|
143
|
+
nfim = n
|
|
144
|
+
|
|
145
|
+
# trilhas: casa cada mão com a do quadro anterior pela palma, só dentro da mesma tomada
|
|
146
|
+
trilhas, ativas, prox_id = {}, [], 0
|
|
147
|
+
for n in range(f0, nfim):
|
|
148
|
+
if n in cortes:
|
|
149
|
+
ativas = []
|
|
150
|
+
novas = []
|
|
151
|
+
livres = list(ativas)
|
|
152
|
+
for pts in cruas.get(n, []):
|
|
153
|
+
pm = media(pts, PALMA)
|
|
154
|
+
melhor = min(livres, key=lambda tr: (tr['ult'][0] - pm[0]) ** 2 + (tr['ult'][1] - pm[1]) ** 2, default=None)
|
|
155
|
+
if melhor and ((melhor['ult'][0] - pm[0]) ** 2 + (melhor['ult'][1] - pm[1]) ** 2) ** 0.5 < 0.15 and n - melhor['n'] <= 3:
|
|
156
|
+
livres.remove(melhor)
|
|
157
|
+
tr = melhor
|
|
158
|
+
else:
|
|
159
|
+
tr = dict(id=prox_id, lado='esquerda_da_imagem' if pm[0] < 0.5 else 'direita_da_imagem', q={})
|
|
160
|
+
prox_id += 1
|
|
161
|
+
tr['ult'], tr['n'] = pm, n
|
|
162
|
+
tr['q'][n] = pts
|
|
163
|
+
trilhas[tr['id']] = tr
|
|
164
|
+
novas.append(tr)
|
|
165
|
+
ativas = novas + [tr for tr in livres if n - tr['n'] <= 3]
|
|
166
|
+
lados = novas
|
|
167
|
+
if len(lados) == 2 and lados[0]['lado'] == lados[1]['lado']:
|
|
168
|
+
e, d = sorted(lados, key=lambda tr: tr['ult'][0])
|
|
169
|
+
e['lado'], d['lado'] = 'esquerda_da_imagem', 'direita_da_imagem'
|
|
170
|
+
quadros = {k: [] for k in range(f0, nfim)}
|
|
171
|
+
for tr in trilhas.values():
|
|
172
|
+
ks = sorted(tr['q'])
|
|
173
|
+
for k in ks: # média de 3 quadros centrada; na ponta da trilha fica o quadro cru
|
|
174
|
+
viz = [tr['q'][j] for j in (k - 1, k, k + 1) if j in tr['q'] and not (j != k and (max(j, k) in cortes))]
|
|
175
|
+
pts = [[sum(v[i][0] for v in viz) / len(viz), sum(v[i][1] for v in viz) / len(viz)] for i in range(21)]
|
|
176
|
+
quadros[k].append(dict(lado=tr['lado'], palma=[round(x, 4) for x in media(pts, PALMA)],
|
|
177
|
+
nos=[round(x, 4) for x in media(pts, NOS)], pts=[[round(x, 4), round(y, 4)] for x, y in pts]))
|
|
178
|
+
saida = dict(fps=fps, largura=W, altura=H, cortes=[round(c / fps, 4) for c in cortes],
|
|
179
|
+
quadros=[dict(t=round(k / fps, 4), maos=quadros[k]) for k in sorted(quadros)])
|
|
180
|
+
os.makedirs(os.path.dirname(os.path.abspath(a.saida)), exist_ok=True)
|
|
181
|
+
with open(a.saida, 'w', encoding='utf-8') as f:
|
|
182
|
+
json.dump(saida, f, separators=(',', ':'))
|
|
183
|
+
com = sum(1 for k in quadros if quadros[k])
|
|
184
|
+
print(f'ok: {a.saida} · {len(quadros)} quadros de {f0 / fps:.2f} a {nfim / fps:.2f} s · mão em {com} · {len(trilhas)} trilha(s)')
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == '__main__':
|
|
189
|
+
sys.exit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
|
|
3
|
+
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
|
|
4
|
+
"paths": {
|
|
5
|
+
"blocks": "compositions",
|
|
6
|
+
"components": "compositions/components",
|
|
7
|
+
"assets": "assets"
|
|
8
|
+
},
|
|
9
|
+
"media": {
|
|
10
|
+
"autoProxy": true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "editor-video-projeto",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "npx --yes hyperframes@0.8.79 preview",
|
|
7
|
+
"lint": "npx --yes hyperframes@0.8.79 lint",
|
|
8
|
+
"check": "npx --yes hyperframes@0.8.79 check",
|
|
9
|
+
"render": "npx --yes hyperframes@0.8.79 render"
|
|
10
|
+
}
|
|
11
|
+
}
|