primocode 10.13.0 → 10.14.1
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/README.md +66 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +56 -3
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/cota.js +157 -0
- package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
- package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
- package/lib/estudio/dados/projetos/clube-agent.json +120 -0
- package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
- package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
- package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
- package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
- package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
- package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
- package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
- package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
- package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
- package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
- package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
- package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
- package/lib/estudio/dados/projetos/clube-clube.json +108 -0
- package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
- package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
- package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
- package/lib/estudio/dados/projetos/clube-dub.json +119 -0
- package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
- package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
- package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
- package/lib/estudio/dados/projetos/clube-frame.json +108 -0
- package/lib/estudio/dados/projetos/clube-glass.json +118 -0
- package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
- package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
- package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
- package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
- package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
- package/lib/estudio/dados/projetos/clube-morph.json +119 -0
- package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
- package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
- package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
- package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
- package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
- package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
- package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
- package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
- package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
- package/lib/estudio/dados/projetos/clube-promo.json +114 -0
- package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
- package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
- package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
- package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
- package/lib/estudio/dados/projetos/clube-robo.json +125 -0
- package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
- package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
- package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
- package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
- package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
- package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
- package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
- package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
- package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
- package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
- package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
- package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
- package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
- package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
- package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
- package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
- package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
- package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
- package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
- package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
- package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
- package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
- package/lib/estudio/editor.js +290 -0
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/index.js +97 -2
- package/lib/estudio/kit-ae/base.jsx +1146 -0
- package/lib/estudio/kit-ae/cenas.jsx +395 -0
- package/lib/estudio/kit-ae/primitivas.jsx +911 -0
- package/lib/estudio/maquina-diretor.js +393 -0
- package/lib/estudio/maquina-elementos.js +134 -0
- package/lib/estudio/maquina-layout.js +198 -0
- package/lib/estudio/maquina-som.js +478 -0
- package/lib/estudio/maquina-trilha.js +160 -0
- package/lib/estudio/maquina-voz.js +429 -0
- package/lib/estudio/maquina.js +516 -0
- package/lib/estudio/projetos.js +72 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/limites.js +100 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +312 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +49 -19
- package/lib/preparar.js +111 -0
- package/lib/retomar.js +55 -0
- package/lib/sandbox.js +197 -0
- package/lib/skills.js +3 -1
- package/lib/tools.js +13 -4
- package/lib/ui.js +1 -1
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""oraculo_take.py — confere um take único antes de alguém assistir, e exporta os eventos para a trilha.
|
|
2
|
+
|
|
3
|
+
python oraculo_take.py comp/index.html --video render/demo.mp4 # relatório (pernas abaixo)
|
|
4
|
+
python oraculo_take.py comp/index.html --eventos audio/eventos.json # só exporta window.__events()
|
|
5
|
+
|
|
6
|
+
Pernas (inspiradas no verify/probe do onetake, 05-pesquisa/referencias/2026-10-12-onetake-take-unico.md):
|
|
7
|
+
- continuidade: em cada fronteira de __meta.batidas, algo de __track(t) aparece na tela dos dois lados e anda ≥ 2 % da
|
|
8
|
+
diagonal ou muda ≥ 10 % de tamanho em ±0,25 s (carried); só um objeto grande parado (anchored, vale ½); nada (bare).
|
|
9
|
+
Nota = (carried + anchored/2) / fronteiras. Reprova < 0,5, alerta < 0,7. Cega a carry feito de dois elementos.
|
|
10
|
+
- enquadramento: cada id de __meta.inFrame = {id: [t0, t1]} fica com o centro dentro do quadro (margem 40 px).
|
|
11
|
+
- velocidade: pico de __motion por quadro; acima de ~80 px/quadro precisa do obturador (render_take.py já faz).
|
|
12
|
+
- vídeo: fração de quadros de baixa energia (respiros) e trecho calmo mais longo; saltos (diferença 3× a mediana
|
|
13
|
+
dos vizinhos); na nossa regra a câmera nunca para, então "parado" aqui é energia baixa, não zero.
|
|
14
|
+
- áudio: loudness integrada (alvo −14 LUFS no narrado) e pico.
|
|
15
|
+
"""
|
|
16
|
+
import argparse, json, subprocess, sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pagina(comp, fn):
|
|
23
|
+
from playwright.sync_api import sync_playwright
|
|
24
|
+
path, _, q = comp.partition("?")
|
|
25
|
+
url = Path(path).resolve().as_uri() + ("?" + q if q else "")
|
|
26
|
+
with sync_playwright() as p:
|
|
27
|
+
br = p.chromium.launch()
|
|
28
|
+
pg = br.new_context(viewport={"width": 1080, "height": 1920}).new_page()
|
|
29
|
+
pg.goto(url); pg.evaluate("window.__ready")
|
|
30
|
+
r = fn(pg)
|
|
31
|
+
br.close()
|
|
32
|
+
return r
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def continuidade(pg, meta):
|
|
36
|
+
W, H = meta.get("w", 1080), meta.get("h", 1920)
|
|
37
|
+
diag = (W * W + H * H) ** 0.5
|
|
38
|
+
dentro = lambda b: b and b["w"] > 1 and b["x"] + b["w"] > 0 and b["x"] < W and b["y"] + b["h"] > 0 and b["y"] < H
|
|
39
|
+
res = []
|
|
40
|
+
for tb in meta.get("batidas", []):
|
|
41
|
+
A = pg.evaluate(f"window.__track({tb - 0.25})"); B = pg.evaluate(f"window.__track({tb + 0.25})")
|
|
42
|
+
tipo, quem = "bare", ""
|
|
43
|
+
for k in A:
|
|
44
|
+
a, b = A[k], B.get(k)
|
|
45
|
+
if not (dentro(a) and dentro(b)):
|
|
46
|
+
continue
|
|
47
|
+
anda = ((a["x"] + a["w"] / 2 - b["x"] - b["w"] / 2) ** 2 + (a["y"] + a["h"] / 2 - b["y"] - b["h"] / 2) ** 2) ** 0.5
|
|
48
|
+
esc = abs((b["w"] * b["h"]) ** 0.5 / max(1, (a["w"] * a["h"]) ** 0.5) - 1)
|
|
49
|
+
if anda >= 0.02 * diag or esc >= 0.10:
|
|
50
|
+
tipo, quem = "carried", k; break
|
|
51
|
+
if a["w"] * a["h"] >= 0.03 * W * H:
|
|
52
|
+
tipo, quem = "anchored", k
|
|
53
|
+
res.append((tb, tipo, quem))
|
|
54
|
+
nota = sum(1 if t == "carried" else 0.5 if t == "anchored" else 0 for _, t, _ in res) / max(1, len(res))
|
|
55
|
+
return nota, res
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def enquadramento(pg, meta, fps=10):
|
|
59
|
+
W, H, falhas = meta.get("w", 1080), meta.get("h", 1920), []
|
|
60
|
+
for k, (t0, t1) in meta.get("inFrame", {}).items():
|
|
61
|
+
for t in np.arange(t0, t1, 1 / fps):
|
|
62
|
+
b = pg.evaluate(f"window.__track({t})").get(k)
|
|
63
|
+
if not b: continue
|
|
64
|
+
cx, cy = b["x"] + b["w"] / 2, b["y"] + b["h"] / 2
|
|
65
|
+
if not (40 <= cx <= W - 40 and 40 <= cy <= H - 40):
|
|
66
|
+
falhas.append((k, round(float(t), 2)))
|
|
67
|
+
return falhas
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def velocidade(pg, meta, fps=30):
|
|
71
|
+
dur = meta["dur"]; v = pg.evaluate(f"(() => {{ const r = []; for (let f = 0; f < {int(dur * fps) - 1}; f++) r.push(window.__motion(f / {fps}, (f + 1) / {fps})); return r; }})()")
|
|
72
|
+
i = int(np.argmax(v)); return float(v[i]), i / fps, float(np.mean(np.array(v) > 80))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def video(vid):
|
|
76
|
+
"""Energia entre quadros em fluxo (um quadro por vez: um vídeo de 2 min não cabe inteiro na memória) e fps lido do arquivo."""
|
|
77
|
+
w, h = 192, 341
|
|
78
|
+
r = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", "-of", "csv=p=0", vid], capture_output=True, text=True).stdout.strip()
|
|
79
|
+
n_, d_ = (r.split("/") + ["1"])[:2]; fps = float(n_) / float(d_ or 1)
|
|
80
|
+
pr = subprocess.Popen(["ffmpeg", "-v", "error", "-i", vid, "-vf", f"scale={w}:{h},format=gray", "-f", "rawvideo", "-"], stdout=subprocess.PIPE)
|
|
81
|
+
tam, ant, dl = w * h, None, []
|
|
82
|
+
while True:
|
|
83
|
+
b = pr.stdout.read(tam)
|
|
84
|
+
if len(b) < tam: break
|
|
85
|
+
q = np.frombuffer(b, np.uint8).astype(np.int16)
|
|
86
|
+
if ant is not None: dl.append(float(np.abs(q - ant).mean()))
|
|
87
|
+
ant = q
|
|
88
|
+
pr.wait()
|
|
89
|
+
d = np.array(dl)
|
|
90
|
+
baixo = d < 0.4
|
|
91
|
+
maior, cur = 0, 0
|
|
92
|
+
for b in baixo:
|
|
93
|
+
cur = cur + 1 if b else 0; maior = max(maior, cur)
|
|
94
|
+
saltos = [round((i + 1) / fps, 2) for i in range(2, len(d) - 2) if d[i] > 3 * max(np.median(np.r_[d[i - 2:i], d[i + 1:i + 3]]), 0.3) and d[i] > 1.5]
|
|
95
|
+
return float(baixo.mean()), maior / fps, saltos
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def audio(vid):
|
|
99
|
+
try:
|
|
100
|
+
import pyloudnorm as pyln, soundfile as sf
|
|
101
|
+
tmp = Path(vid).with_suffix(".oraculo.wav")
|
|
102
|
+
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", vid, "-vn", "-ac", "2", "-ar", "48000", str(tmp)], check=True)
|
|
103
|
+
x, sr = sf.read(str(tmp)); tmp.unlink()
|
|
104
|
+
return pyln.Meter(sr).integrated_loudness(x), 20 * np.log10(np.abs(x).max() + 1e-9)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return None, str(e)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
ap = argparse.ArgumentParser()
|
|
111
|
+
ap.add_argument("comp"); ap.add_argument("--video"); ap.add_argument("--eventos")
|
|
112
|
+
a = ap.parse_args()
|
|
113
|
+
if a.eventos:
|
|
114
|
+
ev = pagina(a.comp, lambda pg: pg.evaluate("window.__events()"))
|
|
115
|
+
json.dump(ev, open(a.eventos, "w", encoding="utf-8"), indent=1); print(len(ev["events"]), "eventos →", a.eventos); return
|
|
116
|
+
def tudo(pg):
|
|
117
|
+
meta = pg.evaluate("window.__meta")
|
|
118
|
+
return meta, continuidade(pg, meta), enquadramento(pg, meta), velocidade(pg, meta)
|
|
119
|
+
meta, (nota, res), falhas, (pico, tpico, rapido) = pagina(a.comp, tudo)
|
|
120
|
+
ok = True
|
|
121
|
+
print(f"continuidade {nota:.2f} sobre {len(res)} fronteiras " + ("REPROVA" if nota < 0.5 else "alerta" if nota < 0.7 else "ok"))
|
|
122
|
+
for tb, tipo, quem in res: print(f" {tb:6.2f}s {tipo:8s} {quem}")
|
|
123
|
+
ok &= nota >= 0.5
|
|
124
|
+
print(f"enquadramento: {'ok' if not falhas else 'FALHA ' + str(falhas[:8])}"); ok &= not falhas
|
|
125
|
+
print(f"velocidade: pico {pico:.0f} px/quadro a {tpico:.2f}s; {rapido:.0%} dos quadros acima de 80 px (obturador obrigatório)")
|
|
126
|
+
if a.video:
|
|
127
|
+
bx, calmo, saltos = video(a.video)
|
|
128
|
+
print(f"respiros: {bx:.0%} de quadros com energia baixa, trecho calmo mais longo {calmo:.1f}s " + ("ok" if bx >= 0.25 else "poucos respiros"))
|
|
129
|
+
print(f"saltos: {saltos or 'nenhum'} (todo salto tem de ser corte de propósito)")
|
|
130
|
+
L, pk = audio(a.video)
|
|
131
|
+
if L is not None: print(f"áudio: {L:.1f} LUFS, pico {pk:.1f} dBFS")
|
|
132
|
+
print("RESULTADO:", "PASSA" if ok else "REPROVA")
|
|
133
|
+
sys.exit(0 if ok else 1)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""render_take.py — renderiza uma composição seek(t) do take único, com desfoque de movimento de obturador real.
|
|
2
|
+
|
|
3
|
+
python render_take.py comp/index.html --out render/demo-v1.mp4 --audio audio/mix.wav # 30 qps, 1080x1920
|
|
4
|
+
python render_take.py comp/index.html --out render/demo-v1-60.mp4 --fps 60 --audio audio/mix.wav
|
|
5
|
+
python render_take.py comp/index.html --stills 0.5,3,6.2 --out revisao/stills-v1.png # folha de quadros
|
|
6
|
+
python render_take.py comp/index.html --out render/previa.mp4 --previa --audio audio/mix.wav # prévia em ~1 min
|
|
7
|
+
python render_take.py comp/index.html --out render/final.mp4 --fps 60 --trechos 4 --turbo --audio audio/mix.wav
|
|
8
|
+
# final: refaz só os trechos de 4 s que mudaram desde o último render (cache em render/_trechos/)
|
|
9
|
+
|
|
10
|
+
Contrato da página: window.__ready (Promise), window.__seek(t) → Promise (resolve com as imagens decodificadas),
|
|
11
|
+
window.__meta = {dur, w, h}; opcional window.__motion(t0, t1) → px que algo anda na tela entre t0 e t1.
|
|
12
|
+
|
|
13
|
+
Obturador: cada quadro é a média, em luz linear, de S capturas espalhadas em `--shutter` graus em volta do instante
|
|
14
|
+
do quadro. Primeiro captura as duas pontas: se forem iguais, nada mexeu e o quadro sai sem custo. S vem do __motion
|
|
15
|
+
(cópias a no máximo `--gap` px uma da outra, entre --smin e --smax); sem __motion, --samples. Se um único passo entre
|
|
16
|
+
capturas vizinhas concentra a mudança (corte seco, letra que aparece), só se média o lado do corte que contém o
|
|
17
|
+
instante do quadro: corte continua corte, não vira dissolve. Vários Chromium em paralelo (--workers).
|
|
18
|
+
Método do onetake (05-pesquisa/referencias/2026-10-12-onetake-take-unico.md), código nosso.
|
|
19
|
+
|
|
20
|
+
Velocidade (13/10, medido no Reel Soldier; ver 07-videos/projetos/2026-10-13-video-mais-rapido/README.md):
|
|
21
|
+
captura pelo CDP com optimizeForSpeed (PNG sem perda, mesmos pixels), Chromium na GPU (padrão; --sem-gpu volta ao
|
|
22
|
+
SwiftShader), --previa (sem obturador, JPEG, resolução cheia: 62 s em ~22 s) e --trechos N (hash de cada quadro numa
|
|
23
|
+
passada rápida na CPU; só os trechos que mudaram são refeitos, o resto vem do cache e é emendado sem recodificar).
|
|
24
|
+
"""
|
|
25
|
+
import argparse, base64, hashlib, io, json, math, multiprocessing as mp, os, shutil, subprocess, sys, time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
import numpy as np
|
|
28
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
29
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
_u = np.arange(256) / 255.0
|
|
32
|
+
S2L = np.where(_u <= 0.04045, _u / 12.92, ((_u + 0.055) / 1.055) ** 2.4).astype(np.float32)
|
|
33
|
+
_l = np.arange(65536) / 65535.0
|
|
34
|
+
L2S = np.clip(np.rint(np.where(_l <= 0.0031308, _l * 12.92, 1.055 * np.power(_l, 1 / 2.4) - 0.055) * 255), 0, 255).astype(np.uint8)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def url_de(comp):
|
|
38
|
+
path, _, q = comp.partition("?")
|
|
39
|
+
return Path(path).resolve().as_uri() + ("?" + q if q else "")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def lado_do_corte(pngs):
|
|
43
|
+
"""Índices das capturas a integrar: todas, a não ser que um passo carregue a mudança (≥ 3× qualquer outro)."""
|
|
44
|
+
n = len(pngs)
|
|
45
|
+
if n < 4:
|
|
46
|
+
return list(range(n))
|
|
47
|
+
small = [np.asarray(Image.open(io.BytesIO(p)).convert("L").reduce(8), np.int16) for p in pngs]
|
|
48
|
+
d = np.array([np.abs(small[k + 1] - small[k]).mean() for k in range(n - 1)])
|
|
49
|
+
j = int(d.argmax())
|
|
50
|
+
if d[j] <= 0 or d[j] < 3 * np.delete(d, j).max():
|
|
51
|
+
return list(range(n))
|
|
52
|
+
return list(range(j + 1, n)) if j < n // 2 else list(range(j + 1))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def integrar(pngs):
|
|
56
|
+
keep = lado_do_corte(pngs)
|
|
57
|
+
acc = None
|
|
58
|
+
for k in keep:
|
|
59
|
+
lin = S2L[np.asarray(Image.open(io.BytesIO(pngs[k])).convert("RGB"))]
|
|
60
|
+
acc = lin if acc is None else acc + lin
|
|
61
|
+
return L2S[np.clip(np.rint(acc * (65535.0 / len(keep))), 0, 65535).astype(np.uint16)], len(keep) < len(pngs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
GPU_ARGS = ["--enable-gpu", "--use-angle=d3d11", "--ignore-gpu-blocklist"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def abrir(p, a):
|
|
68
|
+
args = ["--force-color-profile=srgb", "--disable-lcd-text", "--font-render-hinting=none"]
|
|
69
|
+
br = p.chromium.launch(args=args + (GPU_ARGS if a.get("gpu") else []))
|
|
70
|
+
pg = br.new_context(viewport={"width": a["w"], "height": a["h"]}, device_scale_factor=a["scale"]).new_page()
|
|
71
|
+
erros = []
|
|
72
|
+
pg.on("pageerror", lambda e: erros.append(str(e)))
|
|
73
|
+
pg.on("console", lambda m: erros.append(m.text) if m.type == "error" else None)
|
|
74
|
+
pg.goto(url_de(a["comp"]))
|
|
75
|
+
pg.evaluate("window.__ready")
|
|
76
|
+
pg._cdp = pg.context.new_cdp_session(pg)
|
|
77
|
+
return br, pg, erros
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def capturar(pg, a):
|
|
81
|
+
"""PNG sem perda (padrão) ou JPEG (prévia), direto pelo CDP com optimizeForSpeed. Com escala < 1 o CDP
|
|
82
|
+
devolveria a imagem no tamanho cheio: o clip com a mesma escala mantém o tamanho real (ex.: 270×480 a 0,25)."""
|
|
83
|
+
if a["captura"] == "playwright":
|
|
84
|
+
return pg.screenshot(type="png")
|
|
85
|
+
opts = {"format": "jpeg", "quality": 90} if a["captura"] == "jpeg" else {"format": "png"}
|
|
86
|
+
if a["scale"] != 1:
|
|
87
|
+
opts["clip"] = {"x": 0, "y": 0, "width": a["w"], "height": a["h"], "scale": a["scale"]}
|
|
88
|
+
return base64.b64decode(pg._cdp.send("Page.captureScreenshot", dict(opts, optimizeForSpeed=True))["data"])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def trabalhador(a):
|
|
92
|
+
from playwright.sync_api import sync_playwright
|
|
93
|
+
st = {"capturas": 0, "parados": 0, "cortes": 0, "erros": [], "hist": {}}
|
|
94
|
+
fps, dur, aberto = a["fps"], a["dur"], (a["shutter"] / 360.0) / a["fps"]
|
|
95
|
+
with sync_playwright() as p:
|
|
96
|
+
br, pg, erros = abrir(p, a)
|
|
97
|
+
adapt = a["shutter"] > 0 and pg.evaluate("typeof window.__motion === 'function'")
|
|
98
|
+
|
|
99
|
+
def grab(t):
|
|
100
|
+
pg.evaluate(f"window.__seek({t:.6f})")
|
|
101
|
+
st["capturas"] += 1
|
|
102
|
+
return capturar(pg, a)
|
|
103
|
+
|
|
104
|
+
feitos = 0
|
|
105
|
+
quadros = a["lista"][a["k"]::a["n"]] if a.get("lista") else range(a["f0"] + a["k"], a["f1"], a["n"])
|
|
106
|
+
for f in quadros:
|
|
107
|
+
if feitos and feitos % 200 == 0: # Chrome que tira milhares de capturas cresce; recicla
|
|
108
|
+
br.close(); br, pg, erros2 = abrir(p, a); erros += erros2
|
|
109
|
+
feitos += 1
|
|
110
|
+
t, out = f / fps, os.path.join(a["dir"], f"{f:05d}.{a['ext']}")
|
|
111
|
+
if a.get("retomar") and os.path.exists(out) and os.path.getsize(out) > 0:
|
|
112
|
+
continue
|
|
113
|
+
if a["shutter"] <= 0:
|
|
114
|
+
open(out, "wb").write(grab(t)); continue
|
|
115
|
+
S = a["samples"]
|
|
116
|
+
if adapt:
|
|
117
|
+
try:
|
|
118
|
+
far = pg.evaluate(f"window.__motion({max(t - aberto / 2, 0):.6f}, {min(t + aberto / 2, dur - 1e-6):.6f})")
|
|
119
|
+
S = min(a["smax"], max(a["smin"], math.ceil(far * a["scale"] / a["gap"])))
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
ts = [min(max(t + aberto * ((i + 0.5) / S - 0.5), 0.0), dur - 1e-6) for i in range(S)]
|
|
123
|
+
ini, fim = grab(ts[0]), grab(ts[-1])
|
|
124
|
+
if ini == fim:
|
|
125
|
+
open(out, "wb").write(ini); st["parados"] += 1; continue
|
|
126
|
+
pngs = [ini] + [grab(x) for x in ts[1:-1]] + [fim]
|
|
127
|
+
img, corte = integrar(pngs)
|
|
128
|
+
st["cortes"] += corte
|
|
129
|
+
if a["ext"] == "jpg":
|
|
130
|
+
Image.fromarray(img).save(out, quality=92)
|
|
131
|
+
else:
|
|
132
|
+
Image.fromarray(img).save(out, compress_level=1)
|
|
133
|
+
st["hist"][S] = st["hist"].get(S, 0) + 1
|
|
134
|
+
br.close()
|
|
135
|
+
st["erros"] = erros[:5]
|
|
136
|
+
return st
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def trabalhador_seguro(a):
|
|
140
|
+
"""O navegador (ou o driver do Playwright) às vezes cai num render longo: reabre e continua de onde parou."""
|
|
141
|
+
for tentativa in range(4):
|
|
142
|
+
# a partir da 3ª tentativa sem GPU: "Unable to capture screenshot" da GPU às vezes se repete (13/10); na CPU nunca falhou
|
|
143
|
+
gpu = a.get("gpu") and tentativa < 2
|
|
144
|
+
try:
|
|
145
|
+
return trabalhador(dict(a, retomar=a.get("retomar") or tentativa > 0, gpu=gpu))
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"navegador {a['k']} caiu ({type(e).__name__}: {str(e)[:80]}), tentativa {tentativa + 2}"
|
|
148
|
+
+ ("" if tentativa < 1 or not a.get("gpu") else " sem GPU"), flush=True)
|
|
149
|
+
time.sleep(2)
|
|
150
|
+
raise RuntimeError(f"navegador {a['k']} caiu 4 vezes")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def info(a):
|
|
154
|
+
from playwright.sync_api import sync_playwright
|
|
155
|
+
with sync_playwright() as p:
|
|
156
|
+
br, pg, erros = abrir(p, a)
|
|
157
|
+
m = pg.evaluate("window.__meta || {}")
|
|
158
|
+
br.close()
|
|
159
|
+
return m, erros
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def stills(a, times, out):
|
|
163
|
+
from playwright.sync_api import sync_playwright
|
|
164
|
+
ims = []
|
|
165
|
+
with sync_playwright() as p:
|
|
166
|
+
br, pg, erros = abrir(p, a)
|
|
167
|
+
for t in times:
|
|
168
|
+
pg.evaluate(f"window.__seek({t})")
|
|
169
|
+
ims.append(Image.open(io.BytesIO(pg.screenshot(type="png"))).convert("RGB"))
|
|
170
|
+
br.close()
|
|
171
|
+
cols = min(len(ims), a.get("cols") or 6)
|
|
172
|
+
th = 640; tw = round(ims[0].width * th / ims[0].height)
|
|
173
|
+
rows = math.ceil(len(ims) / cols)
|
|
174
|
+
folha = Image.new("RGB", (cols * tw, rows * (th + 34)), (24, 24, 24))
|
|
175
|
+
dr = ImageDraw.Draw(folha)
|
|
176
|
+
try:
|
|
177
|
+
fonte = ImageFont.truetype("arial.ttf", 24)
|
|
178
|
+
except OSError:
|
|
179
|
+
fonte = ImageFont.load_default()
|
|
180
|
+
for i, (t, im) in enumerate(zip(times, ims)):
|
|
181
|
+
x, y = (i % cols) * tw, (i // cols) * (th + 34)
|
|
182
|
+
folha.paste(im.resize((tw, th), Image.LANCZOS), (x, y + 34))
|
|
183
|
+
dr.text((x + 8, y + 4), f"{t:.2f} s", fill=(255, 220, 90), font=fonte)
|
|
184
|
+
folha.save(out)
|
|
185
|
+
print("folha:", out, "erros:", erros[:3] or "nenhum")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def commit_livre_gb():
|
|
189
|
+
"""Memória que o Windows ainda pode comprometer (RAM + arquivo de paginação). Sem isso o render derruba o PC."""
|
|
190
|
+
try:
|
|
191
|
+
import ctypes
|
|
192
|
+
class M(ctypes.Structure):
|
|
193
|
+
_fields_ = [("a", ctypes.c_ulong), ("b", ctypes.c_ulong)] + [(n, ctypes.c_ulonglong) for n in "cdefghi"]
|
|
194
|
+
m = M(); m.a = ctypes.sizeof(M)
|
|
195
|
+
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m))
|
|
196
|
+
return m.f / 2**30
|
|
197
|
+
except Exception:
|
|
198
|
+
return 999.0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
GB_POR_NAVEGADOR, GB_RESERVA = 1.8, 6.0 # medido em 13/10: 8 navegadores no final a 60 qps = 14 GB
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def renderizar(job, f0, f1, pasta, lista=None):
|
|
205
|
+
"""Quadros f0..f1-1 (ou só os de `lista`) em `pasta` (nome = número absoluto do quadro), em job['n'] navegadores."""
|
|
206
|
+
pasta.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
n = max(1, min(job["n"], len(lista) if lista else f1 - f0))
|
|
208
|
+
with mp.get_context("spawn").Pool(n) as pool:
|
|
209
|
+
res = pool.map(trabalhador_seguro, [dict(job, dir=str(pasta), f0=f0, f1=f1, n=n, k=k, lista=lista) for k in range(n)])
|
|
210
|
+
tot = {"capturas": 0, "parados": 0, "cortes": 0, "hist": {}, "erros": []}
|
|
211
|
+
for r in res:
|
|
212
|
+
for k in ("capturas", "parados", "cortes"):
|
|
213
|
+
tot[k] += r[k]
|
|
214
|
+
for k, v in r["hist"].items():
|
|
215
|
+
tot["hist"][k] = tot["hist"].get(k, 0) + v
|
|
216
|
+
tot["erros"] += r["erros"]
|
|
217
|
+
return tot
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def codificar(pasta, f0, f1, ext, fps, out, crf, preset, audio=None, de=0.0):
|
|
221
|
+
seq = pasta / "_seq"
|
|
222
|
+
shutil.rmtree(seq, ignore_errors=True); seq.mkdir()
|
|
223
|
+
for i, f in enumerate(range(f0, f1)): # renumera a partir de 0 para o ffmpeg (sem mover o original)
|
|
224
|
+
src = pasta / f"{f:05d}.{ext}"
|
|
225
|
+
try:
|
|
226
|
+
os.link(src, seq / f"q{i:05d}.{ext}")
|
|
227
|
+
except OSError:
|
|
228
|
+
shutil.copy2(src, seq / f"q{i:05d}.{ext}")
|
|
229
|
+
cmd = ["ffmpeg", "-v", "error", "-y", "-framerate", str(fps), "-i", str(seq / f"q%05d.{ext}")]
|
|
230
|
+
if audio:
|
|
231
|
+
cmd += ["-ss", f"{de:.3f}", "-i", audio]
|
|
232
|
+
cmd += ["-c:v", "libx264", "-preset", preset, "-crf", str(crf), "-pix_fmt", "yuv420p", "-movflags", "+faststart",
|
|
233
|
+
"-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709"]
|
|
234
|
+
if audio:
|
|
235
|
+
cmd += ["-af", "apad", "-c:a", "aac", "-b:a", "256k", "-shortest"] # apad: a voz acabar antes não corta o vídeo
|
|
236
|
+
subprocess.run(cmd + [str(out)], check=True)
|
|
237
|
+
shutil.rmtree(seq, ignore_errors=True)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
DIGITAIS_N = 8 # sempre o mesmo: o Chromium guarda estado entre seeks e outro nº de navegadores muda ~6% dos quadros
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def digitais(job, f0, f1, pasta):
|
|
244
|
+
"""Impressão digital de cada quadro (hash), para saber o que mudou desde o último render. Captura em escala 1,
|
|
245
|
+
na CPU, JPEG, sem obturador: assim o Chromium desenha o mesmo quadro bit a bit (medido em 13/10: 0 de 160
|
|
246
|
+
diferentes). Na GPU (157 de 1.200) e em escala reduzida (11 de 160) o mesmo quadro varia entre passadas, e com
|
|
247
|
+
outro número de navegadores também (226 de 3.720): por isso DIGITAIS_N fixo."""
|
|
248
|
+
shutil.rmtree(pasta, ignore_errors=True)
|
|
249
|
+
n = DIGITAIS_N
|
|
250
|
+
cabe = max(2, int((commit_livre_gb() - GB_RESERVA) / 1.3))
|
|
251
|
+
if cabe < n:
|
|
252
|
+
print(f"memória curta: digitais em {cabe} navegadores (o cache pode refazer trechos que não mudaram)", flush=True)
|
|
253
|
+
n = cabe
|
|
254
|
+
renderizar(dict(job, n=n, scale=1, shutter=0, captura="jpeg", ext="jpg", retomar=False, gpu=False), f0, f1, pasta)
|
|
255
|
+
out = {f: hashlib.sha1((pasta / f"{f:05d}.jpg").read_bytes()).hexdigest()[:16] for f in range(f0, f1)}
|
|
256
|
+
shutil.rmtree(pasta, ignore_errors=True)
|
|
257
|
+
return out
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main():
|
|
261
|
+
ap = argparse.ArgumentParser()
|
|
262
|
+
ap.add_argument("comp"); ap.add_argument("--out", required=True)
|
|
263
|
+
ap.add_argument("--fps", type=int, default=30); ap.add_argument("--scale", type=float, default=None)
|
|
264
|
+
ap.add_argument("--shutter", type=float, default=None); ap.add_argument("--samples", type=int, default=8)
|
|
265
|
+
ap.add_argument("--gap", type=float, default=18); ap.add_argument("--smin", type=int, default=3); ap.add_argument("--smax", type=int, default=16)
|
|
266
|
+
ap.add_argument("--workers", type=int, default=None)
|
|
267
|
+
ap.add_argument("--de", type=float, default=0); ap.add_argument("--ate", type=float, default=None)
|
|
268
|
+
ap.add_argument("--audio"); ap.add_argument("--crf", type=int, default=None)
|
|
269
|
+
ap.add_argument("--stills"); ap.add_argument("--cols", type=int, default=6)
|
|
270
|
+
ap.add_argument("--manter-quadros", action="store_true")
|
|
271
|
+
ap.add_argument("--retomar", action="store_true", help="mantém os quadros de um render que caiu e faz só os que faltam")
|
|
272
|
+
ap.add_argument("--previa", action="store_true", help="rápida: sem obturador (sem desfoque de movimento), JPEG; para ver, não para entregar")
|
|
273
|
+
ap.add_argument("--turbo", action="store_true", help="usa o PC inteiro (mais navegadores); para o render final")
|
|
274
|
+
ap.add_argument("--trechos", type=float, default=0, help="segundos por trecho: refaz só os trechos que mudaram (cache)")
|
|
275
|
+
ap.add_argument("--refazer", help="com --trechos: 'a,b' em segundos = o que você mexeu; pula as digitais e refaz só esses trechos")
|
|
276
|
+
ap.add_argument("--sem-gpu", action="store_true", help="Chromium sem GPU (SwiftShader), como antes de 13/10")
|
|
277
|
+
ap.add_argument("--captura", choices=["png", "jpeg", "playwright"], default=None)
|
|
278
|
+
a = ap.parse_args()
|
|
279
|
+
cpu = os.cpu_count() or 4
|
|
280
|
+
if a.previa:
|
|
281
|
+
scale, shutter, captura, crf, preset = a.scale or 1, 0 if a.shutter is None else a.shutter, a.captura or "jpeg", a.crf or 23, "veryfast"
|
|
282
|
+
else:
|
|
283
|
+
scale, shutter, captura, crf, preset = a.scale or 1, 180 if a.shutter is None else a.shutter, a.captura or "png", a.crf or 16, "medium"
|
|
284
|
+
workers = a.workers or (12 if a.turbo else min(10, cpu - 2)) # acima de ~12 não rende mais (medido) e alguns travam
|
|
285
|
+
livre = commit_livre_gb()
|
|
286
|
+
cabe = max(2, int((livre - GB_RESERVA) / GB_POR_NAVEGADOR))
|
|
287
|
+
if workers > cabe:
|
|
288
|
+
print(f"memória: {livre:.0f} GB livres → {cabe} navegadores em vez de {workers}", flush=True)
|
|
289
|
+
workers = cabe
|
|
290
|
+
ext = "jpg" if captura == "jpeg" else "png"
|
|
291
|
+
base = dict(comp=a.comp, scale=scale, w=1080, h=1920, cols=a.cols, gpu=not a.sem_gpu, captura=captura, ext=ext)
|
|
292
|
+
meta, erros = info(base)
|
|
293
|
+
base.update(w=int(meta.get("w", 1080)), h=int(meta.get("h", 1920)))
|
|
294
|
+
if a.stills:
|
|
295
|
+
return stills(base, [float(x) for x in a.stills.split(",")], a.out)
|
|
296
|
+
dur = float(meta.get("dur", 10)); ate = a.ate if a.ate is not None else dur
|
|
297
|
+
out = Path(a.out).resolve(); out.parent.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
f0, f1 = round(a.de * a.fps), round(ate * a.fps)
|
|
299
|
+
job = dict(base, fps=a.fps, dur=dur, n=workers, shutter=shutter, samples=a.samples, gap=a.gap, smin=a.smin, smax=a.smax, retomar=a.retomar)
|
|
300
|
+
modo = "prévia" if a.previa else ("turbo" if a.turbo else "normal")
|
|
301
|
+
print(f"{f1 - f0} quadros · {modo} · {workers} navegadores · escala {scale:g} · obturador {shutter:g}° · captura {captura}"
|
|
302
|
+
f" · GPU {'sim' if base['gpu'] else 'não'} · erros na carga: {erros[:2] or 'nenhum'}", flush=True)
|
|
303
|
+
t0 = time.time()
|
|
304
|
+
rel = {"fps": a.fps, "shutter": shutter, "scale": scale, "captura": captura, "gpu": base["gpu"], "workers": workers,
|
|
305
|
+
"modo": modo, "quadros": f1 - f0}
|
|
306
|
+
|
|
307
|
+
if a.trechos > 0:
|
|
308
|
+
N = max(1, round(a.trechos * a.fps))
|
|
309
|
+
cfg = f"{a.fps}qps-{scale:g}x-obt{shutter:g}-{captura}-crf{crf}-{base['w']}x{base['h']}"
|
|
310
|
+
cache = out.parent / "_trechos" / cfg
|
|
311
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
segs = [(max(s, f0), min(s + N, f1)) for s in range((f0 // N) * N, f1, N)]
|
|
313
|
+
if a.refazer: # atalho: quem mexeu diz onde; sem digitais (o .sha desses trechos fica sem valer)
|
|
314
|
+
ra, rb = (float(x) for x in a.refazer.split(","))
|
|
315
|
+
fa, fb = math.floor(ra * a.fps), math.ceil(rb * a.fps)
|
|
316
|
+
dig = None
|
|
317
|
+
else:
|
|
318
|
+
dig = digitais(job, f0, f1, out.parent / ("_digitais-" + out.stem))
|
|
319
|
+
t_dig = time.time() - t0
|
|
320
|
+
tot, lista, mudou = {"capturas": 0, "parados": 0, "cortes": 0}, [], []
|
|
321
|
+
for s, e in segs:
|
|
322
|
+
mp4, sha = cache / f"t{s:05d}-{e:05d}.mp4", cache / f"t{s:05d}-{e:05d}.sha"
|
|
323
|
+
lista.append(mp4)
|
|
324
|
+
tem = mp4.exists() and mp4.stat().st_size > 0
|
|
325
|
+
if dig is None:
|
|
326
|
+
if not tem or (e > fa and s <= fb):
|
|
327
|
+
mudou.append((s, e, mp4))
|
|
328
|
+
sha.unlink(missing_ok=True)
|
|
329
|
+
sha.with_suffix(".novo").write_text("")
|
|
330
|
+
continue
|
|
331
|
+
novo = hashlib.sha1("|".join(dig[f] for f in range(s, e)).encode()).hexdigest()
|
|
332
|
+
if not (tem and sha.exists() and sha.read_text().strip() == novo):
|
|
333
|
+
mudou.append((s, e, mp4))
|
|
334
|
+
sha.with_suffix(".novo").write_text(novo)
|
|
335
|
+
feitos, reaproveitados = len(mudou), len(segs) - len(mudou)
|
|
336
|
+
print(f"{len(segs)} trechos de {a.trechos:g} s · {feitos} mudaram, {reaproveitados} vêm do cache "
|
|
337
|
+
f"(digitais em {t_dig:.0f} s)", flush=True)
|
|
338
|
+
pasta = cache / "_quadros"
|
|
339
|
+
lote = []
|
|
340
|
+
for i, (s, e, mp4) in enumerate(mudou): # lotes de até ~900 quadros: um pool só por lote
|
|
341
|
+
lote.append((s, e, mp4))
|
|
342
|
+
if i == len(mudou) - 1 or sum(b - a_ for a_, b, _ in lote) + (mudou[i + 1][1] - mudou[i + 1][0]) > 900:
|
|
343
|
+
shutil.rmtree(pasta, ignore_errors=True)
|
|
344
|
+
r = renderizar(job, 0, 0, pasta, lista=[f for s_, e_, _ in lote for f in range(s_, e_)])
|
|
345
|
+
for k in tot:
|
|
346
|
+
tot[k] += r[k]
|
|
347
|
+
for s_, e_, mp4_ in lote:
|
|
348
|
+
codificar(pasta, s_, e_, ext, a.fps, mp4_, crf, preset)
|
|
349
|
+
nv = mp4_.with_suffix(".novo")
|
|
350
|
+
if nv.read_text():
|
|
351
|
+
os.replace(nv, mp4_.with_suffix(".sha")) # só depois do mp4 pronto
|
|
352
|
+
else:
|
|
353
|
+
nv.unlink()
|
|
354
|
+
lote = []
|
|
355
|
+
shutil.rmtree(pasta, ignore_errors=True)
|
|
356
|
+
lst = cache / "_lista.txt"
|
|
357
|
+
lst.write_text("".join(f"file '{m.as_posix()}'\n" for m in lista), encoding="utf-8")
|
|
358
|
+
tmp = out.with_name(out.stem + "._junta.mp4")
|
|
359
|
+
subprocess.run(["ffmpeg", "-v", "error", "-y", "-f", "concat", "-safe", "0", "-i", str(lst), "-c", "copy", str(tmp)], check=True)
|
|
360
|
+
cmd = ["ffmpeg", "-v", "error", "-y", "-i", str(tmp)]
|
|
361
|
+
if a.audio:
|
|
362
|
+
cmd += ["-ss", f"{a.de:.3f}", "-i", a.audio, "-map", "0:v", "-map", "1:a", "-af", "apad", "-c:a", "aac", "-b:a", "256k", "-shortest"]
|
|
363
|
+
cmd += ["-c:v", "copy", "-movflags", "+faststart"]
|
|
364
|
+
subprocess.run(cmd + [str(out)], check=True)
|
|
365
|
+
tmp.unlink()
|
|
366
|
+
seg = time.time() - t0
|
|
367
|
+
print(f"{len(segs)} trechos de {a.trechos:g} s: {feitos} refeitos, {reaproveitados} do cache · digitais {t_dig:.0f} s · total {seg:.0f} s")
|
|
368
|
+
rel.update(trechos=len(segs), refeitos=feitos, reaproveitados=reaproveitados, segundos_digitais=round(t_dig, 1), **tot)
|
|
369
|
+
else:
|
|
370
|
+
fr = out.parent / ("_quadros-" + out.stem)
|
|
371
|
+
if not a.retomar:
|
|
372
|
+
shutil.rmtree(fr, ignore_errors=True)
|
|
373
|
+
tot = renderizar(job, f0, f1, fr)
|
|
374
|
+
print(f"{tot['capturas']} capturas, {tot['parados']} quadros parados, {tot['cortes']} cortes mantidos secos, "
|
|
375
|
+
f"{time.time() - t0:.0f} s; erros: {tot['erros'][:3] or 'nenhum'}")
|
|
376
|
+
codificar(fr, f0, f1, ext, a.fps, out, crf, preset, a.audio, a.de)
|
|
377
|
+
if not a.manter_quadros:
|
|
378
|
+
shutil.rmtree(fr, ignore_errors=True)
|
|
379
|
+
seg = time.time() - t0
|
|
380
|
+
rel.update(capturas=tot["capturas"], parados=tot["parados"], cortes=tot["cortes"],
|
|
381
|
+
amostras_por_quadro={str(k): tot["hist"][k] for k in sorted(tot["hist"])})
|
|
382
|
+
rel["segundos"] = round(seg, 1)
|
|
383
|
+
json.dump(rel, open(out.with_suffix(".render.json"), "w"), indent=1)
|
|
384
|
+
print(f"ok: {out} ({seg:.0f} s)")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
main()
|