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,222 @@
1
+ """contador: a number that counts up (or down) to its value, with prefix, suffix and a label.
2
+
3
+ REFERENCE TEMPLATE. Copy this file to write a new one (SPEC Contract 12):
4
+ 1. TEMPLATE describes the template (id = file name) and its params; the registry checks it.
5
+ 2. render(frame_index, fps, params, box) draws ONE frame on a fresh Canvas and returns it.
6
+ Time is frame_index / fps. `params` is already complete and checked. `box` gives the place
7
+ (design px, 1080 wide), the theme colors and the item length (box.frames).
8
+ 3. Optional: sfx(params, fps, frames), suggested_duration(params), settle(params, fps).
9
+ Keep the layout math in a cached function (it runs once per item, not once per frame).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from functools import lru_cache
16
+
17
+ from motion import engine as E
18
+
19
+ TEMPLATE = {
20
+ "id": "contador",
21
+ "label_pt": "Contador de número",
22
+ "description_pt": "Um número que sobe até o valor final num cartão, com prefixo (R$), sufixo (%, x, pessoas) "
23
+ "e uma legenda curta embaixo. Uma barra acompanha a contagem e o número dá um pulinho no fim.",
24
+ "use_when_pt": "Quando a fala cita um número que precisa ficar na cabeça: quantidade de alunos, porcentagem, "
25
+ "tempo, preço, faturamento. Use o número que a pessoa disse ou mostrou, sem inventar.",
26
+ "category": "números",
27
+ "version": 1,
28
+ "params": {
29
+ "value": {"type": "number", "default": 1000, "min": -1e13, "max": 1e13,
30
+ "label_pt": "Número final (use ponto para decimais: 1904152.87)"},
31
+ "start_value": {"type": "number", "default": 0, "min": -1e13, "max": 1e13,
32
+ "label_pt": "Número onde a contagem começa"},
33
+ "prefix": {"type": "text", "default": "", "max_len": 6, "label_pt": "Antes do número, ex.: R$ ou +"},
34
+ "suffix": {"type": "text", "default": "", "max_len": 12, "label_pt": "Depois do número, ex.: %, x ou pessoas"},
35
+ "decimals": {"type": "integer", "default": None, "nullable": True, "min": 0, "max": 3,
36
+ "label_pt": "Casas decimais (vazio: automático; dinheiro com centavos usa 2)"},
37
+ "label": {"type": "text", "default": "", "max_len": 40, "label_pt": "Legenda curta embaixo do número"},
38
+ "duration": {"type": "number", "default": 1.4, "min": 0.3, "max": 10,
39
+ "label_pt": "Segundos da contagem"},
40
+ "delay": {"type": "number", "default": 0.15, "min": 0, "max": 5,
41
+ "label_pt": "Segundos antes de começar a contar"},
42
+ "ease": {"type": "choice", "choices": ["suave", "linear"], "default": "suave",
43
+ "label_pt": "Ritmo: suave (rápido e freia no fim) ou linear (constante, bom para tempo)"},
44
+ "size": {"type": "choice", "choices": ["normal", "grande"], "default": "normal",
45
+ "label_pt": "Tamanho do número"},
46
+ "card": {"type": "bool", "default": True, "label_pt": "Cartão atrás do número (sem cartão: contorno e sombra)"},
47
+ "bar": {"type": "bool", "default": True, "label_pt": "Barra que enche junto com a contagem"},
48
+ },
49
+ "default_duration": 3.0,
50
+ "default_region": "centro",
51
+ "sfx": [{"at": 0.0, "name": "pop"}, {"at": 0.15, "name": "count", "volume": 1.3}],
52
+ "example": {"value": 1250, "suffix": "alunos", "label": "na primeira turma"},
53
+ }
54
+
55
+ LAND = 0.32 # seconds of the landing pop
56
+
57
+
58
+ def _decimals(p: dict) -> int:
59
+ if p["decimals"] is not None:
60
+ return int(p["decimals"])
61
+ money = E.looks_like_money(p["prefix"], p["suffix"])
62
+ d = max(E.auto_decimals(p["value"]), E.auto_decimals(p["start_value"]))
63
+ return 2 if money and d else d
64
+
65
+
66
+ def _affix_scale(text: str) -> float:
67
+ """Small symbols (%, x, +) sit almost at number size; words and currencies are smaller."""
68
+ return 0.82 if len(text.strip()) <= 1 else 0.52
69
+
70
+
71
+ @lru_cache(maxsize=64)
72
+ def _layout(params_json: str, box: E.Box) -> dict:
73
+ p = json.loads(params_json)
74
+ dec = _decimals(p)
75
+ prefix, suffix = p["prefix"].strip(), p["suffix"].strip()
76
+ label = p["label"].strip()
77
+ card = p["card"]
78
+ theme = box.theme
79
+ pad_x = 64.0 if card else 12.0
80
+ max_size = 200.0 if p["size"] == "grande" else 150.0
81
+
82
+ widest = max((E.format_number(v, dec) for v in (p["value"], p["start_value"])),
83
+ key=lambda s: E.measure(s, "sans_black", 100, tabular=True).width)
84
+
85
+ def block(size: float) -> dict:
86
+ num_w = E.measure(widest, "sans_black", size, tabular=True).width
87
+ pre_size, suf_size = size * _affix_scale(prefix), size * _affix_scale(suffix)
88
+ pre_w = E.measure(prefix, "sans_black", pre_size).width if prefix else 0.0
89
+ suf_w = E.measure(suffix, "sans_black", suf_size).width if suffix else 0.0
90
+ gap_p = size * (0.02 if len(prefix) <= 1 else 0.14) if prefix else 0.0
91
+ gap_s = size * (0.02 if len(suffix) <= 1 else 0.14) if suffix else 0.0
92
+ return {"size": size, "num_w": num_w, "pre_size": pre_size, "suf_size": suf_size, "pre_w": pre_w,
93
+ "suf_w": suf_w, "gap_p": gap_p, "gap_s": gap_s,
94
+ "width": pre_w + gap_p + num_w + gap_s + suf_w}
95
+
96
+ avail = box.w - 2 * pad_x
97
+ b = block(max_size)
98
+ if b["width"] > avail:
99
+ b = block(max(44.0, max_size * avail / b["width"] * 0.995))
100
+ size = b["size"]
101
+ cap = E.measure("0", "sans_black", size).cap
102
+ label_size = max(36.0, min(54.0, size * 0.40))
103
+ if label:
104
+ label_size = E.fit_text(label, "text_semibold", avail, label_size, 24.0)
105
+ label_cap = E.measure("H", "text_semibold", label_size).cap if label else 0.0
106
+ bar_h = max(8.0, size * 0.07) if p["bar"] else 0.0
107
+ gap_bar = size * 0.30 if p["bar"] else 0.0
108
+ gap_label = (size * 0.26 if p["bar"] else size * 0.30) if label else 0.0
109
+ pad_y = max(52.0, size * 0.50) if card else 0.0
110
+ content_h = cap + gap_bar + bar_h + gap_label + label_cap + (label_size * 0.25 if label else 0.0)
111
+ card_h = content_h + 2 * pad_y
112
+ card_w = min(box.w, max(b["width"], E.measure(label, "text_semibold", label_size).width if label else 0.0)
113
+ + 2 * pad_x)
114
+ top = (box.h - card_h) / 2.0
115
+ x0 = (box.w - b["width"]) / 2.0
116
+ baseline = top + pad_y + cap
117
+ bar_y = baseline + gap_bar
118
+ number_color = "accent"
119
+ if card and E.contrast(theme.accent, theme.card) < 2.4:
120
+ number_color = "text" # a light accent on a white card: the bar keeps the accent
121
+ return {
122
+ "dec": dec, "prefix": prefix, "suffix": suffix, "label": label, "card": card,
123
+ "size": size, "cap": cap, "block": b, "x0": x0, "baseline": baseline,
124
+ "bar_y": bar_y, "bar_h": bar_h, "label_size": label_size,
125
+ "label_base": bar_y + bar_h + gap_label + label_cap,
126
+ "card_box": ((box.w - card_w) / 2.0, top, card_w, card_h), "number_color": number_color,
127
+ "radius": min(48.0, card_h * 0.22),
128
+ }
129
+
130
+
131
+ def _count(p: dict, t: float) -> float:
132
+ fn = "linear" if p["ease"] == "linear" else "out_expo"
133
+ return E.eased(t, p["delay"], p["duration"], fn)
134
+
135
+
136
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
137
+ cv = E.Canvas(box)
138
+ t = frame_index / fps
139
+ L = _layout(json.dumps(params, sort_keys=True), box)
140
+ b = L["block"]
141
+ t_end = params["delay"] + params["duration"]
142
+
143
+ # entrance of the whole card: fade, rise and a small overshoot. It starts one frame early, so
144
+ # frame 0 (where the entrance sound lands) already shows the card.
145
+ t_in = t + 1.0 / fps
146
+ alpha = E.eased(t_in, 0.0, 0.18, "out_quad")
147
+ scale = 0.88 + 0.12 * E.out_back(E.progress(t_in, 0.0, 0.5), 1.4)
148
+ rise = 36.0 * (1.0 - E.eased(t_in, 0.0, 0.45))
149
+ with cv.group(opacity=alpha, scale=scale, dy=rise):
150
+ cx0, cy0, cw, ch = L["card_box"]
151
+ if L["card"]:
152
+ cv.card(cx0, cy0, cw, ch, radius=L["radius"])
153
+
154
+ p = _count(params, t)
155
+ value = params["start_value"] + (params["value"] - params["start_value"]) * p
156
+ digits = E.format_number(value, L["dec"])
157
+ size = L["size"]
158
+ prefix, pre_w = L["prefix"], b["pre_w"]
159
+ if prefix and digits.startswith("-"):
160
+ # a negative amount is written -R$ 37,00, not R$ -37,00
161
+ prefix, digits = ("-" if prefix == "+" else "-" + prefix), digits[1:]
162
+ pre_w = E.measure(prefix, "sans_black", b["pre_size"]).width
163
+ num_w = E.measure(digits, "sans_black", size, tabular=True).width
164
+ # the line is centered on what it shows now; tabular digits keep it still once the digit count is final
165
+ x_pre = (cv.w - (pre_w + b["gap_p"] + num_w + b["gap_s"] + b["suf_w"])) / 2.0
166
+ x_num = x_pre + pre_w + b["gap_p"]
167
+ base = L["baseline"]
168
+ outline = {} if L["card"] else {"stroke": size * 0.05, "stroke_color": "black", "shadow": E.TEXT_SHADOW}
169
+
170
+ land = E.pulse(t, t_end - 0.02, LAND)
171
+ pivot = (cv.cx, base - L["cap"] / 2.0)
172
+
173
+ def number() -> None:
174
+ cv.text(x_num, base, digits, "sans_black", size, color=L["number_color"], tabular=True, **outline)
175
+ if prefix:
176
+ cv.text(x_pre, base, prefix, "sans_black", b["pre_size"], color="text", opacity=0.92, **outline)
177
+ if L["suffix"]:
178
+ cv.text(x_num + num_w + b["gap_s"], base, L["suffix"], "sans_black", b["suf_size"], color="text",
179
+ opacity=0.92, **outline)
180
+
181
+ with cv.group(scale=1.0 + 0.07 * land, pivot=pivot):
182
+ if land > 0.02:
183
+ cv.glow(number, radius=30, opacity=0.5 * land)
184
+ else:
185
+ number()
186
+
187
+ if L["bar_h"]:
188
+ bw, bh = b["width"], L["bar_h"]
189
+ bx = L["x0"]
190
+ track = "track" if L["card"] else (0, 0, 0, 110)
191
+ cv.pill(bx, L["bar_y"], bw, bh, fill=track)
192
+ if p > 0:
193
+ cv.pill(bx, L["bar_y"], max(bh, bw * p), bh, fill="accent")
194
+
195
+ if L["label"]:
196
+ la = E.eased(t, 0.12, 0.35)
197
+ if cv.visible(la):
198
+ cv.text(cv.cx, L["label_base"] + 14.0 * (1.0 - la), L["label"], "text_semibold", L["label_size"],
199
+ color="muted" if L["card"] else "text", opacity=la, anchor="ms",
200
+ **({} if L["card"] else {"stroke": L["label_size"] * 0.08, "stroke_color": "black"}))
201
+ return cv
202
+
203
+
204
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
205
+ cues = [{"at": 0.0, "name": "pop", "why": "entrada"}]
206
+ if params["value"] != params["start_value"]:
207
+ start, dur = params["delay"], params["duration"]
208
+ # the ticking follows the count: once for the fast "suave" count, every 0.6 s for a linear one
209
+ steps = 1 if params["ease"] != "linear" else max(1, int(dur / 0.6 + 0.5))
210
+ for k in range(steps):
211
+ cues.append({"at": start + k * 0.6, "name": "count", "volume": 1.3, "why": "contagem"})
212
+ return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
213
+
214
+
215
+ def suggested_duration(params: dict) -> float:
216
+ """Count, landing, and about 1.5 s to read the number."""
217
+ return round(max(TEMPLATE["default_duration"], params["delay"] + params["duration"] + LAND + 1.5), 2)
218
+
219
+
220
+ def settle(params: dict, fps: int) -> float:
221
+ """From here on every frame is the same (until the exit)."""
222
+ return params["delay"] + params["duration"] + LAND + 0.05
@@ -0,0 +1,263 @@
1
+ """destaque: a marker stroke that draws itself at a place on the screen: a circle around it, an underline
2
+ under it or an arrow pointing at it, with an optional handwritten note.
3
+
4
+ The place is `target` = [x, y, w, h] in design px of the output canvas (1080x1920 for vertical video),
5
+ measured on a frame of the video. The template never moves (`movable: False`), draws on the whole safe
6
+ area ("tela") and uses a large bleed so a target near the edges is still covered.
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
+ from motion.engine import MotionError
17
+
18
+ TEMPLATE = {
19
+ "id": "destaque",
20
+ "label_pt": "Destaque desenhado",
21
+ "description_pt": "Um traço de caneta que se desenha sozinho num ponto da tela: círculo em volta, sublinhado "
22
+ "embaixo ou seta apontando para a área escolhida. Pode ter um texto escrito à mão ao lado.",
23
+ "use_when_pt": "Para levar o olho a algo que já aparece no vídeo: um preço, um botão, um número num print, uma "
24
+ "palavra na tela. Meça a área num quadro do vídeo (pixels da tela 1080x1920) e passe em target.",
25
+ "notes_pt": "Fica exatamente no alvo e não sai do lugar para fugir do rosto. Se o alvo se mexe no vídeo, o "
26
+ "traço não acompanha: use num trecho parado. A cor do traço é a cor de destaque do vídeo; troque "
27
+ "só neste item com cor (ex.: vermelho).",
28
+ "category": "marcações",
29
+ "version": 1,
30
+ "params": {
31
+ "target": {"type": "box", "default": [290, 1040, 500, 160],
32
+ "label_pt": "Área a destacar: [x, y, largura, altura] em pixels da tela 1080x1920"},
33
+ "mode": {"type": "choice", "choices": ["circulo", "sublinhado", "seta"], "default": "circulo",
34
+ "label_pt": "Traço: círculo em volta, sublinhado embaixo ou seta apontando"},
35
+ "side": {"type": "choice", "choices": ["auto", "baixo", "cima", "esquerda", "direita"], "default": "auto",
36
+ "label_pt": "Só para a seta: de que lado ela vem (auto: o lado com mais espaço na tela; sem "
37
+ "espaço no lado pedido, usa o de mais espaço)"},
38
+ "label": {"type": "text", "default": "", "max_len": 22,
39
+ "label_pt": "Texto escrito à mão junto do traço (opcional), ex.: olha isso"},
40
+ "thickness": {"type": "number", "default": 12, "min": 4, "max": 32,
41
+ "label_pt": "Espessura do traço em pixels"},
42
+ "duration": {"type": "number", "default": 0.6, "min": 0.2, "max": 3,
43
+ "label_pt": "Segundos para desenhar o traço"},
44
+ "shadow": {"type": "bool", "default": True,
45
+ "label_pt": "Sombra leve atrás do traço (ajuda a ler em fundo claro)"},
46
+ },
47
+ "default_duration": 2.0,
48
+ "default_region": "tela",
49
+ "movable": False,
50
+ "bleed": 160,
51
+ "sfx": [{"at": 0.15, "name": "whoosh", "volume": 0.7}],
52
+ "example": {"target": [240, 900, 600, 200], "mode": "circulo", "label": "olha isso"},
53
+ }
54
+
55
+ LABEL_FONT = "marker"
56
+ LABEL_SIZE = 72.0
57
+ EDGE = 14.0 # the stroke keeps this far from the screen edges
58
+ LABEL_IN = 0.32
59
+ SIDES = ("baixo", "cima", "esquerda", "direita")
60
+ MIN_ROOM = 90.0 # a forced arrow side with less room than this falls back to the side with the most room
61
+ LABEL_APPEAR = {"circulo": 0.55, "sublinhado": 0.7, "seta": 0.0} # fraction of the draw time
62
+
63
+
64
+ def _smooth(x: float) -> float:
65
+ x = E.clamp01(x)
66
+ return x * x * (3 - 2 * x)
67
+
68
+
69
+ def _draw_curve(u: float) -> float:
70
+ """A hand stroke: leaves fast and eases into the end."""
71
+ return 0.6 * E.out_cubic(u) + 0.4 * E.in_out_cubic(u)
72
+
73
+
74
+ def _taper(n: int, start: float = 0.55, end: float = 0.55) -> list[float]:
75
+ """Width factor per point: thin at the pen-down, full in the middle, thinning at the lift."""
76
+ out = []
77
+ for k in range(n):
78
+ u = k / max(1, n - 1)
79
+ out.append((start + (1 - start) * _smooth(u / 0.14)) * (1 - (1 - end) * _smooth((u - 0.82) / 0.18)))
80
+ return out
81
+
82
+
83
+ def _rotate(points, cx: float, cy: float, deg: float):
84
+ a = math.radians(deg)
85
+ c, s = math.cos(a), math.sin(a)
86
+ return [(cx + (x - cx) * c - (y - cy) * s, cy + (x - cx) * s + (y - cy) * c) for x, y in points]
87
+
88
+
89
+ def _check_target(target, design) -> None:
90
+ x, y, w, h = target
91
+ dw, dh = design
92
+ if x + w <= 0 or y + h <= 0 or x >= dw or y >= dh:
93
+ raise MotionError(f"destaque.params.target [{x:g}, {y:g}, {w:g}, {h:g}] fica fora da tela "
94
+ f"({dw:.0f}x{dh:.0f}); meça a área num quadro do vídeo")
95
+
96
+
97
+ def _pick_side(side: str, target, safe) -> tuple[str, dict]:
98
+ x, y, w, h = target
99
+ sx0, sy0, sx1, sy1 = safe
100
+ room = {"baixo": sy1 - (y + h), "cima": y - sy0, "esquerda": x - sx0, "direita": sx1 - (x + w)}
101
+ if side != "auto" and room[side] >= MIN_ROOM:
102
+ return side, room
103
+ # vertical arrows read more naturally on a phone: horizontal ones need clearly more room to win
104
+ weight = {"baixo": 1.0, "cima": 0.95, "esquerda": 0.8, "direita": 0.8}
105
+ return max(SIDES, key=lambda k: (room[k] * weight[k], -SIDES.index(k))), room
106
+
107
+
108
+ @lru_cache(maxsize=64)
109
+ def _geometry(params_json: str, box: E.Box) -> dict:
110
+ """Stroke paths and the label place, in CANVAS design px (converted to box-local when drawing)."""
111
+ p = json.loads(params_json)
112
+ target = p["target"]
113
+ dw, dh = box.design
114
+ _check_target(target, (dw, dh))
115
+ tx, ty, tw, th = target
116
+ cx, cy = tx + tw / 2.0, ty + th / 2.0
117
+ thick = p["thickness"]
118
+ seed = E.stable_seed("destaque", tuple(target), p["mode"])
119
+ safe = E.safe_area(box.canvas)
120
+ label = p["label"].strip()
121
+ lsize = 0.0
122
+ lw = lcap = 0.0
123
+ if label:
124
+ lsize = E.fit_text(label, LABEL_FONT, (safe[2] - safe[0]) * 0.9, LABEL_SIZE, 30.0)
125
+ m = E.measure(label, LABEL_FONT, lsize)
126
+ lw, lcap = m.width, m.cap
127
+ strokes = [] # (points, width factors, start fraction, end fraction of the draw time)
128
+ label_at = None # (x center, y baseline, rotation, appear time fraction)
129
+
130
+ if p["mode"] == "circulo":
131
+ pad = thick * 0.5 + 14.0
132
+ rx = tw / 2.0 * 1.22 + pad
133
+ ry = th / 2.0 * 1.22 + pad
134
+ rx = min(rx, max(tw / 2.0 + thick, min(cx, dw - cx) - EDGE))
135
+ ry = min(ry, max(th / 2.0 + thick, min(cy, dh - cy) - EDGE))
136
+ pts = E.ellipse_points(cx, cy, rx, ry, start_deg=-128.0, sweep_deg=382.0, n=72, wobble=0.035, seed=seed)
137
+ pts = _rotate(pts, cx, cy, -3.0)
138
+ strokes.append((pts, _taper(len(pts), 0.5, 0.45), 0.0, 1.0))
139
+ if label:
140
+ above = cy - ry - 22.0
141
+ if above - lcap - 30.0 >= safe[1]:
142
+ label_at = (cx + rx * 0.25, above, -5.0, LABEL_APPEAR["circulo"])
143
+ else:
144
+ label_at = (cx + rx * 0.25, cy + ry + 22.0 + lcap, -5.0, LABEL_APPEAR["circulo"])
145
+ elif p["mode"] == "sublinhado":
146
+ gap = thick * 0.6 + 12.0
147
+ x0 = max(EDGE, tx - tw * 0.03)
148
+ x1 = min(dw - EDGE, tx + tw * 1.03)
149
+ rise = min(24.0, (x1 - x0) * 0.045)
150
+ y = ty + th + gap + rise # the rising end stays under the target
151
+ pts = E.bezier_points((x0, y + rise * 0.15), ((x0 + x1) / 2.0, y + rise * 0.75), (x1, y - rise), n=40)
152
+ pts = E.wobble_points(pts, max(0.8, thick * 0.10), seed=seed, step=14.0)
153
+ strokes.append((pts, _taper(len(pts), 0.55, 0.5), 0.0, 0.78))
154
+ # a second, shorter swash under the first: the classic marker emphasis
155
+ y2 = y + thick * 1.35 + 6.0
156
+ a2, b2 = x0 + (x1 - x0) * 0.10, x0 + (x1 - x0) * 0.86
157
+ pts2 = E.bezier_points((a2, y2 + rise * 0.1), ((a2 + b2) / 2.0, y2 + rise * 0.55), (b2, y2 - rise * 0.7), n=30)
158
+ pts2 = E.wobble_points(pts2, max(0.8, thick * 0.08), seed=seed + 1, step=14.0)
159
+ strokes.append((pts2, [w * 0.72 for w in _taper(len(pts2), 0.55, 0.45)], 0.62, 1.0))
160
+ if label:
161
+ below = y2 + thick + 22.0 + lcap
162
+ if below + 20.0 <= safe[3]:
163
+ label_at = (cx, below, -4.0, LABEL_APPEAR["sublinhado"])
164
+ else:
165
+ label_at = (cx, ty - 24.0, -4.0, LABEL_APPEAR["sublinhado"])
166
+ else:
167
+ side, room = _pick_side(p["side"], target, safe)
168
+ g = 16.0 + thick
169
+ label_room = (lcap + 40.0) if label else 10.0
170
+ length = max(110.0, min(300.0, room[side] - g - label_room))
171
+ right_half = cx > dw / 2.0
172
+ if side in ("baixo", "cima"):
173
+ sign = 1.0 if side == "baixo" else -1.0
174
+ tip = (cx, (ty + th + g) if side == "baixo" else (ty - g))
175
+ lateral = -1.0 if right_half else 1.0
176
+ tail = (tip[0] + lateral * length * 0.42, tip[1] + sign * length)
177
+ else:
178
+ sign = -1.0 if side == "esquerda" else 1.0
179
+ tip = ((tx - g) if side == "esquerda" else (tx + tw + g), cy)
180
+ down = 1.0 if room["baixo"] >= room["cima"] else -1.0
181
+ tail = (tip[0] + sign * length, tip[1] + down * length * 0.38)
182
+ # keep the tail on the screen (shorten along the arrow)
183
+ tail = (min(max(tail[0], safe[0] + EDGE), safe[2] - EDGE), min(max(tail[1], EDGE), dh - EDGE))
184
+ L = math.dist(tail, tip)
185
+ mx, my = (tail[0] + tip[0]) / 2.0, (tail[1] + tip[1]) / 2.0
186
+ nx, ny = -(tip[1] - tail[1]) / max(L, 1e-6), (tip[0] - tail[0]) / max(L, 1e-6)
187
+ bend = 0.22 * L * (1.0 if (side in ("baixo", "direita")) != right_half else -1.0)
188
+ shaft = E.bezier_points(tail, (mx + nx * bend, my + ny * bend), tip, n=40)
189
+ shaft = E.wobble_points(shaft, max(0.6, thick * 0.08), seed=seed, step=16.0)
190
+ shaft[-1] = tip
191
+ strokes.append((shaft, _taper(len(shaft), 0.6, 1.0), 0.0, 0.72))
192
+ (x_a, y_a), (x_b, y_b) = shaft[-4], shaft[-1]
193
+ ang = math.atan2(y_b - y_a, x_b - x_a)
194
+ hl = max(thick * 3.4, 40.0)
195
+ w1 = (tip[0] + hl * math.cos(ang + math.pi - math.radians(30)), tip[1] + hl * math.sin(ang + math.pi - math.radians(30)))
196
+ w2 = (tip[0] + hl * math.cos(ang + math.pi + math.radians(33)), tip[1] + hl * math.sin(ang + math.pi + math.radians(33)))
197
+ strokes.append(([w1, tip, w2], [0.8, 1.0, 0.85], 0.66, 1.0))
198
+ if label:
199
+ # on the far side of the tail from the shaft, nudged away from the tip on horizontal arrows
200
+ lx = tail[0] if side in ("baixo", "cima") else tail[0] + sign * lw * 0.25
201
+ if tail[1] < tip[1] - 1.0:
202
+ label_at = (lx, tail[1] - 24.0, -4.0, LABEL_APPEAR["seta"])
203
+ else:
204
+ label_at = (lx, tail[1] + 22.0 + lcap, -4.0, LABEL_APPEAR["seta"])
205
+ if label_at is not None:
206
+ lx, ly, rot, appear = label_at
207
+ lx = min(max(lx, safe[0] + lw / 2.0 + 6.0), safe[2] - lw / 2.0 - 6.0)
208
+ ly = min(max(ly, EDGE + lcap), dh - EDGE)
209
+ label_at = (lx, ly, rot, appear)
210
+ return {"strokes": strokes, "label": label, "label_size": lsize, "label_cap": lcap, "label_at": label_at,
211
+ "thick": thick}
212
+
213
+
214
+ def render(frame_index: int, fps: int, params: dict, box: E.Box) -> E.Canvas:
215
+ cv = E.Canvas(box)
216
+ t = frame_index / fps
217
+ t_in = t + 1.0 / fps
218
+ G = _geometry(json.dumps(params, sort_keys=True), box)
219
+ dur = params["duration"]
220
+ shadow = E.Shadow(blur=8.0, dy=3.0, opacity=0.45) if params["shadow"] else None
221
+ thick = G["thick"]
222
+ u = E.clamp01(t_in / dur)
223
+ for pts, widths, a, b in G["strokes"]:
224
+ prog = _draw_curve(E.remap(u, a, b))
225
+ if prog <= 0:
226
+ continue
227
+ local = [box.from_canvas(x, y) for x, y in pts]
228
+ cv.polyline(local, thick, color="accent", progress=prog, widths=[thick * w for w in widths],
229
+ shadow=shadow)
230
+ if G["label_at"] is not None:
231
+ lx, ly, rot, appear = G["label_at"]
232
+ la = E.progress(t_in, appear * dur, LABEL_IN)
233
+ if la > 0:
234
+ x, y = box.from_canvas(lx, ly)
235
+ pivot = (x, y - G["label_cap"] / 2.0)
236
+ with cv.group(opacity=E.out_quad(min(1.0, la * 1.6)), scale=0.85 + 0.15 * E.out_back(la, 2.0),
237
+ pivot=pivot, rotate=rot):
238
+ cv.text(x, y, G["label"], LABEL_FONT, G["label_size"], color="accent", anchor="ms",
239
+ stroke=4.0, stroke_color=(0, 0, 0, 150),
240
+ shadow=E.Shadow(blur=8.0, dy=3.0, opacity=0.5) if params["shadow"] else None)
241
+ return cv
242
+
243
+
244
+ def sfx(params: dict, fps: int, frames: int) -> list[dict]:
245
+ dur = params["duration"]
246
+ if params["mode"] == "seta":
247
+ cues = [{"at": dur * 0.2, "name": "whoosh", "volume": 0.7, "why": "seta se desenhando"},
248
+ {"at": dur * 0.72, "name": "pop", "volume": 0.8, "why": "ponta da seta"}]
249
+ else:
250
+ cues = [{"at": dur * 0.25, "name": "whoosh", "volume": 0.7, "why": "traço se desenhando"}]
251
+ return [{"frame": E.frame_of(c.pop("at"), fps), **c} for c in cues]
252
+
253
+
254
+ def suggested_duration(params: dict) -> float:
255
+ return round(max(TEMPLATE["default_duration"], params["duration"] + LABEL_IN + 1.4), 2)
256
+
257
+
258
+ def settle(params: dict, fps: int) -> float:
259
+ """The drawing runs on t + 1/fps: it is complete one frame before `end`; half a frame of margin for rounding."""
260
+ end = params["duration"]
261
+ if params["label"].strip():
262
+ end = max(end, LABEL_APPEAR[params["mode"]] * params["duration"] + LABEL_IN)
263
+ return end - 0.5 / fps + 0.01