primocode 10.13.0 → 10.14.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.
Files changed (195) hide show
  1. package/README.md +66 -12
  2. package/bin/primocode.js +148 -8
  3. package/lib/act.js +48 -1
  4. package/lib/catalogo.js +12 -1
  5. package/lib/config.js +1 -0
  6. package/lib/estudio/cota.js +150 -0
  7. package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
  8. package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
  9. package/lib/estudio/dados/projetos/clube-agent.json +120 -0
  10. package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
  11. package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
  12. package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
  13. package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
  14. package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
  15. package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
  16. package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
  17. package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
  18. package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
  19. package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
  20. package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
  21. package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
  22. package/lib/estudio/dados/projetos/clube-clube.json +108 -0
  23. package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
  24. package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
  25. package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
  26. package/lib/estudio/dados/projetos/clube-dub.json +119 -0
  27. package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
  28. package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
  29. package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
  30. package/lib/estudio/dados/projetos/clube-frame.json +108 -0
  31. package/lib/estudio/dados/projetos/clube-glass.json +118 -0
  32. package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
  33. package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
  34. package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
  35. package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
  36. package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
  37. package/lib/estudio/dados/projetos/clube-morph.json +119 -0
  38. package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
  39. package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
  40. package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
  41. package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
  42. package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
  43. package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
  44. package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
  45. package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
  46. package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
  47. package/lib/estudio/dados/projetos/clube-promo.json +114 -0
  48. package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
  49. package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
  50. package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
  51. package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
  52. package/lib/estudio/dados/projetos/clube-robo.json +125 -0
  53. package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
  54. package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
  55. package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
  56. package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
  57. package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
  58. package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
  59. package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
  60. package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
  61. package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
  62. package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
  63. package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
  64. package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
  65. package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
  66. package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
  67. package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
  68. package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
  69. package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
  70. package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
  71. package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
  72. package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
  73. package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
  74. package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
  75. package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
  76. package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
  77. package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
  78. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
  79. package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
  80. package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
  81. package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
  82. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
  83. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
  84. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
  85. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
  86. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
  87. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
  88. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
  89. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
  90. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
  91. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
  92. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
  93. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
  94. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
  95. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
  96. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
  97. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
  98. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
  99. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
  100. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
  101. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
  102. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
  103. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
  104. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
  105. package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
  106. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
  107. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
  108. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
  109. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
  110. package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
  111. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
  112. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
  113. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
  114. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
  115. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
  116. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
  117. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
  118. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
  119. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
  120. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
  121. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
  122. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
  123. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
  124. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
  125. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
  126. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
  127. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
  128. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
  129. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
  130. package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
  131. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
  132. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
  133. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
  134. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
  135. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
  136. package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
  137. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
  138. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
  139. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
  140. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
  141. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
  142. package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
  143. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
  144. package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
  145. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
  146. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
  147. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
  148. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
  149. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
  150. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
  151. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
  152. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
  153. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
  154. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
  155. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
  156. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
  157. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
  158. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
  159. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
  160. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
  161. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
  162. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
  163. package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
  164. package/lib/estudio/editor.js +285 -0
  165. package/lib/estudio/effectcraft.js +152 -0
  166. package/lib/estudio/exportar.js +181 -0
  167. package/lib/estudio/index.js +90 -2
  168. package/lib/estudio/kit-ae/base.jsx +1146 -0
  169. package/lib/estudio/kit-ae/cenas.jsx +395 -0
  170. package/lib/estudio/kit-ae/primitivas.jsx +911 -0
  171. package/lib/estudio/maquina-diretor.js +393 -0
  172. package/lib/estudio/maquina-elementos.js +134 -0
  173. package/lib/estudio/maquina-layout.js +198 -0
  174. package/lib/estudio/maquina-som.js +478 -0
  175. package/lib/estudio/maquina-trilha.js +160 -0
  176. package/lib/estudio/maquina-voz.js +429 -0
  177. package/lib/estudio/maquina.js +516 -0
  178. package/lib/estudio/projetos.js +60 -0
  179. package/lib/headless.js +71 -0
  180. package/lib/hooks.js +72 -0
  181. package/lib/local/cerebro.json +398 -12
  182. package/lib/local/entrada.js +46 -2
  183. package/lib/marca-kit.js +309 -0
  184. package/lib/mcp.js +286 -6
  185. package/lib/perigo.js +31 -3
  186. package/lib/planos.js +47 -19
  187. package/lib/preparar.js +111 -0
  188. package/lib/retomar.js +55 -0
  189. package/lib/sandbox.js +197 -0
  190. package/lib/skills.js +3 -1
  191. package/lib/tools.js +13 -4
  192. package/lib/ui.js +1 -1
  193. package/package.json +4 -2
  194. package/skills/after-effects/SKILL.md +52 -0
  195. 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()