primocode 10.16.0 → 10.17.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.
Files changed (237) 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 +28 -8
  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 +36 -8
  198. package/lib/catalogo.js +4 -0
  199. package/lib/chamada-torta.js +81 -0
  200. package/lib/editor-pro.js +786 -0
  201. package/lib/entrega.js +58 -0
  202. package/lib/estudio/conferencia-visual.js +76 -0
  203. package/lib/estudio/diretor.js +6 -1
  204. package/lib/estudio/exportar.js +3 -1
  205. package/lib/estudio/gosto.js +45 -4
  206. package/lib/estudio/index.js +8 -5
  207. package/lib/estudio/kit-ae/base.jsx +119 -13
  208. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  209. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  210. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  211. package/lib/estudio/maquina-diretor.js +61 -7
  212. package/lib/estudio/maquina-foto.js +177 -0
  213. package/lib/estudio/maquina-guarda.js +207 -0
  214. package/lib/estudio/maquina-layout.js +22 -1
  215. package/lib/estudio/maquina.js +60 -11
  216. package/lib/estudio/referencias.js +1 -1
  217. package/lib/estudio/visao/visao.swift +97 -0
  218. package/lib/estudio/visao.js +60 -0
  219. package/lib/local/cerebro.json +192 -6
  220. package/lib/local/motor.js +5 -2
  221. package/lib/mcp.js +105 -7
  222. package/lib/nuvem.js +11 -1
  223. package/lib/outra-pasta.js +1 -1
  224. package/lib/passagem.js +50 -0
  225. package/lib/perigo.js +29 -12
  226. package/lib/protecao.js +134 -0
  227. package/lib/sandbox.js +11 -4
  228. package/lib/titulo-conversa.js +46 -0
  229. package/lib/tools.js +3 -0
  230. package/lib/ui.js +1 -1
  231. package/package.json +3 -2
  232. package/skills/editar-meu-video/SKILL.md +6 -2
  233. package/skills/editar-video/SKILL.md +113 -0
  234. package/studio/pessoa.py +52 -0
  235. package/voz/primo_voz/app.py +43 -4
  236. package/voz/primo_voz/chat.html +17 -6
  237. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,246 @@
1
+ """Sample PNGs of every caption and hook style on a real frame of the cut (Contract 7).
2
+
3
+ Writes edit/samples/caption_<id>.png and edit/samples/hook_<id>.png at 540x960 (960x540 for a
4
+ horizontal cut) plus edit/samples/index.json, and points state.json "samples" at the index.
5
+
6
+ The speaker's face is found on the frame (face_layout.py, YuNet) and each sample goes to the free
7
+ spot closest to its usual height, off the face, the way compose_final.py places the real text.
8
+ Without --time, when the frame at 25% shows no speaker (a screen, a product), the frame moves to the
9
+ middle of the nearest cut segment with a face (face_layout.json, which compose_final.py reuses); a
10
+ frame where the detector misses the face uses its segment's face zone. A
11
+ number in edit_data.json (hook.center_y, captions.center_y) is used as it is. index.json also
12
+ gets "face" (the zone found, fractions of the frame, or null), "positions" ({"caption": {id: px},
13
+ "hook": {id: px}} on the full-size canvas) and "suggest" ({"camera_focus_y": eyes line or null}).
14
+
15
+ Usage:
16
+ uv run python helpers/style_samples.py --edit-dir DIR [--time T] [--accent HEX] [--text "..."]
17
+ [--hook-text "..."] [--video other.mp4]
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import time
27
+ from pathlib import Path
28
+
29
+ from PIL import Image
30
+
31
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
32
+ from common import hex_to_rgb, load_json, load_style_catalog, now_iso, probe_video, require_binary, save_json # noqa: E402
33
+ import face_layout # noqa: E402
34
+ import text_render as tr # noqa: E402
35
+
36
+ DEFAULT_CAPTION_TEXT = "Olha só isso aqui"
37
+ DEFAULT_HOOK_TEXT = "Você está perdendo venda assim"
38
+
39
+
40
+ def grab_frame(video: Path, t: float) -> Image.Image:
41
+ with tempfile.TemporaryDirectory(prefix="samples-") as tmp:
42
+ out = Path(tmp) / "frame.png"
43
+ cmd = ["ffmpeg", "-v", "error", "-y", "-ss", f"{max(0.0, t):.3f}", "-i", str(video), "-frames:v", "1", str(out)]
44
+ proc = subprocess.run(cmd, capture_output=True, text=True)
45
+ if proc.returncode != 0 or not out.exists():
46
+ # seeking past the last frame gives nothing: fall back to the first frame
47
+ cmd[5] = "0"
48
+ proc = subprocess.run(cmd, capture_output=True, text=True)
49
+ if not out.exists():
50
+ raise RuntimeError(f"não consegui extrair um quadro de {video}: {proc.stderr[-400:]}")
51
+ with Image.open(out) as im:
52
+ return im.convert("RGB")
53
+
54
+
55
+ def caption_text_near(captions: dict | None, t: float) -> str | None:
56
+ """A real cue around time t (prefers cues with 2+ words)."""
57
+ if not captions:
58
+ return None
59
+ cues = [c for c in captions.get("cues", []) if c.get("words")]
60
+ if not cues:
61
+ return None
62
+ multi = [c for c in cues if len(c["words"]) >= 2] or cues
63
+ best = min(multi, key=lambda c: abs((c["start"] + c["end"]) / 2.0 - t))
64
+ return " ".join(w["text"] for w in best["words"])
65
+
66
+
67
+ def _load_dict(path: Path, notes: list[str]) -> dict:
68
+ """A JSON object from the edit dir, or {} (with a note) when it is missing or unreadable."""
69
+ try:
70
+ data = load_json(path)
71
+ except (ValueError, OSError) as e:
72
+ notes.append(f"ignorei {path.name}: não consegui ler ({e})")
73
+ return {}
74
+ return data if isinstance(data, dict) else {}
75
+
76
+
77
+ def _manual(sec: dict, key: str) -> float | None:
78
+ v = sec.get(key) if isinstance(sec, dict) else None
79
+ if v is None or (isinstance(v, str) and v.strip().lower() == "auto"):
80
+ return None
81
+ try:
82
+ return float(v)
83
+ except (TypeError, ValueError):
84
+ return None
85
+
86
+
87
+ def free_spot(img, pref: float, faces: list[tuple[float, float]], lay: dict) -> float:
88
+ """Center for a text drawn at `pref` in `img` (ink box measured) that clears the faces."""
89
+ bb = tr.ink_bbox(img)
90
+ if not bb:
91
+ return pref
92
+ c = face_layout.free_center(pref, pref - bb[1], bb[3] - pref, faces, lay["top"], lay["bottom"])
93
+ return pref if c is None else c
94
+
95
+
96
+ def main(argv: list[str] | None = None) -> int:
97
+ ap = argparse.ArgumentParser(description="Gera amostras de todos os estilos de legenda e gancho sobre um quadro do corte.")
98
+ ap.add_argument("--edit-dir", required=True, help="pasta edit/ da sessão")
99
+ ap.add_argument("--time", type=float, help="segundo do quadro (padrão: 25%% do vídeo)")
100
+ ap.add_argument("--accent", help="cor de destaque em hex (padrão: a do edit_data, do preview ou do catálogo)")
101
+ ap.add_argument("--text", help="texto das amostras (legenda e gancho)")
102
+ ap.add_argument("--hook-text", help="texto só do gancho (padrão: --text, o gancho do edit_data ou um exemplo)")
103
+ ap.add_argument("--video", help="vídeo de onde tirar o quadro (padrão: edit/cut.mp4)")
104
+ args = ap.parse_args(argv)
105
+
106
+ require_binary("ffmpeg")
107
+ edit = Path(args.edit_dir).resolve()
108
+ if not edit.is_dir():
109
+ print(f"erro: pasta de edição não encontrada: {edit}", file=sys.stderr)
110
+ return 2
111
+ video = Path(args.video).resolve() if args.video else edit / "cut.mp4"
112
+ if not video.exists():
113
+ print(f"erro: vídeo não encontrado: {video}. Renderize o corte primeiro (render_cut.py) ou passe --video.",
114
+ file=sys.stderr)
115
+ return 2
116
+
117
+ catalog = load_style_catalog()
118
+ notes = []
119
+ edit_data = _load_dict(edit / "edit_data.json", notes)
120
+ picks = _load_dict(edit / "preview_style.json", notes)
121
+ captions = _load_dict(edit / "captions.json", notes) or None
122
+
123
+ accent = args.accent or edit_data.get("accent") or picks.get("accent") \
124
+ or (catalog.get("defaults") or {}).get("accent") or tr.DEFAULT_ACCENT
125
+ try:
126
+ hex_to_rgb(accent)
127
+ except ValueError:
128
+ print(f"erro: cor inválida: {accent!r} (use algo como #FFC400)", file=sys.stderr)
129
+ return 2
130
+
131
+ info = probe_video(video)
132
+ duration = float(info.get("duration") or 0.0)
133
+ t = args.time if args.time is not None else duration * 0.25
134
+ if duration > 0:
135
+ t = min(max(0.0, t), max(0.0, duration - 0.05))
136
+ layout_seg = None
137
+ if video == edit / "cut.mp4":
138
+ # the face of each cut segment (face_layout.json, cached for compose_final): the default frame
139
+ # is moved to the nearest segment where the speaker shows up
140
+ try:
141
+ segs = [s for s in face_layout.build(edit).get("segments") or [] if isinstance(s, dict)]
142
+ except Exception as e: # noqa: BLE001 (samples still work without the face)
143
+ segs = []
144
+ notes.append(f"não consegui procurar o rosto no corte ({e})")
145
+ here = next((s for s in segs if s["start"] <= t < s["end"]), None)
146
+ with_face = [s for s in segs if s.get("face")]
147
+ if args.time is None and with_face and not (here and here.get("face")):
148
+ here = min(with_face, key=lambda s: min(abs(t - s["start"]), abs(t - s["end"])))
149
+ t = round((here["start"] + here["end"]) / 2.0, 3)
150
+ layout_seg = here if here and here.get("face") else None
151
+
152
+ caption_text = args.text or caption_text_near(captions, t) or DEFAULT_CAPTION_TEXT
153
+ hook_text = args.hook_text or args.text or (edit_data.get("hook") if isinstance(edit_data.get("hook"), dict) else {}).get("text") \
154
+ or picks.get("hook_text") or DEFAULT_HOOK_TEXT
155
+
156
+ started = time.time()
157
+ frame = grab_frame(video, t)
158
+ vertical = frame.height >= frame.width
159
+ size = (540, 960) if vertical else (960, 540)
160
+ # compose on the full-size canvas, then shrink: sharper than drawing at 540 wide
161
+ background = tr.cover_image(frame, (1080, 1920) if vertical else (1920, 1080))
162
+
163
+ samples = edit / "samples"
164
+ samples.mkdir(parents=True, exist_ok=True)
165
+ cap_ids = [s["id"] for s in catalog.get("caption_styles", [])] or tr.caption_style_ids()
166
+ hook_ids = [s["id"] for s in catalog.get("hook_styles", [])] or tr.hook_style_ids()
167
+
168
+ canvas = tr.sample_canvas(background.size)
169
+ lay = tr.layout_defaults(canvas)
170
+ face = zone = None
171
+ try:
172
+ face = face_layout.detect_image(background)
173
+ except Exception as e: # noqa: BLE001 (samples still work without the face)
174
+ notes.append(f"não consegui procurar o rosto ({e}); amostras nas alturas padrão")
175
+ faces: list[tuple[float, float]] = []
176
+ if not face and layout_seg is not None:
177
+ face = layout_seg["face"] # not found on this very frame: the segment's face
178
+ if face:
179
+ _, zone, _ = face_layout._summarize([face])
180
+ if layout_seg is not None and face is layout_seg["face"] and layout_seg.get("zone"):
181
+ zone = layout_seg["zone"]
182
+ gap = face_layout.GAP_FRAC * canvas[1]
183
+ faces = [(zone["y0"] * canvas[1] - gap, zone["y1"] * canvas[1] + gap)]
184
+ manual = {"caption": _manual(edit_data.get("captions"), "center_y"), "hook": _manual(edit_data.get("hook"), "center_y")}
185
+ out_cfg = edit_data.get("output") if isinstance(edit_data.get("output"), dict) else {}
186
+ try:
187
+ scale_y = canvas[1] / float(out_cfg.get("height") or canvas[1])
188
+ except (TypeError, ValueError, ZeroDivisionError):
189
+ scale_y = 1.0
190
+
191
+ def position(kind: str, sid: str, text: str) -> float:
192
+ if manual[kind] is not None:
193
+ return manual[kind] * scale_y
194
+ if kind == "caption":
195
+ words, active = tr.sample_caption_words(text, sid)
196
+ probe = tr.render_caption_image(words, sid, accent, canvas, active=active, center_y=lay["caption"])
197
+ return free_spot(probe, lay["caption"], faces, lay) if faces else lay["caption"]
198
+ probe = tr.render_hook(text, sid, accent, canvas, center_y=int(round(lay["hook"])))
199
+ return free_spot(probe, lay["hook"], faces, lay) if faces else lay["hook"]
200
+
201
+ index = {"frame_time": round(t, 3), "accent": accent, "caption": {}, "hook": {},
202
+ "caption_text": caption_text, "hook_text": hook_text, "size": list(size),
203
+ "face": zone, "positions": {"caption": {}, "hook": {}},
204
+ "suggest": {"camera_focus_y": round(face["eyes_y"], 3) if face else None},
205
+ "created_at": now_iso()}
206
+ skipped = []
207
+ for kind, ids, text in (("caption", cap_ids, caption_text), ("hook", hook_ids, hook_text)):
208
+ for sid in ids:
209
+ try:
210
+ cy = position(kind, sid, text)
211
+ img = tr.render_sample_frame(background, kind, sid, accent, text, center_y=cy).resize(size, Image.LANCZOS)
212
+ except ValueError as e:
213
+ skipped.append(f"{kind} {sid}: {e}")
214
+ continue
215
+ index["positions"][kind][sid] = int(round(cy))
216
+ name = f"{kind}_{sid}.png"
217
+ tmp = samples / f".tmp-{name}"
218
+ img.save(tmp, "PNG", optimize=False, compress_level=6)
219
+ tmp.replace(samples / name)
220
+ index[kind][sid] = name
221
+ save_json(samples / "index.json", index)
222
+
223
+ state_path = edit / "state.json"
224
+ state = load_json(state_path, default={}) or {}
225
+ state["samples"] = "samples/index.json"
226
+ state["updated_at"] = now_iso()
227
+ save_json(state_path, state)
228
+
229
+ for msg in skipped:
230
+ print(f"aviso: estilo ignorado ({msg})", file=sys.stderr)
231
+ for msg in notes + tr.text_issues("hook", hook_text, hook_ids[0] if hook_ids else "manchete"):
232
+ print(f"aviso: {msg}", file=sys.stderr)
233
+ where = ""
234
+ if face:
235
+ caps = sorted(set(index["positions"]["caption"].values()))
236
+ hooks = sorted(set(index["positions"]["hook"].values()))
237
+ span = lambda v: f"{v[0]}" if len(v) == 1 else f"{v[0]} a {v[-1]}" # noqa: E731
238
+ where = (f", rosto em {zone['y0']:.2f}-{zone['y1']:.2f} da altura: legenda em {span(caps)} px, "
239
+ f"gancho em {span(hooks)} px, camera.focus_y sugerido {index['suggest']['camera_focus_y']}")
240
+ print(f"ok: {len(index['caption'])} amostras de legenda e {len(index['hook'])} de gancho "
241
+ f"(quadro em {t:.2f}s, cor {accent}, {time.time() - started:.1f}s{where}) -> {samples / 'index.json'}")
242
+ return 0
243
+
244
+
245
+ if __name__ == "__main__":
246
+ sys.exit(main())