primocode 10.16.0 → 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 (234) 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 +11 -2
  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/nuvem.js +11 -1
  222. package/lib/passagem.js +50 -0
  223. package/lib/perigo.js +19 -11
  224. package/lib/protecao.js +134 -0
  225. package/lib/titulo-conversa.js +46 -0
  226. package/lib/tools.js +3 -0
  227. package/lib/ui.js +1 -1
  228. package/package.json +3 -2
  229. package/skills/editar-meu-video/SKILL.md +6 -2
  230. package/skills/editar-video/SKILL.md +113 -0
  231. package/studio/pessoa.py +52 -0
  232. package/voz/primo_voz/app.py +43 -4
  233. package/voz/primo_voz/chat.html +17 -6
  234. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,150 @@
1
+ """selo_garantia: o selo redondo de garantia, com serrilha, fita e o prazo em dias.
2
+
3
+ O selo entra girando e crescendo, a fita cai atrás e o número do prazo dá um pulinho. Serve para a
4
+ parte da garantia: "são 7 dias", "30 dias para testar", "garantia de 1 ano".
5
+
6
+ Tempo: `delay` até o selo entrar; o número pulsa logo depois.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ from functools import lru_cache
14
+
15
+ from motion import engine as E
16
+
17
+ TEMPLATE = {
18
+ "id": "selo_garantia",
19
+ "label_pt": "Selo de garantia",
20
+ "description_pt": "Selo redondo com serrilha e fita, com o prazo em dias no meio e uma linha embaixo. "
21
+ "Entra girando e o número dá um pulinho.",
22
+ "use_when_pt": "Quando a fala fala de garantia, prazo para testar ou devolução: 7 dias, 30 dias, "
23
+ "risco zero.",
24
+ "notes_pt": "O número é o que aparece grande: deixe a palavra em `unidade` curta (dias, meses).",
25
+ "category": "enfase",
26
+ "version": 1,
27
+ "params": {
28
+ "numero": {"type": "text", "default": "7", "max_len": 4, "required_text": True,
29
+ "label_pt": "O número do prazo (até 4 caracteres)"},
30
+ "unidade": {"type": "text", "default": "dias", "max_len": 10,
31
+ "label_pt": "A unidade embaixo do número (dias, meses...)"},
32
+ "titulo": {"type": "text", "default": "GARANTIA", "max_len": 16,
33
+ "label_pt": "Palavra em cima do número, dentro do selo"},
34
+ "legenda": {"type": "text", "default": "", "max_len": 34,
35
+ "label_pt": "Linha embaixo do selo (opcional). *Palavra* entre asteriscos fica na cor de destaque"},
36
+ "delay": {"type": "number", "default": 0.15, "min": 0, "max": 30,
37
+ "label_pt": "Segundos até o selo entrar"},
38
+ "fita": {"type": "bool", "default": True, "label_pt": "Fitas atrás do selo"},
39
+ },
40
+ "default_duration": 2.4,
41
+ "default_region": "centro",
42
+ "sfx": [{"at": 0.15, "name": "pop"}, {"at": 0.55, "name": "success", "volume": 0.8}],
43
+ "example": {"numero": "30", "unidade": "dias", "titulo": "GARANTIA", "legenda": "para testar *sem risco*"},
44
+ }
45
+
46
+ NUM_FONT = "sans_black"
47
+ TXT_FONT = "text_bold"
48
+ ENTRA = 0.42 # entrada do selo (gira e cresce)
49
+ PULO = 0.30 # pulinho do número
50
+ PONTAS = 18 # dentes da serrilha
51
+ PRE = 0.16 # o selo começa a crescer um pouco antes, para o quadro 0 nunca sair vazio
52
+
53
+
54
+ @lru_cache(maxsize=64)
55
+ def _layout(params_json: str, box: E.Box) -> dict:
56
+ p = json.loads(params_json)
57
+ legenda = p["legenda"].strip()
58
+ plain = " ".join(w for w, _ in E.parse_accent(legenda)) if legenda else ""
59
+ leg_size = leg_cap = 0.0
60
+ if legenda:
61
+ leg_size = E.fit_text(plain, TXT_FONT, box.w * 0.9, 52.0, 22.0)
62
+ leg_cap = E.measure("H", TXT_FONT, leg_size).cap
63
+ espaco = box.h - (leg_cap * 2.2 if legenda else 0.0)
64
+ r = max(90.0, min(box.w * 0.42, espaco * 0.42))
65
+ num_size = E.fit_text(p["numero"], NUM_FONT, r * 1.25, r * 1.12, 40.0)
66
+ uni_size = min(r * 0.30, E.fit_text(p["unidade"], TXT_FONT, r * 1.3, r * 0.30, 16.0))
67
+ tit_size = min(r * 0.24, E.fit_text(p["titulo"], TXT_FONT, r * 1.3, r * 0.24, 14.0))
68
+ cy = box.cy - (leg_cap * 1.1 if legenda else 0.0)
69
+ return {"r": r, "cx": box.cx, "cy": cy, "num_size": num_size, "uni_size": uni_size,
70
+ "tit_size": tit_size, "leg_size": leg_size, "leg_cap": leg_cap, "legenda": legenda,
71
+ "num_cap": E.measure("H", NUM_FONT, num_size).cap}
72
+
73
+
74
+ def _serrilha(cx: float, cy: float, r: float) -> list[tuple[float, float]]:
75
+ """Contorno do selo: dentes suaves em volta do círculo."""
76
+ pts = []
77
+ passos = PONTAS * 6
78
+ for i in range(passos):
79
+ a = 2 * math.pi * i / passos
80
+ raio = r * (1.0 + 0.055 * math.cos(PONTAS * a))
81
+ pts.append((cx + raio * math.cos(a), cy + raio * math.sin(a)))
82
+ return pts
83
+
84
+
85
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
86
+ cv = E.Canvas(box)
87
+ t = frame_index / fps + 1.0 / fps
88
+ L = _layout(json.dumps(params, sort_keys=True), box)
89
+ inicio = max(0.0, float(params["delay"]) - PRE)
90
+ if t < inicio:
91
+ return cv
92
+ ent = E.eased(t, inicio, ENTRA + PRE, "out_back")
93
+ if ent <= 0:
94
+ return cv
95
+ opac = min(1.0, E.eased(t, inicio, (ENTRA + PRE) * 0.5, "out_quad") * 1.4)
96
+ giro = -26.0 * (1.0 - E.clamp01(ent))
97
+ cx, cy, r = L["cx"], L["cy"], L["r"]
98
+ with cv.group(opacity=opac, scale=0.35 + 0.65 * min(1.15, ent), pivot=(cx, cy), rotate=giro):
99
+ if params["fita"]:
100
+ larg = r * 0.42
101
+ for lado in (-1, 1):
102
+ x0 = cx + lado * r * 0.36
103
+ pts = [(x0 - larg / 2, cy + r * 0.2), (x0 + larg / 2, cy + r * 0.2),
104
+ (x0 + larg / 2 + lado * r * 0.14, cy + r * 1.25),
105
+ (x0 + lado * r * 0.07, cy + r * 1.0),
106
+ (x0 - larg / 2 + lado * r * 0.14, cy + r * 1.25)]
107
+ cv.polygon(pts, fill="danger", shadow=E.Shadow(blur=12.0, dy=6.0, opacity=0.35))
108
+ cv.polygon(_serrilha(cx, cy, r), fill="accent",
109
+ shadow=E.Shadow(blur=16.0, dy=8.0, opacity=0.4))
110
+ cv.ring(cx, cy, r * 0.86, max(3.0, r * 0.035), color="on_accent", opacity=0.55)
111
+ cv.text(cx, cy - r * 0.34, params["titulo"], TXT_FONT, L["tit_size"], color="on_accent",
112
+ anchor="mc", opacity=0.9, upper=True)
113
+ bate = inicio + (ENTRA + PRE) * 0.7
114
+ pulo = 1.0 + 0.10 * (1.0 - E.clamp01((t - bate) / PULO)) if bate <= t < bate + PULO else 1.0
115
+ with cv.group(scale=pulo, pivot=(cx, cy + r * 0.05)):
116
+ cv.text(cx, cy + L["num_cap"] * 0.5 + r * 0.02, params["numero"], NUM_FONT, L["num_size"],
117
+ color="on_accent", anchor="ms")
118
+ cv.text(cx, cy + r * 0.56, params["unidade"], TXT_FONT, L["uni_size"], color="on_accent",
119
+ anchor="mc", opacity=0.95)
120
+ if L["legenda"]:
121
+ la = E.eased(t, inicio + (ENTRA + PRE) * 0.6, 0.3, "out_cubic")
122
+ if la > 0:
123
+ y = cy + r * 1.35 + L["leg_cap"]
124
+ palavras = E.parse_accent(L["legenda"])
125
+ larguras = [E.measure(w, TXT_FONT, L["leg_size"]).width for w, _ in palavras]
126
+ espaco = (E.measure("a a", TXT_FONT, L["leg_size"]).width
127
+ - E.measure("aa", TXT_FONT, L["leg_size"]).width)
128
+ total = sum(larguras) + espaco * max(0, len(palavras) - 1)
129
+ x = cx - total / 2.0
130
+ for (palavra, destaque), larg in zip(palavras, larguras):
131
+ cor = "accent" if destaque else "text"
132
+ cv.text(x, y + (1.0 - la) * 12.0, palavra, TXT_FONT, L["leg_size"], color=cor,
133
+ opacity=la, anchor="ls", stroke=3.0, stroke_color=(0, 0, 0, 140))
134
+ x += larg + espaco
135
+ return cv
136
+
137
+
138
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
139
+ d = float(params["delay"])
140
+ return [{"frame": E.frame_of(d, fps), "name": "pop", "why": "entrada do selo"},
141
+ {"frame": E.frame_of(d + ENTRA * 0.7, fps), "name": "success", "volume": 0.8,
142
+ "why": "o prazo aparece"}]
143
+
144
+
145
+ def settle(params: dict, fps: int) -> float:
146
+ return float(params["delay"]) + ENTRA + PULO + 0.05
147
+
148
+
149
+ def suggested_duration(params: dict) -> float:
150
+ return round(max(TEMPLATE["default_duration"], float(params["delay"]) + ENTRA + PULO + 1.2), 2)
@@ -0,0 +1,241 @@
1
+ """Renders one animated template (helpers/motion) to check it: transparent .mov, PNG frames, an .mp4 over a
2
+ background and a contact sheet. The compose uses the same renderer (SPEC Contract 12).
3
+
4
+ Usage:
5
+ uv run python helpers/motion_render.py --list [--json]
6
+ uv run python helpers/motion_render.py --describe contador
7
+ uv run python helpers/motion_render.py --template contador --params '{"value": 1904152.87, "prefix": "R$"}' \\
8
+ --out DIR [--formato mov,sheet,mp4,png,clip] [--duracao 3] [--fps 30] [--tamanho 1080x1920] \\
9
+ [--cor '#FFC400'] [--regiao centro | --regiao 140,640,800,640] [--fundo frame.png] [--quadros 8]
10
+ --params-file F reads the params from a JSON file (easier with R$ and quotes).
11
+
12
+ The last stdout line is a JSON object with the files, the frames and the sound cues.
13
+ Exit 2 for things to fix (unknown template, bad params), 1 for ffmpeg or disk trouble.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+ import time
22
+ from pathlib import Path
23
+
24
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
25
+ from motion import engine as E # noqa: E402
26
+ from motion import registry as Rg # noqa: E402
27
+ from motion import render as R # noqa: E402
28
+ from motion.engine import MotionError # noqa: E402
29
+
30
+ FORMATS = ("mov", "sheet", "mp4", "png", "clip")
31
+
32
+
33
+ def _utf8() -> None:
34
+ for stream in (sys.stdout, sys.stderr):
35
+ try:
36
+ stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
37
+ except (AttributeError, ValueError):
38
+ pass
39
+
40
+
41
+ def parse_size(text: str) -> tuple[int, int]:
42
+ try:
43
+ w, h = (int(v) for v in str(text).lower().split("x"))
44
+ except ValueError:
45
+ raise MotionError(f"--tamanho inválido: {text!r} (ex.: 1080x1920)") from None
46
+ if w < 16 or h < 16 or w > 4096 or h > 4096:
47
+ raise MotionError(f"--tamanho fora do limite: {text!r}")
48
+ return w, h
49
+
50
+
51
+ def parse_region(text: str | None):
52
+ if text is None:
53
+ return None
54
+ t = text.strip()
55
+ if "," in t:
56
+ parts = t.replace("[", "").replace("]", "").split(",")
57
+ try:
58
+ return [float(v) for v in parts]
59
+ except ValueError:
60
+ raise MotionError(f"--regiao inválida: {text!r} (use um nome ou x,y,largura,altura)") from None
61
+ return t
62
+
63
+
64
+ def load_params(args) -> dict:
65
+ if args.params and args.params_file:
66
+ raise MotionError("use --params ou --params-file, não os dois")
67
+ raw = None
68
+ if args.params_file:
69
+ p = Path(args.params_file)
70
+ if not p.exists():
71
+ raise MotionError(f"arquivo de params não encontrado: {p}")
72
+ raw = p.read_text(encoding="utf-8")
73
+ elif args.params:
74
+ raw = args.params
75
+ if raw is None:
76
+ return {}
77
+ try:
78
+ data = json.loads(raw)
79
+ except ValueError as e:
80
+ raise MotionError(f"params não é um JSON válido ({e}). Dica: use --params-file com o JSON num arquivo") from None
81
+ if not isinstance(data, dict):
82
+ raise MotionError("params precisa ser um objeto JSON, ex.: {\"value\": 37}")
83
+ return data
84
+
85
+
86
+ def cmd_list(as_json: bool) -> int:
87
+ found, errs = Rg.discover()
88
+ if as_json:
89
+ print(json.dumps({"templates": [t.describe() for t in found.values()], "errors": errs}, ensure_ascii=False))
90
+ return 0
91
+ if not found:
92
+ print("nenhum modelo de animação encontrado")
93
+ for t in sorted(found.values(), key=lambda x: x.id):
94
+ print(f"{t.id:18s} {t.label} · {t.default_duration:g} s · {t.description}")
95
+ for e in errs:
96
+ print(f"aviso: {e}", file=sys.stderr)
97
+ return 0
98
+
99
+
100
+ def _limits(spec: dict) -> list[str]:
101
+ extra = []
102
+ if "choices" in spec:
103
+ extra.append("opções: " + ", ".join(map(str, spec["choices"])))
104
+ if "min" in spec or "max" in spec:
105
+ lo = E.format_number(spec["min"], E.auto_decimals(spec["min"])) if "min" in spec else "-"
106
+ hi = E.format_number(spec["max"], E.auto_decimals(spec["max"])) if "max" in spec else "-"
107
+ extra.append(f"de {lo} a {hi}")
108
+ if "max_len" in spec:
109
+ extra.append(f"até {spec['max_len']} letras")
110
+ if "min_items" in spec or "max_items" in spec:
111
+ extra.append(f"de {spec.get('min_items', 0)} a {spec.get('max_items', '?')} itens")
112
+ if spec.get("nullable"):
113
+ extra.append("pode ser null")
114
+ return extra
115
+
116
+
117
+ def cmd_describe(tid: str) -> int:
118
+ t = Rg.get(tid)
119
+ print(f"{t.id}: {t.label}")
120
+ print(t.description)
121
+ if t.use_when:
122
+ print(f"Quando usar: {t.use_when}")
123
+ if t.notes:
124
+ print(f"Atenção: {t.notes}")
125
+ region = t.default_region if isinstance(t.default_region, str) else list(t.default_region)
126
+ print(f"Duração padrão: {t.default_duration:g} s · região padrão: {region} · saída: {t.exit}")
127
+ print("Parâmetros:")
128
+ for name, spec in t.all_params().items():
129
+ extra = _limits(spec)
130
+ default = json.dumps(spec["default"], ensure_ascii=False) if "default" in spec else "obrigatório"
131
+ print(f" {name} ({spec['type']}, padrão {default}){' · ' + '; '.join(extra) if extra else ''}: "
132
+ f"{spec.get('label_pt', '')}")
133
+ if spec.get("type") == "list" and isinstance(spec.get("item"), dict):
134
+ item = spec["item"]
135
+ if item.get("type") == "object":
136
+ for fname, fspec in (item.get("fields") or {}).items():
137
+ fextra = _limits(fspec)
138
+ fdefault = (json.dumps(fspec["default"], ensure_ascii=False) if "default" in fspec
139
+ else "obrigatório")
140
+ print(f" cada item.{fname} ({fspec['type']}, padrão {fdefault})"
141
+ f"{' · ' + '; '.join(fextra) if fextra else ''}: {fspec.get('label_pt', '')}")
142
+ else:
143
+ iextra = _limits(item)
144
+ if iextra:
145
+ print(f" cada item ({item['type']}): {'; '.join(iextra)}")
146
+ print("Sons: " + (", ".join(f"{c['name']} em {c.get('at', 0):g} s" for c in t.sfx) or "nenhum"))
147
+ if t.example:
148
+ print("Exemplo de params: " + json.dumps(t.example, ensure_ascii=False))
149
+ return 0
150
+
151
+
152
+ def main(argv: list[str] | None = None) -> int:
153
+ _utf8()
154
+ ap = argparse.ArgumentParser(description="Renderiza um modelo de animação (contador, lista, preço...) para conferir.")
155
+ ap.add_argument("--list", action="store_true", help="lista os modelos disponíveis")
156
+ ap.add_argument("--json", action="store_true", help="com --list: saída em JSON")
157
+ ap.add_argument("--describe", metavar="ID", help="mostra os parâmetros de um modelo")
158
+ ap.add_argument("--template", "-t", help="id do modelo (veja --list)")
159
+ ap.add_argument("--params", help="parâmetros em JSON, ex.: '{\"value\": 37}'")
160
+ ap.add_argument("--params-file", help="arquivo .json com os parâmetros")
161
+ ap.add_argument("--out", "-o", help="pasta de saída")
162
+ ap.add_argument("--formato", default="mov,sheet",
163
+ help=f"o que gerar, separado por vírgula: {', '.join(FORMATS)} (padrão mov,sheet)")
164
+ ap.add_argument("--duracao", type=float, help="segundos na tela (padrão: o do modelo)")
165
+ ap.add_argument("--fps", type=int, default=30)
166
+ ap.add_argument("--tamanho", default="1080x1920", help="tamanho da tela, ex.: 1080x1920 ou 540x960")
167
+ ap.add_argument("--cor", default=E.DEFAULT_ACCENT, help="cor de destaque do vídeo (#RRGGBB)")
168
+ ap.add_argument("--regiao", help="nome (topo, centro, baixo, faixa_topo, tela) ou x,y,largura,altura em 1080x1920")
169
+ ap.add_argument("--fundo", help="imagem de fundo para a folha e o .mp4 (ex.: um quadro do corte)")
170
+ ap.add_argument("--quadros", type=int, default=8, help="quadros na folha (padrão 8)")
171
+ ap.add_argument("--nome", help="nome base dos arquivos (padrão: o id do modelo)")
172
+ args = ap.parse_args(argv)
173
+
174
+ try:
175
+ if args.list:
176
+ return cmd_list(args.json)
177
+ if args.describe:
178
+ return cmd_describe(args.describe)
179
+ if not args.template:
180
+ raise MotionError("diga o modelo com --template (veja --list)")
181
+ if not args.out:
182
+ raise MotionError("diga a pasta de saída com --out")
183
+ formats = [f.strip() for f in args.formato.split(",") if f.strip()]
184
+ bad = [f for f in formats if f not in FORMATS]
185
+ if bad or not formats:
186
+ raise MotionError(f"--formato inválido: {', '.join(bad) or '(vazio)'} (use {', '.join(FORMATS)})")
187
+ E.rgba(args.cor)
188
+ if args.quadros < 1 or args.quadros > 48:
189
+ raise MotionError("--quadros precisa ficar entre 1 e 48")
190
+ if args.nome is not None and (not args.nome.strip() or any(c in args.nome for c in '/\\:*?"<>|')):
191
+ raise MotionError("--nome precisa ser um nome de arquivo simples, sem barras")
192
+ params = load_params(args)
193
+ t0 = time.time()
194
+ item = R.plan_item(args.template, params, fps=args.fps, canvas=parse_size(args.tamanho),
195
+ region=parse_region(args.regiao), duration=args.duracao, accent=args.cor)
196
+ out = Path(args.out).expanduser().resolve()
197
+ out.mkdir(parents=True, exist_ok=True)
198
+ base = args.nome or item.template.id
199
+ result: dict = {"template": item.template.id, "frames": item.frames, "fps": item.fps,
200
+ "duration": round(item.frames / item.fps, 4), "canvas": list(item.canvas),
201
+ "region": [round(v, 2) for v in item.region], "params": item.params,
202
+ "sfx": R.sfx_cues(item, check_names=True), "files": {}}
203
+ print(f"{item.template.label}: {item.frames} quadros ({E.format_number(item.frames / item.fps, 2)} s) "
204
+ f"em {item.canvas[0]}x{item.canvas[1]}")
205
+ if "mov" in formats:
206
+ result["files"]["mov"] = str(R.write_mov(item, out / f"{base}.mov"))
207
+ if "clip" in formats:
208
+ clip = R.render_clip(item, out, cache=False)
209
+ result["files"]["clip"] = clip["file"]
210
+ result["clip"] = {k: clip[k] for k in ("x", "y", "w", "h", "ink")}
211
+ if "png" in formats:
212
+ pngs = R.write_pngs(item, out / f"{base}_quadros")
213
+ result["files"]["png"] = str(out / f"{base}_quadros")
214
+ result["png_count"] = len(pngs)
215
+ if "mp4" in formats:
216
+ result["files"]["mp4"] = str(R.write_mp4(item, out / f"{base}.mp4", args.fundo))
217
+ if "sheet" in formats:
218
+ result["files"]["sheet"] = str(R.contact_sheet(item, out / f"{base}_folha.png", args.quadros, args.fundo))
219
+ result["seconds"] = round(time.time() - t0, 2)
220
+ for kind, path in result["files"].items():
221
+ print(f"ok {kind}: {path}")
222
+ sounds = ", ".join(f"{c['name']} no quadro {c['frame']}" for c in result["sfx"]) or "nenhum"
223
+ print(f"sons sugeridos: {sounds}")
224
+ for w in item.warnings:
225
+ print(f"aviso: {w}")
226
+ print(f"pronto em {result['seconds']:.1f} s")
227
+ print(json.dumps(result, ensure_ascii=False))
228
+ return 0
229
+ except MotionError as e:
230
+ print(f"erro: {e}", file=sys.stderr)
231
+ return e.code
232
+ except KeyboardInterrupt:
233
+ print("interrompido", file=sys.stderr)
234
+ return 130
235
+ except OSError as e:
236
+ print(f"erro: {e}", file=sys.stderr)
237
+ return 1
238
+
239
+
240
+ if __name__ == "__main__":
241
+ sys.exit(main())