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.
Files changed (245) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +74 -9
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +38 -8
  198. package/lib/app-chat.js +97 -0
  199. package/lib/catalogo.js +7 -1
  200. package/lib/chamada-torta.js +81 -0
  201. package/lib/editor-pro.js +786 -0
  202. package/lib/entrega.js +58 -0
  203. package/lib/estudio/conferencia-visual.js +76 -0
  204. package/lib/estudio/diretor.js +6 -1
  205. package/lib/estudio/exportar.js +3 -1
  206. package/lib/estudio/gosto/tela.html +1349 -0
  207. package/lib/estudio/gosto.js +305 -0
  208. package/lib/estudio/index.js +12 -1
  209. package/lib/estudio/kit-ae/base.jsx +119 -13
  210. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  211. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  212. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  213. package/lib/estudio/maquina-diretor.js +70 -12
  214. package/lib/estudio/maquina-foto.js +177 -0
  215. package/lib/estudio/maquina-guarda.js +207 -0
  216. package/lib/estudio/maquina-layout.js +22 -1
  217. package/lib/estudio/maquina.js +64 -11
  218. package/lib/estudio/referencias.js +1 -1
  219. package/lib/estudio/visao/visao.swift +97 -0
  220. package/lib/estudio/visao.js +60 -0
  221. package/lib/headless.js +15 -3
  222. package/lib/local/cerebro.json +285 -7
  223. package/lib/local/motor.js +5 -2
  224. package/lib/memoria-usuario.js +164 -0
  225. package/lib/nuvem.js +33 -2
  226. package/lib/passagem.js +50 -0
  227. package/lib/perigo.js +19 -11
  228. package/lib/protecao.js +134 -0
  229. package/lib/repeticao.js +1 -1
  230. package/lib/sandbox.js +57 -9
  231. package/lib/titulo-conversa.js +46 -0
  232. package/lib/tools.js +23 -4
  233. package/lib/ui.js +2 -2
  234. package/package.json +3 -2
  235. package/skills/editar-meu-video/SKILL.md +6 -2
  236. package/skills/editar-video/SKILL.md +113 -0
  237. package/studio/pessoa.py +52 -0
  238. package/voz/primo_voz/__main__.py +55 -40
  239. package/voz/primo_voz/app.py +500 -4
  240. package/voz/primo_voz/chat.html +1773 -0
  241. package/voz/primo_voz/conversa.py +0 -26
  242. package/voz/primo_voz/hud.html +8 -2
  243. package/voz/primo_voz/hud.py +129 -5
  244. package/voz/primo_voz/identidade.py +1 -1
  245. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,298 @@
1
+ """The face inside each take, 10 times a second, smoothed: the path the framing follows (`acompanhar` blocks).
2
+
3
+ face_layout.json keeps one face per take, so the framing stands still inside a take. When the person leans,
4
+ walks or drifts inside a take and the block is zoomed (moldura, a punch-in, tela cheia at 1.2), the face slides
5
+ to the border or the forehead goes. This helper measures the face with the same YuNet detector as
6
+ face_layout.py, 10 times a second on the cut, and writes the smoothed path that modelo-projeto/build.py follows:
7
+
8
+ 1. raw measure every 0.1 s (the largest confident face, as face_layout.py);
9
+ 2. median of 5 measures in a row (a single wrong detection does not move anything);
10
+ 3. lost face (hand in front, turned aside): the last good point is held. After a loss of 0.3 s or more, a new
11
+ detection is only trusted when 2 of the next 3 samples also find the face;
12
+ 4. dead zone: the target moves only when the centre walks more than 3% of the video width (or the face size
13
+ changes more than 12%); micro movements and detector jitter never move the frame;
14
+ 5. every change of target becomes a sine.inOut move of 0.5 s centred on the change (the path is continuous,
15
+ no jump);
16
+ 6. everything restarts at each take of cut_timeline.json (one take's path never leaks into the next).
17
+
18
+ Output <edit>/rosto_trilha.json (fractions of the cut.mp4 frame; times in seconds of the cut):
19
+ {"versao": 1, "video": "cut.mp4", "video_sig": "size:mtime_ns", "largura", "altura", "fps", "passo_s": 0.1,
20
+ "parametros": {...},
21
+ "tomadas": [{"id", "de", "ate", "amostras", "medidos", "w_med", "h_med",
22
+ "pontos": [[t, cx, cy, w, h, medido], ...]}]} # medido: 1 = face found near this sample
23
+ A take with no face at all has "pontos": [] (the build keeps the take's framing and says so).
24
+
25
+ The build runs this by itself when a block asks for `"acompanhar": true` and the file is missing or older than the
26
+ cut. By hand:
27
+ uv run --directory '<SKILL>' python helpers/rastrear_rosto.py '<EDIT>' [--forcar]
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import math
34
+ import subprocess
35
+ import sys
36
+ import time
37
+ from pathlib import Path
38
+
39
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
40
+ from common import file_signature, load_json, now_iso, probe_video, require_binary, save_json # noqa: E402
41
+
42
+ VERSAO = 1
43
+ PASSO_S = 0.1 # 10 measures a second
44
+ JANELA = 5 # median of 5 measures in a row
45
+ ZONA_MORTA = 0.03 # the target moves when the centre walks more than 3% of the width
46
+ ZONA_TAMANHO = 0.12 # ... or the face width changes more than 12%
47
+ TRANSICAO_S = 0.5 # each move of the target: sine.inOut of 0.5 s, centred on the change
48
+ PERDA_LONGA = 3 # samples without a face that count as "lost" (0.3 s)
49
+ CONFIRMA = 2 # after a long loss: 2 of the next 3 samples must find the face
50
+ LADO_DETECCAO = 540 # frames are scaled to this short side before detection (same as face_layout.py)
51
+
52
+
53
+ class TrilhaErro(Exception):
54
+ pass
55
+
56
+
57
+ # ---------------------------------------------------------------- smoothing (pure: tested without a video)
58
+
59
+ def _mediana(vals: list[float]) -> float:
60
+ v = sorted(vals)
61
+ n = len(v)
62
+ return v[n // 2] if n % 2 else (v[n // 2 - 1] + v[n // 2]) / 2.0
63
+
64
+
65
+ def _suave(u: float) -> float:
66
+ """sine.inOut from 0 to 1"""
67
+ if u <= 0:
68
+ return 0.0
69
+ if u >= 1:
70
+ return 1.0
71
+ return 0.5 - 0.5 * math.cos(math.pi * u)
72
+
73
+
74
+ CHAVES = ("cx", "cy", "w", "h")
75
+
76
+
77
+ def suavizar(amostras: list, largura: int, altura: int, passo: float = PASSO_S) -> list | None:
78
+ """The path of one take. `amostras`: [(t, {"cx", "cy", "w", "h"} or None)] in time order, fractions of the
79
+ frame. Returns [[t, cx, cy, w, h, medido]] (same times) or None when the take has no face at all."""
80
+ n = len(amostras)
81
+ if not n:
82
+ return None
83
+ brutos = [a[1] for a in amostras]
84
+ # 3a. after a long loss, a lone detection is not trusted yet
85
+ confiaveis = list(brutos)
86
+ perdidos = PERDA_LONGA # the start of a take counts as a loss (nothing to hold yet)
87
+ for k in range(n):
88
+ if brutos[k] is None:
89
+ perdidos += 1
90
+ continue
91
+ if perdidos >= PERDA_LONGA:
92
+ achados = sum(1 for j in range(k, min(n, k + 3)) if brutos[j] is not None)
93
+ if achados < min(CONFIRMA, n - k):
94
+ confiaveis[k] = None
95
+ continue
96
+ perdidos = 0
97
+ # 2. median of the trusted measures around each sample
98
+ med = []
99
+ meia = JANELA // 2
100
+ for k in range(n):
101
+ if confiaveis[k] is None:
102
+ med.append(None)
103
+ continue
104
+ viz = [confiaveis[j] for j in range(max(0, k - meia), min(n, k + meia + 1)) if confiaveis[j] is not None]
105
+ med.append({c: _mediana([v[c] for v in viz]) for c in CHAVES})
106
+ bons = [m for m in med if m is not None]
107
+ if not bons:
108
+ return None
109
+ # 3b. hold the last good point (and the first good one before the first detection)
110
+ seguro, ult = [], bons[0]
111
+ for m in med:
112
+ if m is not None:
113
+ ult = m
114
+ seguro.append(ult)
115
+ # 4. dead zone: the target stays until the face really moved
116
+ esc_y = altura / float(largura) # a vertical fraction in widths
117
+ alvo, anc = [], seguro[0]
118
+ for v in seguro:
119
+ if (abs(v["cx"] - anc["cx"]) > ZONA_MORTA or abs(v["cy"] - anc["cy"]) * esc_y > ZONA_MORTA
120
+ or abs(v["w"] / max(anc["w"], 1e-6) - 1) > ZONA_TAMANHO):
121
+ anc = v
122
+ alvo.append(anc)
123
+ # 5. each change of target is an eased move of TRANSICAO_S centred on it
124
+ ts = [a[0] for a in amostras]
125
+ mudancas = [(ts[k], {c: alvo[k][c] - alvo[k - 1][c] for c in CHAVES}) for k in range(1, n) if alvo[k] is not alvo[k - 1]]
126
+ saida = []
127
+ for k in range(n):
128
+ v = dict(alvo[0])
129
+ for tm, d in mudancas:
130
+ e = _suave((ts[k] - (tm - TRANSICAO_S / 2)) / TRANSICAO_S)
131
+ if e <= 0:
132
+ break
133
+ for c in CHAVES:
134
+ v[c] += d[c] * e
135
+ medido = any(brutos[j] is not None for j in range(max(0, k - 1), min(n, k + 2)))
136
+ saida.append([round(ts[k], 3), round(v["cx"], 4), round(v["cy"], 4), round(v["w"], 4), round(v["h"], 4),
137
+ 1 if medido else 0])
138
+ return saida
139
+
140
+
141
+ def tomadas_do_corte(timeline: dict | None, duracao: float) -> list[dict]:
142
+ segs = []
143
+ for i, s in enumerate((timeline or {}).get("segments", []) or []):
144
+ try:
145
+ a, b = float(s["out_start"]), float(s.get("out_end", 0) or 0)
146
+ except (KeyError, TypeError, ValueError):
147
+ continue
148
+ segs.append({"id": str(s.get("id") or f"t{i + 1}"), "de": a, "ate": b})
149
+ segs.sort(key=lambda s: s["de"])
150
+ for k, s in enumerate(segs): # a timeline without out_end: the next take (or the end) closes it
151
+ if s["ate"] <= s["de"]:
152
+ s["ate"] = segs[k + 1]["de"] if k + 1 < len(segs) else duracao
153
+ if not segs:
154
+ segs = [{"id": "t1", "de": 0.0, "ate": duracao}]
155
+ return segs
156
+
157
+
158
+ def montar_tomadas(tomadas: list[dict], medidas: dict[float, dict | None], largura: int, altura: int) -> list[dict]:
159
+ """medidas: {t: face or None} for every sample of the cut. One path per take."""
160
+ ts = sorted(medidas)
161
+ saida = []
162
+ for tm in tomadas:
163
+ # 2 ms of slack towards the new take, as the build does (a cut time rounded to 3 digits)
164
+ am = [(t, medidas[t]) for t in ts if tm["de"] - 2e-3 <= t < tm["ate"] - 2e-3]
165
+ pontos = suavizar(am, largura, altura) or []
166
+ achados = [m for _t, m in am if m is not None]
167
+ saida.append({"id": tm["id"], "de": round(tm["de"], 4), "ate": round(tm["ate"], 4), "amostras": len(am),
168
+ "medidos": len(achados),
169
+ "w_med": round(_mediana([m["w"] for m in achados]), 4) if achados else None,
170
+ "h_med": round(_mediana([m["h"] for m in achados]), 4) if achados else None,
171
+ "pontos": pontos})
172
+ return saida
173
+
174
+
175
+ # ---------------------------------------------------------------- measuring
176
+
177
+ def medir(video: Path, fps: float, largura: int, altura: int, progresso: bool = True) -> dict[float, dict | None]:
178
+ """One decoding pass (every `step`-th frame, scaled to 540 on the short side), one detection per sample."""
179
+ import numpy as np # noqa: PLC0415
180
+
181
+ import face_layout # noqa: PLC0415
182
+
183
+ passo = max(1, int(round(fps * PASSO_S)))
184
+ esc = min(1.0, LADO_DETECCAO / max(1, min(largura, altura)))
185
+ dw, dh = max(2, int(round(largura * esc / 2)) * 2), max(2, int(round(altura * esc / 2)) * 2)
186
+ cmd = ["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-i", str(video),
187
+ "-vf", f"select='not(mod(n\\,{passo}))',scale={dw}:{dh}:flags=area", "-fps_mode", "passthrough",
188
+ "-an", "-f", "rawvideo", "-pix_fmt", "bgr24", "-"]
189
+ tam = dw * dh * 3
190
+ out: dict[float, dict | None] = {}
191
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
192
+ k, inicio = 0, time.time()
193
+ try:
194
+ while True:
195
+ buf = proc.stdout.read(tam) if proc.stdout else b""
196
+ if len(buf) < tam:
197
+ break
198
+ img = np.frombuffer(buf, np.uint8).reshape(dh, dw, 3)
199
+ f = face_layout.detect_array(img)
200
+ t = round(k * passo / fps, 4)
201
+ out[t] = None if not f else {"cx": (f["x0"] + f["x1"]) / 2, "cy": (f["y0"] + f["y1"]) / 2,
202
+ "w": f["x1"] - f["x0"], "h": f["y1"] - f["y0"]}
203
+ k += 1
204
+ if progresso and k % 100 == 0:
205
+ print(f" {t:6.1f} s medidos ({time.time() - inicio:.0f} s)", flush=True)
206
+ finally:
207
+ if proc.poll() is None:
208
+ proc.kill()
209
+ err = proc.stderr.read().decode("utf-8", "replace") if proc.stderr else ""
210
+ proc.wait()
211
+ if not out:
212
+ raise TrilhaErro(f"não consegui ler os quadros de {video.name}: {' '.join(err.strip().splitlines()[-3:])}")
213
+ return out
214
+
215
+
216
+ def construir(edit: Path, forcar: bool = False) -> dict:
217
+ edit = edit.resolve()
218
+ video = edit / "cut.mp4"
219
+ if not video.exists():
220
+ raise TrilhaErro(f"não achei {video}: termine o corte antes (render_cut.py)")
221
+ saida = edit / "rosto_trilha.json"
222
+ sig = file_signature(video)
223
+ velho = load_json(saida) if saida.exists() and not forcar else None
224
+ if isinstance(velho, dict) and velho.get("versao") == VERSAO and velho.get("video_sig") == sig:
225
+ velho["_de_novo"] = False
226
+ return velho
227
+ require_binary("ffmpeg")
228
+ info = probe_video(video)
229
+ largura = int(info.get("display_width") or info.get("width") or 0)
230
+ altura = int(info.get("display_height") or info.get("height") or 0)
231
+ fps = float(info.get("fps") or 30.0)
232
+ duracao = float(info.get("duration") or 0.0)
233
+ if largura <= 0 or altura <= 0:
234
+ raise TrilhaErro(f"não consegui ler o tamanho de {video.name}")
235
+ tl_p = edit / "cut_timeline.json"
236
+ timeline = load_json(tl_p) if tl_p.exists() else None
237
+ tomadas = tomadas_do_corte(timeline, duracao)
238
+ inicio = time.time()
239
+ medidas = medir(video, fps, largura, altura)
240
+ trilha = {
241
+ "versao": VERSAO, "video": "cut.mp4", "video_sig": sig, "largura": largura, "altura": altura,
242
+ "fps": round(fps, 3), "duracao": round(duracao, 3), "passo_s": PASSO_S,
243
+ "parametros": {"mediana": JANELA, "zona_morta": ZONA_MORTA, "zona_tamanho": ZONA_TAMANHO,
244
+ "transicao_s": TRANSICAO_S, "perda_longa_s": round(PERDA_LONGA * PASSO_S, 2),
245
+ "confirma": f"{CONFIRMA} de 3"},
246
+ "tomadas": montar_tomadas(tomadas, medidas, largura, altura),
247
+ "segundos": round(time.time() - inicio, 1),
248
+ "criado_em": now_iso(),
249
+ }
250
+ save_json(saida, trilha)
251
+ trilha["_de_novo"] = True
252
+ return trilha
253
+
254
+
255
+ def resumo(trilha: dict) -> list[str]:
256
+ linhas = []
257
+ W = trilha["largura"]
258
+ for tm in trilha["tomadas"]:
259
+ pts = tm["pontos"]
260
+ if not pts:
261
+ linhas.append(f" {tm['id']:>5} {tm['de']:6.2f}-{tm['ate']:6.2f}s sem rosto ({tm['medidos']}/{tm['amostras']}): "
262
+ f"o enquadramento fica o da tomada")
263
+ continue
264
+ xs, ys = [p[1] for p in pts], [p[2] for p in pts]
265
+ anda = max(max(xs) - min(xs), (max(ys) - min(ys)) * trilha["altura"] / W)
266
+ linhas.append(f" {tm['id']:>5} {tm['de']:6.2f}-{tm['ate']:6.2f}s rosto {tm['medidos']}/{tm['amostras']} "
267
+ f"anda até {anda * W:.0f} px ({anda:.0%} da largura)")
268
+ return linhas
269
+
270
+
271
+ def main(argv: list[str] | None = None) -> int:
272
+ ap = argparse.ArgumentParser(description="Rosto 10 vezes por segundo em cada tomada do corte, suavizado "
273
+ "(o caminho que o enquadramento segue nos blocos com acompanhar).")
274
+ ap.add_argument("edit", type=Path, help="pasta edit do corte (cut.mp4 e cut_timeline.json)")
275
+ ap.add_argument("--forcar", action="store_true", help="mede de novo mesmo com o rosto_trilha.json em dia")
276
+ a = ap.parse_args(argv)
277
+ edit = a.edit.expanduser()
278
+ if not edit.is_dir():
279
+ print(f"erro: pasta edit não encontrada: {edit}", file=sys.stderr)
280
+ return 2
281
+ try:
282
+ print("medindo o rosto 10 vezes por segundo no corte (uns segundos por minuto de vídeo)...", flush=True)
283
+ trilha = construir(edit, a.forcar)
284
+ except TrilhaErro as e:
285
+ print(f"erro: {e}", file=sys.stderr)
286
+ return 2
287
+ except Exception as e: # noqa: BLE001 (OpenCV or the model missing: say it plainly)
288
+ print(f"erro: {e}", file=sys.stderr)
289
+ return 2
290
+ for linha in resumo(trilha):
291
+ print(linha)
292
+ estado = "medido agora" if trilha.get("_de_novo") else "já estava em dia"
293
+ print(f"ok: {edit.resolve() / 'rosto_trilha.json'} ({estado})")
294
+ return 0
295
+
296
+
297
+ if __name__ == "__main__":
298
+ sys.exit(main())