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.
- package/LICENSES/edit-ia-pro.txt +32 -0
- package/LICENSES/fontes-OFL.txt +476 -0
- package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
- package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
- package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
- package/LICENSES/three-MIT.txt +23 -0
- package/LICENSES/video-use-MIT.txt +24 -0
- package/LICENSES/woven-sfx-CC0.txt +16 -0
- package/LICENSES/yunet-MIT.txt +21 -0
- package/README.md +2 -2
- package/bin/primocode.js +11 -2
- package/editor/LICENSE.md +11 -0
- package/editor/README.md +321 -0
- package/editor/SKILL.md +307 -0
- package/editor/THIRD_PARTY_NOTICES.md +228 -0
- package/editor/assets/baixar.json +591 -0
- package/editor/assets/fonts/anton/OFL.txt +93 -0
- package/editor/assets/fonts/caveat/OFL.txt +93 -0
- package/editor/assets/fonts/figtree/OFL.txt +93 -0
- package/editor/assets/fonts/fonts.json +10 -0
- package/editor/assets/fonts/montserrat/OFL.txt +93 -0
- package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
- package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
- package/editor/assets/models/YUNET_LICENSE.txt +21 -0
- package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
- package/editor/assets/sfx/licencas/cash.txt +76 -0
- package/editor/assets/sfx/licencas/charge.txt +31 -0
- package/editor/assets/sfx/licencas/click.txt +46 -0
- package/editor/assets/sfx/licencas/coin.txt +50 -0
- package/editor/assets/sfx/licencas/count.txt +47 -0
- package/editor/assets/sfx/licencas/data.txt +31 -0
- package/editor/assets/sfx/licencas/error.txt +32 -0
- package/editor/assets/sfx/licencas/flash.txt +31 -0
- package/editor/assets/sfx/licencas/gears.txt +82 -0
- package/editor/assets/sfx/licencas/keyboard.txt +48 -0
- package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
- package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
- package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
- package/editor/assets/sfx/licencas/notification.txt +54 -0
- package/editor/assets/sfx/licencas/pop.txt +38 -0
- package/editor/assets/sfx/licencas/rewind.txt +32 -0
- package/editor/assets/sfx/licencas/rise.txt +46 -0
- package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
- package/editor/assets/sfx/licencas/snap.txt +50 -0
- package/editor/assets/sfx/licencas/success.txt +40 -0
- package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
- package/editor/assets/sfx/licencas/tick.txt +14 -0
- package/editor/assets/sfx/licencas/whoosh.txt +47 -0
- package/editor/assets/sfx/licencas/wind.txt +31 -0
- package/editor/assets/sfx/sfx.json +832 -0
- package/editor/assets/sfx/sfx_sinteticos.json +317 -0
- package/editor/assets/sfx/sfx_woven.json +387 -0
- package/editor/assets/sfx/woven/LICENCA.md +16 -0
- package/editor/fichas/briefing-do-video.md +45 -0
- package/editor/fichas/conferencia-da-amostra.md +56 -0
- package/editor/fichas/matriz-de-ganchos.csv +3 -0
- package/editor/fichas/padrao.json +16 -0
- package/editor/fichas/planilha-de-cenas.csv +4 -0
- package/editor/fichas/planilha-de-versoes.csv +4 -0
- package/editor/fichas/plano-em-blocos.md +32 -0
- package/editor/fichas/registro-de-materiais.json +27 -0
- package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
- package/editor/helpers/bordas_precisas.py +272 -0
- package/editor/helpers/camera_viva.py +426 -0
- package/editor/helpers/captions.py +601 -0
- package/editor/helpers/common.py +308 -0
- package/editor/helpers/compose_final.py +3555 -0
- package/editor/helpers/conferir_corte.py +234 -0
- package/editor/helpers/conferir_dados.py +405 -0
- package/editor/helpers/contact_sheet.py +282 -0
- package/editor/helpers/corte_por_onda.py +433 -0
- package/editor/helpers/corte_seco.py +595 -0
- package/editor/helpers/denoise.py +971 -0
- package/editor/helpers/efeitos.py +408 -0
- package/editor/helpers/efeitos_gerar.py +159 -0
- package/editor/helpers/elevenlabs_api.py +442 -0
- package/editor/helpers/face_layout.py +570 -0
- package/editor/helpers/fetch_images.py +423 -0
- package/editor/helpers/ganho_por_trecho.py +214 -0
- package/editor/helpers/gen_image.py +312 -0
- package/editor/helpers/gen_music.py +316 -0
- package/editor/helpers/gen_sfx.py +148 -0
- package/editor/helpers/grade.py +307 -0
- package/editor/helpers/icloud.py +119 -0
- package/editor/helpers/make_sfx.py +940 -0
- package/editor/helpers/matte.py +1342 -0
- package/editor/helpers/medir_referencia.py +2232 -0
- package/editor/helpers/mixar_trilha.py +354 -0
- package/editor/helpers/montar_corte.py +295 -0
- package/editor/helpers/motion/__init__.py +13 -0
- package/editor/helpers/motion/engine.py +1915 -0
- package/editor/helpers/motion/registry.py +510 -0
- package/editor/helpers/motion/render.py +665 -0
- package/editor/helpers/motion/templates/barra_progresso.py +370 -0
- package/editor/helpers/motion/templates/carimbo.py +132 -0
- package/editor/helpers/motion/templates/checklist.py +271 -0
- package/editor/helpers/motion/templates/comentario_cta.py +380 -0
- package/editor/helpers/motion/templates/contador.py +222 -0
- package/editor/helpers/motion/templates/destaque.py +263 -0
- package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
- package/editor/helpers/motion/templates/grafico_barras.py +510 -0
- package/editor/helpers/motion/templates/grafico_linha.py +462 -0
- package/editor/helpers/motion/templates/notificacao.py +266 -0
- package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
- package/editor/helpers/motion/templates/passos.py +321 -0
- package/editor/helpers/motion/templates/riscado.py +298 -0
- package/editor/helpers/motion/templates/selo_garantia.py +150 -0
- package/editor/helpers/motion_render.py +241 -0
- package/editor/helpers/motion_sugerir.py +961 -0
- package/editor/helpers/noise_check.py +134 -0
- package/editor/helpers/pack_transcripts.py +223 -0
- package/editor/helpers/palavras_na_onda.py +176 -0
- package/editor/helpers/plano_sfx.py +839 -0
- package/editor/helpers/portao.py +427 -0
- package/editor/helpers/preview_server.py +1177 -0
- package/editor/helpers/rastrear_rosto.py +298 -0
- package/editor/helpers/render_cut.py +1256 -0
- package/editor/helpers/respiros.py +89 -0
- package/editor/helpers/sincronizar_cameras.py +571 -0
- package/editor/helpers/speech_regions.py +423 -0
- package/editor/helpers/style_catalog.json +231 -0
- package/editor/helpers/style_samples.py +246 -0
- package/editor/helpers/tela_verde.py +2113 -0
- package/editor/helpers/text_render.py +2059 -0
- package/editor/helpers/texto_na_tela.py +366 -0
- package/editor/helpers/timeline_view.py +292 -0
- package/editor/helpers/tira_de_quadros.py +113 -0
- package/editor/helpers/transcribe.py +734 -0
- package/editor/helpers/transcribe_batch.py +129 -0
- package/editor/helpers/trilhas.py +1146 -0
- package/editor/helpers/trilhas_humores.json +66 -0
- package/editor/helpers/valores.py +187 -0
- package/editor/helpers/verify_cut.py +820 -0
- package/editor/helpers/voice_levels.py +233 -0
- package/editor/helpers/voz_check.py +386 -0
- package/editor/helpers/voz_limpa.py +81 -0
- package/editor/helpers/watch_preview.py +372 -0
- package/editor/modelo-projeto/LEIA.md +402 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
- package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
- package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
- package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
- package/editor/modelo-projeto/assets/three/LICENSE +21 -0
- package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
- package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
- package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
- package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
- package/editor/modelo-projeto/build.py +4828 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
- package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
- package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
- package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
- package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
- package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
- package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
- package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
- package/editor/modelo-projeto/hyperframes.json +12 -0
- package/editor/modelo-projeto/package.json +11 -0
- package/editor/modelo-projeto/projeto.json +16 -0
- package/editor/preview/app.css +1009 -0
- package/editor/preview/app.js +2122 -0
- package/editor/preview/index.html +307 -0
- package/editor/pyproject.toml +23 -0
- package/editor/references/3d.md +236 -0
- package/editor/references/animacao-catalogo.md +672 -0
- package/editor/references/animacao.md +498 -0
- package/editor/references/comandos.md +92 -0
- package/editor/references/corte-seco-como-funciona.md +50 -0
- package/editor/references/corte-seco.md +88 -0
- package/editor/references/corte.md +290 -0
- package/editor/references/direcao-por-blocos.md +218 -0
- package/editor/references/enquadramento.md +654 -0
- package/editor/references/estilos-prontos.md +355 -0
- package/editor/references/estudio.md +100 -0
- package/editor/references/exemplo-eventos.json +67 -0
- package/editor/references/lettering.md +252 -0
- package/editor/references/modelos.md +494 -0
- package/editor/references/phase2.md +1007 -0
- package/editor/references/referencia-aplicar.md +203 -0
- package/editor/references/referencia-medir.md +375 -0
- package/editor/references/revisao.md +335 -0
- package/editor/references/som-e-trilha.md +146 -0
- package/editor/references/tabela-de-sons.md +130 -0
- package/editor/references/variacoes.md +234 -0
- package/editor/uv.lock +1141 -0
- package/lib/act.js +36 -8
- package/lib/catalogo.js +4 -0
- package/lib/chamada-torta.js +81 -0
- package/lib/editor-pro.js +786 -0
- package/lib/entrega.js +58 -0
- package/lib/estudio/conferencia-visual.js +76 -0
- package/lib/estudio/diretor.js +6 -1
- package/lib/estudio/exportar.js +3 -1
- package/lib/estudio/gosto.js +45 -4
- package/lib/estudio/index.js +8 -5
- package/lib/estudio/kit-ae/base.jsx +119 -13
- package/lib/estudio/kit-ae/cenas.jsx +47 -15
- package/lib/estudio/kit-ae/guarda.jsx +274 -0
- package/lib/estudio/kit-ae/primitivas.jsx +180 -24
- package/lib/estudio/maquina-diretor.js +61 -7
- package/lib/estudio/maquina-foto.js +177 -0
- package/lib/estudio/maquina-guarda.js +207 -0
- package/lib/estudio/maquina-layout.js +22 -1
- package/lib/estudio/maquina.js +60 -11
- package/lib/estudio/referencias.js +1 -1
- package/lib/estudio/visao/visao.swift +97 -0
- package/lib/estudio/visao.js +60 -0
- package/lib/local/cerebro.json +192 -6
- package/lib/local/motor.js +5 -2
- package/lib/nuvem.js +11 -1
- package/lib/passagem.js +50 -0
- package/lib/perigo.js +19 -11
- package/lib/protecao.js +134 -0
- package/lib/titulo-conversa.js +46 -0
- package/lib/tools.js +3 -0
- package/lib/ui.js +1 -1
- package/package.json +3 -2
- package/skills/editar-meu-video/SKILL.md +6 -2
- package/skills/editar-video/SKILL.md +113 -0
- package/studio/pessoa.py +52 -0
- package/voz/primo_voz/app.py +43 -4
- package/voz/primo_voz/chat.html +17 -6
- 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())
|