primocode 8.38.0 → 8.39.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/app/app.js +6 -2
- package/app/primo-marca.js +32 -10
- package/bin/primocode.js +22 -15
- package/lib/act.js +87 -2
- package/lib/audio-livre.js +172 -0
- package/lib/boas-vindas.js +4 -4
- package/lib/catalogo.js +7 -0
- package/lib/marca.js +280 -0
- package/lib/referencia.js +210 -63
- package/lib/studio.js +22 -1
- package/lib/tools.js +10 -0
- package/package.json +2 -2
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/arquivos/tela-motion-3ecaa.json +135 -0
- package/studio/arquivos/tela-motion-d6150.json +135 -0
- package/studio/edge.py +66 -16
- package/studio/ferramentas/01-modelos.md +20 -2
- package/studio/ferramentas/corte.md +121 -11
- package/studio/ferramentas/tela.md +21 -0
- package/studio/server.py +47 -11
- package/studio/studio.log +28 -0
- package/studio/web/js/player.js +30 -2
- package/studio/web/js/render.js +74 -4
- package/studio/web/js/sfx.js +128 -2
- package/studio/web/render.css +95 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "tela-motion-d6150",
|
|
3
|
+
"ferramenta": "tela",
|
|
4
|
+
"titulo": "motion",
|
|
5
|
+
"doc": {
|
|
6
|
+
"tema": "meia-noite",
|
|
7
|
+
"largura": 1920,
|
|
8
|
+
"altura": 1080,
|
|
9
|
+
"cenas": [
|
|
10
|
+
{
|
|
11
|
+
"layout": "livre",
|
|
12
|
+
"duracao": 6,
|
|
13
|
+
"fundo": "aurora",
|
|
14
|
+
"elementos": [
|
|
15
|
+
{
|
|
16
|
+
"tipo": "forma",
|
|
17
|
+
"forma": "circulo",
|
|
18
|
+
"x": 1250,
|
|
19
|
+
"y": 120,
|
|
20
|
+
"l": 520,
|
|
21
|
+
"a": 520,
|
|
22
|
+
"preenchimento": "#ff5a36",
|
|
23
|
+
"opacidade": 0.9
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"tipo": "forma",
|
|
27
|
+
"forma": "retangulo",
|
|
28
|
+
"material": "vidro",
|
|
29
|
+
"x": 120,
|
|
30
|
+
"y": 140,
|
|
31
|
+
"l": 860,
|
|
32
|
+
"a": 420,
|
|
33
|
+
"raio": 36,
|
|
34
|
+
"preenchimento": "#22d3ee"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"tipo": "texto",
|
|
38
|
+
"x": 170,
|
|
39
|
+
"y": 190,
|
|
40
|
+
"l": 760,
|
|
41
|
+
"a": 120,
|
|
42
|
+
"texto": "Liquid glass de verdade",
|
|
43
|
+
"tamanho": 74,
|
|
44
|
+
"peso": 750
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"tipo": "texto",
|
|
48
|
+
"x": 170,
|
|
49
|
+
"y": 330,
|
|
50
|
+
"l": 760,
|
|
51
|
+
"a": 60,
|
|
52
|
+
"texto": "desfoca o que está atrás, bisel em cima",
|
|
53
|
+
"tamanho": 34,
|
|
54
|
+
"cor": "#bfe9f4"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"tipo": "texto",
|
|
58
|
+
"material": "vidro",
|
|
59
|
+
"x": 170,
|
|
60
|
+
"y": 430,
|
|
61
|
+
"l": 420,
|
|
62
|
+
"a": 70,
|
|
63
|
+
"texto": "pílula de vidro",
|
|
64
|
+
"tamanho": 32
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"tipo": "icone",
|
|
68
|
+
"nome": "foguete",
|
|
69
|
+
"estilo": "app",
|
|
70
|
+
"x": 1130,
|
|
71
|
+
"y": 640,
|
|
72
|
+
"tamanho": 220,
|
|
73
|
+
"fundo": "#8b5cf6",
|
|
74
|
+
"som": "pop"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"tipo": "icone",
|
|
78
|
+
"nome": "raio",
|
|
79
|
+
"estilo": "vidro",
|
|
80
|
+
"x": 1400,
|
|
81
|
+
"y": 640,
|
|
82
|
+
"tamanho": 220,
|
|
83
|
+
"cor": "#ffb03a"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"tipo": "objeto3d",
|
|
87
|
+
"forma": "cubo",
|
|
88
|
+
"x": 620,
|
|
89
|
+
"y": 620,
|
|
90
|
+
"tamanho": 260,
|
|
91
|
+
"girar": 0,
|
|
92
|
+
"faces": [
|
|
93
|
+
"IA",
|
|
94
|
+
"3D",
|
|
95
|
+
"Motion",
|
|
96
|
+
"Vidro",
|
|
97
|
+
"Som",
|
|
98
|
+
"Marca"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"tipo": "texto",
|
|
103
|
+
"x": 120,
|
|
104
|
+
"y": 700,
|
|
105
|
+
"l": 400,
|
|
106
|
+
"a": 100,
|
|
107
|
+
"texto": "chaves 3D",
|
|
108
|
+
"tamanho": 60,
|
|
109
|
+
"peso": 700,
|
|
110
|
+
"chaves": [
|
|
111
|
+
{
|
|
112
|
+
"t": 0,
|
|
113
|
+
"giroY": -60,
|
|
114
|
+
"opacidade": 0,
|
|
115
|
+
"x": -200
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"t": 0.4,
|
|
119
|
+
"giroY": 0,
|
|
120
|
+
"opacidade": 1,
|
|
121
|
+
"x": 0
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"t": 1,
|
|
125
|
+
"giroY": 20
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"suavizar": "elastico"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"atualizado": "2026-09-09T12:31:01+00:00"
|
|
135
|
+
}
|
package/studio/edge.py
CHANGED
|
@@ -117,32 +117,81 @@ def disponivel():
|
|
|
117
117
|
return _verificado
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
# Por que a instalação falhava calada, e o vídeo saía com a voz do Google:
|
|
121
|
+
# `pip install --user` é recusado pelo Python do sistema em Debian/Ubuntu e
|
|
122
|
+
# no do Homebrew ("externally-managed-environment", PEP 668), e em máquina
|
|
123
|
+
# sem pip o `-m pip` nem existe. Um `pip install` só, em segundo plano, com a
|
|
124
|
+
# saída jogada fora, era exatamente a falha que ninguém vê. Agora são várias
|
|
125
|
+
# tentativas, em ordem, e a última linha do erro de cada uma fica guardada
|
|
126
|
+
# para o status contar o que houve.
|
|
127
|
+
ESTRATEGIAS = [
|
|
128
|
+
["-m", "pip", "install", "--user", "--quiet", "edge-tts"],
|
|
129
|
+
["-m", "pip", "install", "--user", "--quiet", "--break-system-packages", "edge-tts"],
|
|
130
|
+
["-m", "pip", "install", "--quiet", "edge-tts"],
|
|
131
|
+
["-m", "pip", "install", "--quiet", "--break-system-packages", "edge-tts"],
|
|
132
|
+
]
|
|
133
|
+
_ultimo_erro = ""
|
|
134
|
+
_thread = None
|
|
135
|
+
|
|
136
|
+
|
|
120
137
|
def _rotina_instalar():
|
|
121
|
-
global _instalando, _verificado
|
|
138
|
+
global _instalando, _verificado, _ultimo_erro
|
|
122
139
|
try:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
# Sem pip, primeiro o ensurepip — é o que o python.org e a Apple
|
|
141
|
+
# trazem para o caso.
|
|
142
|
+
try:
|
|
143
|
+
r = subprocess.run([sys.executable, "-m", "pip", "--version"],
|
|
144
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30)
|
|
145
|
+
if r.returncode != 0:
|
|
146
|
+
subprocess.run([sys.executable, "-m", "ensurepip", "--user", "--default-pip"],
|
|
147
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=120)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
for args in ESTRATEGIAS:
|
|
151
|
+
try:
|
|
152
|
+
r = subprocess.run([sys.executable] + args, stdout=subprocess.DEVNULL,
|
|
153
|
+
stderr=subprocess.PIPE, timeout=240)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
_ultimo_erro = str(e)[-200:]
|
|
156
|
+
continue
|
|
157
|
+
_verificado = None
|
|
158
|
+
if r.returncode == 0 and disponivel():
|
|
159
|
+
_ultimo_erro = ""
|
|
160
|
+
sys.stderr.write("[edge] edge-tts instalado (%s)\n" % " ".join(args[2:]))
|
|
161
|
+
return
|
|
162
|
+
_ultimo_erro = (r.stderr or b"").decode("utf-8", "replace").strip().splitlines()[-1:][0][-200:] \
|
|
163
|
+
if (r.stderr or b"").strip() else "pip devolveu %s" % r.returncode
|
|
164
|
+
sys.stderr.write("[edge] não consegui instalar o edge-tts: %s\n" % _ultimo_erro)
|
|
128
165
|
finally:
|
|
129
166
|
_verificado = None # força nova checagem
|
|
130
167
|
with _tranca:
|
|
131
168
|
_instalando = False
|
|
132
169
|
|
|
133
170
|
|
|
134
|
-
def instalar():
|
|
135
|
-
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
def instalar(esperar=0):
|
|
172
|
+
"""Traz o pacote — em segundo plano por padrão; com `esperar` (segundos),
|
|
173
|
+
quem chamou fica aguardando até esse teto.
|
|
174
|
+
|
|
175
|
+
Esperar existe porque a narração de um vídeo NÃO pode sair pela voz de
|
|
176
|
+
reserva só porque o pip ainda estava rodando: quem pede voz agora e tem
|
|
177
|
+
internet merece a voz neural agora. Meio minuto de pip é menos ruim que
|
|
178
|
+
um vídeo inteiro com voz de robô."""
|
|
179
|
+
global _instalando, _thread
|
|
139
180
|
if disponivel():
|
|
140
|
-
return
|
|
181
|
+
return True
|
|
141
182
|
with _tranca:
|
|
142
|
-
if _instalando:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
183
|
+
if not _instalando:
|
|
184
|
+
_instalando = True
|
|
185
|
+
_thread = threading.Thread(target=_rotina_instalar, daemon=True)
|
|
186
|
+
_thread.start()
|
|
187
|
+
t = _thread
|
|
188
|
+
if t and esperar:
|
|
189
|
+
t.join(esperar)
|
|
190
|
+
return disponivel()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def ultimo_erro():
|
|
194
|
+
return _ultimo_erro
|
|
146
195
|
|
|
147
196
|
|
|
148
197
|
def _dur_do_vtt(caminho):
|
|
@@ -215,6 +264,7 @@ def estado():
|
|
|
215
264
|
"motor": "edge",
|
|
216
265
|
"disponivel": disponivel(),
|
|
217
266
|
"instalando": inst,
|
|
267
|
+
"erro": _ultimo_erro or None,
|
|
218
268
|
"precisa_internet": True,
|
|
219
269
|
"vozes": [{"id": v["id"], "nome": v["nome"], "genero": v["genero"],
|
|
220
270
|
"padrao": bool(v.get("padrao"))} for v in CATALOGO],
|
|
@@ -89,12 +89,22 @@ um elemento flutuando por cima do layout.
|
|
|
89
89
|
// path SVG numa caixa 24×24 (string ou lista de strings), no traço da casa
|
|
90
90
|
{ "tipo":"icone", "nome":"raio", "tamanho":120, "cor":"#ffb03a" }
|
|
91
91
|
{ "tipo":"icone", "caminho":"M3 6h18l-2 10H5zM8 20h.01M16 20h.01", "tamanho":96 }
|
|
92
|
+
// como OBJETO: estilo "app" (azulejo de aplicativo) ou "vidro" (liquid glass)
|
|
93
|
+
{ "tipo":"icone", "nome":"foguete", "estilo":"app", "tamanho":200, "fundo":"#8b5cf6" }
|
|
92
94
|
|
|
93
95
|
// FORMA
|
|
94
96
|
{ "tipo":"forma", "forma":"retangulo", "x":200,"y":200,"l":400,"a":220,
|
|
95
97
|
"preenchimento":"#ff5a36", "raio":24, "rotacao":-4, "opacidade":0.9 }
|
|
96
98
|
// forma: retangulo | circulo | triangulo | linha | seta
|
|
97
99
|
|
|
100
|
+
// MATERIAL — em qualquer elemento: "vidro" é o liquid glass (desfoca o que
|
|
101
|
+
// está atrás, bisel de luz, reflexo). Na forma, preenchimento vira a tinta.
|
|
102
|
+
{ "tipo":"forma", "forma":"retangulo", "material":"vidro", "raio":32, "x":100,"y":100,"l":800,"a":400 }
|
|
103
|
+
|
|
104
|
+
// SOM — no elemento, toca quando ele entra: whoosh | impacto | click | pop |
|
|
105
|
+
// ding | subida | tique | brilho, ou {"arquivo":"/midia/x.mp3","volume":0.8}
|
|
106
|
+
{ "tipo":"numeros", "itens":["+38% | em 12 meses"], "contar":true, "som":"ding" }
|
|
107
|
+
|
|
98
108
|
// CÓDIGO
|
|
99
109
|
{ "tipo":"codigo", "texto":"npm install primocode" }
|
|
100
110
|
|
|
@@ -186,8 +196,16 @@ completa em segundos (`0` deixa parado no `angulo` dado), `inclinar` levanta
|
|
|
186
196
|
ou baixa a câmera, `cores` pinta face a face.
|
|
187
197
|
|
|
188
198
|
Aceita `chaves` como qualquer elemento: dá para o cubo atravessar a cena
|
|
189
|
-
girando. Use **um** por cena — dois objetos girando disputam o olho.
|
|
190
|
-
|
|
199
|
+
girando. Use **um** por cena — dois objetos girando disputam o olho.
|
|
200
|
+
|
|
201
|
+
### Chaves de animação (motion)
|
|
202
|
+
|
|
203
|
+
`chaves` em qualquer elemento: instantes de `t` 0 a 1 com `x`, `y`
|
|
204
|
+
(deslocamento), `escala`, `escalaX`, `escalaY`, `giro`, **`giroX`, `giroY`,
|
|
205
|
+
`prof`** (3D: vira em profundidade, aproxima da câmera), `opacidade`,
|
|
206
|
+
`desfoque`. No elemento: `suavizar` (`suave` · `linear` · `entrada` · `saida`
|
|
207
|
+
· `elastico` · `salto` · `degrau`), `repetir`, `vaiEVolta`. A spec do Corte
|
|
208
|
+
tem os exemplos.
|
|
191
209
|
|
|
192
210
|
### Mídia do computador
|
|
193
211
|
|
|
@@ -181,9 +181,37 @@ narração, e uma disputa com quem fala em qualquer outro caso.
|
|
|
181
181
|
`src` vem de `POST /api/midia` (upload) ou é uma URL direta de áudio.
|
|
182
182
|
`inicio` pula os primeiros segundos da faixa.
|
|
183
183
|
|
|
184
|
+
### Música da internet, com licença (`buscar_audio`)
|
|
185
|
+
|
|
186
|
+
Uma faixa de verdade, tocada por gente, sem o usuário ter arquivo nenhum:
|
|
187
|
+
`buscar_audio` procura no Openverse (Jamendo, Freesound, Wikimedia — licenças
|
|
188
|
+
Creative Commons que permitem uso, inclusive comercial), **baixa** a melhor e
|
|
189
|
+
devolve a URL `/midia/…` pronta:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
buscar_audio { "consulta": "upbeat corporate", "duracao": 30 }
|
|
193
|
+
→ url "/midia/corporate-succes-8c2911.mp3", licenca "CC BY-SA",
|
|
194
|
+
credito "Corporate Succes — Arkadii Kaplan (CC BY-SA 3.0, via jamendo)"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
"trilha": { "tipo": "arquivo", "src": "/midia/corporate-succes-8c2911.mp3", "volume": 0.55, "repetir": true }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Consulta **em inglês**, pelo clima: `upbeat corporate`, `calm piano`, `epic
|
|
202
|
+
cinematic`, `lofi chill`, `acoustic warm`. `duracao` é a do vídeo, para a faixa
|
|
203
|
+
combinar. Para efeitos, `"tipo": "efeito"` (`whoosh`, `click`, `ding`,
|
|
204
|
+
`swoosh`, `pop`) — a URL vai em `som: {"arquivo": …}`.
|
|
205
|
+
|
|
206
|
+
**O crédito faz parte da peça.** Licença `CC BY`/`BY-SA` pede o crédito: ponha
|
|
207
|
+
a linha `credito` da resposta na assinatura ou numa legenda pequena da última
|
|
208
|
+
cena. `CC0` e domínio público não pedem nada. A resposta diz qual é o caso.
|
|
209
|
+
Quando a busca não acha nada, use a trilha **gerada** — nunca um "parecido"
|
|
210
|
+
de YouTube.
|
|
211
|
+
|
|
184
212
|
**Não baixe áudio de YouTube, Spotify ou similares** — quebra os termos dessas
|
|
185
|
-
plataformas e o direito autoral da faixa. Use música do próprio
|
|
186
|
-
|
|
213
|
+
plataformas e o direito autoral da faixa. Use `buscar_audio`, música do próprio
|
|
214
|
+
usuário ou uma das trilhas geradas acima.
|
|
187
215
|
|
|
188
216
|
---
|
|
189
217
|
|
|
@@ -242,6 +270,15 @@ número, botão, detalhe, o cartão — e libera o fundo `"marca"`:
|
|
|
242
270
|
"tema": "claro"
|
|
243
271
|
```
|
|
244
272
|
|
|
273
|
+
**De onde vêm a cor e a logo: do site da empresa.** Antes de escrever a peça,
|
|
274
|
+
chame `marca_da_empresa { "nome": "PicPay" }` (ou `"site": "https://…"`). Ela
|
|
275
|
+
lê o site, baixa a logo para o estúdio e devolve `marca: {cor, logo}` pronta
|
|
276
|
+
para colar, mais a paleta, as fontes e a frase da empresa. Não chute cor de
|
|
277
|
+
marca nem peça o arquivo ao usuário antes de tentar isso — a logo está no
|
|
278
|
+
site. Só quando a tool não achar (site montado por JavaScript, sem logo
|
|
279
|
+
baixável) é que se pede o arquivo (`studio_midia`) ou se assina com o nome em
|
|
280
|
+
texto.
|
|
281
|
+
|
|
245
282
|
Tema CLARO na maioria dos anúncios de marca. O escuro é para a cena de
|
|
246
283
|
produto e para a vitrine; o resto da peça respira no branco.
|
|
247
284
|
|
|
@@ -423,11 +460,38 @@ cena e a série ganha movimento sem esforço. `opcoes` marca a alternativa
|
|
|
423
460
|
`certa` com ✓ (não escreva o ✓ no texto: ele vem sozinho). Alternativa faz o
|
|
424
461
|
espectador pensar antes da resposta, e pensar é o que segura alguém no vídeo.
|
|
425
462
|
|
|
426
|
-
### Vidro
|
|
463
|
+
### Vidro — o liquid glass, em qualquer elemento
|
|
464
|
+
|
|
465
|
+
`"material": "vidro"` em **qualquer** elemento: texto vira pílula de vidro,
|
|
466
|
+
`forma` vira painel, `imagem` ganha moldura, `lista`/`cartoes`/`numeros`
|
|
467
|
+
ganham fundo. É vidro de verdade: **desfoca o que está atrás** (o fundo
|
|
468
|
+
`aurora`, a foto, o círculo colorido), tem o bisel de luz em cima e a sombra
|
|
469
|
+
embaixo, e o reflexo diagonal atravessa devagar durante a cena.
|
|
427
470
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
471
|
+
```json
|
|
472
|
+
{ "tipo": "forma", "forma": "retangulo", "material": "vidro", "raio": 36,
|
|
473
|
+
"x": 120, "y": 140, "l": 860, "a": 420, "preenchimento": "#22d3ee" }
|
|
474
|
+
{ "tipo": "texto", "material": "vidro", "texto": "pílula de vidro", "tamanho": 32 }
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Na forma, `preenchimento` vira a **tinta** do vidro (fraca) — não pinta o
|
|
478
|
+
fundo. `raio` arredonda (padrão 28). **Ponha algo atrás**: vidro sobre fundo
|
|
479
|
+
liso é só um retângulo claro; sobre `aurora`, `brilho`, uma foto ou uma forma
|
|
480
|
+
colorida, é o efeito. `"estilo": "vidro"` em `cartoes` continua valendo.
|
|
481
|
+
|
|
482
|
+
### Ícone como objeto (`estilo: "app"` / `"vidro"`)
|
|
483
|
+
|
|
484
|
+
```json
|
|
485
|
+
{ "tipo": "icone", "nome": "foguete", "estilo": "app", "tamanho": 220, "fundo": "#8b5cf6" }
|
|
486
|
+
{ "tipo": "icone", "nome": "raio", "estilo": "vidro", "tamanho": 220, "cor": "#ffb03a" }
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`app` é o azulejo de aplicativo: gradiente da cor, brilho na metade de cima,
|
|
490
|
+
sombra colorida embaixo, o traço branco. `vidro` é o mesmo azulejo em liquid
|
|
491
|
+
glass. `fundo` pinta o azulejo (sem ele, a cor do tema). Um ícone de traço
|
|
492
|
+
sozinho é sinal; no azulejo vira **coisa** — é o que se usa para "o app", "o
|
|
493
|
+
produto", "o recurso" numa cena de lançamento. Combina com `som: "pop"` na
|
|
494
|
+
entrada e com `chaves` de `giroY` para ele virar ao pousar.
|
|
431
495
|
|
|
432
496
|
---
|
|
433
497
|
|
|
@@ -647,11 +711,33 @@ Para controlar:
|
|
|
647
711
|
|
|
648
712
|
```json
|
|
649
713
|
"sfx": false // no documento: desliga todos os efeitos
|
|
650
|
-
{ "transicao": "deslizar", "
|
|
714
|
+
{ "transicao": "deslizar", "som": "impacto" } // na cena: força um efeito
|
|
651
715
|
```
|
|
652
716
|
|
|
653
|
-
Valores
|
|
654
|
-
|
|
717
|
+
Valores: `"whoosh"` · `"impacto"` · `"click"` · `"pop"` · `"ding"` ·
|
|
718
|
+
`"subida"` · `"tique"` · `"brilho"` · `"nenhum"`. (`sfx` na cena ainda vale;
|
|
719
|
+
`som` é o nome novo, o mesmo dos elementos.)
|
|
720
|
+
|
|
721
|
+
### Som por elemento — o que faz motion soar motion
|
|
722
|
+
|
|
723
|
+
`"som"` num **elemento** toca no instante em que ele entra (o mesmo atraso da
|
|
724
|
+
animação de entrada). É a diferença entre um slide animado e uma peça de
|
|
725
|
+
motion: o número faz **ding** ao aparecer, o ícone faz **pop** ao pousar, a
|
|
726
|
+
barra sobe com a **subida** (o riser) e o texto final chega com **brilho**.
|
|
727
|
+
|
|
728
|
+
```json
|
|
729
|
+
{ "tipo": "numeros", "itens": ["+38% | em 12 meses"], "contar": true, "som": "ding" }
|
|
730
|
+
{ "tipo": "icone", "nome": "foguete", "estilo": "app", "som": "pop" }
|
|
731
|
+
{ "tipo": "texto", "texto": "Agora.", "som": { "arquivo": "/midia/little-whoosh-3.mp3", "volume": 0.8 } }
|
|
732
|
+
{ "tipo": "texto", "texto": "3, 2, 1", "som": { "nome": "tique", "em": 1.5 } }
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
`som` aceita o nome, ou `{ "arquivo": "/midia/…", "volume": 0–1 }` para um
|
|
736
|
+
efeito baixado com `buscar_audio` (tipo `"efeito"`) ou enviado pelo usuário;
|
|
737
|
+
`"em"` (segundos na cena) força o instante. O arquivo entra no mesmo grafo da
|
|
738
|
+
trilha, então vai junto para o MP4. Regra: **um som por ideia, não por
|
|
739
|
+
elemento** — três ou quatro por vídeo; tudo apitando é ruído, e a narração
|
|
740
|
+
tem de continuar por cima.
|
|
655
741
|
|
|
656
742
|
---
|
|
657
743
|
|
|
@@ -684,8 +770,32 @@ Campos de uma chave, todos opcionais: `x` e `y` (**deslocamento** em pixels a
|
|
|
684
770
|
partir de onde o elemento está — não é posição absoluta), `escala` (1 = tamanho
|
|
685
771
|
normal), `giro` (graus), `opacidade` (0 a 1) e `desfoque` (pixels).
|
|
686
772
|
|
|
687
|
-
|
|
688
|
-
|
|
773
|
+
**Em 3D:** `giroY` e `giroX` (graus — o elemento vira em profundidade, como um
|
|
774
|
+
cartão girando na mão), `prof` (pixels — aproxima ou afasta da câmera) e
|
|
775
|
+
`escalaX`/`escalaY` (achatar num eixo só: a gota que pousa e se espalha).
|
|
776
|
+
|
|
777
|
+
```json
|
|
778
|
+
{ "tipo": "icone", "nome": "foguete", "estilo": "app", "tamanho": 220,
|
|
779
|
+
"chaves": [
|
|
780
|
+
{ "t": 0, "giroY": -90, "prof": -400, "opacidade": 0 },
|
|
781
|
+
{ "t": 0.35, "giroY": 0, "prof": 0, "opacidade": 1 },
|
|
782
|
+
{ "t": 1, "giroY": 12 }
|
|
783
|
+
], "suavizar": "elastico", "som": "pop" }
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**A curva** (`suavizar`, no elemento): `"suave"` (padrão) · `"linear"` ·
|
|
787
|
+
`"entrada"` (acelera) · `"saida"` (freia) · `"elastico"` (passa do ponto e
|
|
788
|
+
volta — o pouso de app) · `"salto"` (recua, dispara e assenta) · `"degrau"`
|
|
789
|
+
(quadro a quadro, stop-motion). Aceita também um `cubic-bezier(…)` cru.
|
|
790
|
+
`"repetir": true` roda sem parar; com `"vaiEVolta": true` ele vai e volta em
|
|
791
|
+
vez de recomeçar — é a levitação de um objeto.
|
|
792
|
+
|
|
793
|
+
Um exemplo de cada coisa que motion pede — **entrar de lado, respirar, sair**:
|
|
794
|
+
|
|
795
|
+
```json
|
|
796
|
+
"chaves": [ { "t": 0, "x": -300, "opacidade": 0 }, { "t": 0.2, "x": 0, "opacidade": 1 },
|
|
797
|
+
{ "t": 0.85, "x": 0, "opacidade": 1 }, { "t": 1, "x": 120, "opacidade": 0 } ]
|
|
798
|
+
```
|
|
689
799
|
|
|
690
800
|
Duas chaves já bastam. Quem tem `chaves` ignora `anim` — as duas disputariam o
|
|
691
801
|
mesmo movimento, e a chave é mais específica. Use em **um ou dois** elementos
|
|
@@ -61,6 +61,27 @@ você controla cada `x`, `y`, `l`, `a` em pixels da tela.
|
|
|
61
61
|
`globo` `chave` `presente` `sino` `mapa` `dinheiro` `crescimento` `grafico` `balao`
|
|
62
62
|
`pasta` `livro` `play2` `check` `imagem` `codigo` `tabela` `lista`
|
|
63
63
|
|
|
64
|
+
## Vidro e ícone-objeto
|
|
65
|
+
|
|
66
|
+
`"material": "vidro"` em qualquer elemento é o liquid glass: painel
|
|
67
|
+
translúcido que **desfoca o que está atrás**, com bisel de luz em cima e
|
|
68
|
+
reflexo diagonal. Precisa de algo atrás (gradiente, forma colorida, foto) para
|
|
69
|
+
aparecer. Na forma, `preenchimento` vira a tinta do vidro.
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{ "tipo": "forma", "forma": "retangulo", "material": "vidro", "raio": 32,
|
|
73
|
+
"x": 86, "y": 300, "l": 908, "a": 380, "preenchimento": "#ff5a36" }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`"estilo": "app"` no ícone faz o azulejo de aplicativo (gradiente da cor,
|
|
77
|
+
brilho, sombra, traço branco); `"estilo": "vidro"` faz o mesmo azulejo em
|
|
78
|
+
vidro. `fundo` pinta o azulejo. É o ícone de produto para capa e post de
|
|
79
|
+
lançamento:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{ "tipo": "icone", "nome": "foguete", "estilo": "app", "x": 86, "y": 120, "tamanho": 200, "fundo": "#8b5cf6" }
|
|
83
|
+
```
|
|
84
|
+
|
|
64
85
|
## Várias artes no mesmo arquivo
|
|
65
86
|
|
|
66
87
|
Cada item de `cenas` é uma prancha independente. Um carrossel de Instagram é um
|
package/studio/server.py
CHANGED
|
@@ -374,11 +374,21 @@ def preparar_voz(doc):
|
|
|
374
374
|
Com o ElevenLabs configurado, não há nada para baixar: a síntese é
|
|
375
375
|
imediata e os 90 MB do Piper ficam para o dia em que faltarem chave ou
|
|
376
376
|
internet."""
|
|
377
|
-
if _tem_narracao(doc) and not elevenlabs_lib.configurado()
|
|
377
|
+
if _tem_narracao(doc) and not elevenlabs_lib.configurado():
|
|
378
|
+
# O Edge (pip, segundos) e o Piper (90 MB, minutos) começam JUNTOS.
|
|
379
|
+
# Antes só o Piper era preparado aqui, e o Edge — a melhor voz grátis —
|
|
380
|
+
# só era instalado quando a primeira cena já estava pedindo áudio: a
|
|
381
|
+
# narração caía no Google enquanto o pip rodava, e o dono ouviu a
|
|
382
|
+
# voz de robô no vídeo inteiro.
|
|
378
383
|
try:
|
|
379
|
-
|
|
384
|
+
edge_lib.instalar()
|
|
380
385
|
except Exception:
|
|
381
386
|
pass
|
|
387
|
+
if not voz_lib.pronta():
|
|
388
|
+
try:
|
|
389
|
+
voz_lib.instalar()
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
382
392
|
|
|
383
393
|
|
|
384
394
|
# ------------------------------------------------------- narração em arquivo --
|
|
@@ -474,6 +484,7 @@ def _gravar_narracao(arq_id):
|
|
|
474
484
|
if c.get("narracaoAudio") and not c.get("narracaoVoz"):
|
|
475
485
|
continue
|
|
476
486
|
audio, ext, dur = None, ".wav", None
|
|
487
|
+
motor_usado = None
|
|
477
488
|
# A CORRENTE, na ordem de qualidade. Cada motor cai no seguinte
|
|
478
489
|
# quando não devolve áudio — e o Google, no fim, garante que a
|
|
479
490
|
# cena nunca fique muda. É o que faz o pré-render nunca parar.
|
|
@@ -483,14 +494,19 @@ def _gravar_narracao(arq_id):
|
|
|
483
494
|
if elevenlabs_lib.configurado():
|
|
484
495
|
audio, dur = elevenlabs_lib.sintetizar(texto, voz)
|
|
485
496
|
if audio:
|
|
486
|
-
ext = ".mp3"
|
|
497
|
+
ext, motor_usado = ".mp3", "elevenlabs"
|
|
487
498
|
|
|
488
499
|
# 2) Edge TTS: neural, grátis, sem chave. MP3, duração medida pelo
|
|
489
|
-
# VTT que o próprio edge escreve.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
500
|
+
# VTT que o próprio edge escreve. Ninguém está olhando este
|
|
501
|
+
# laço: se o pip ainda roda, espera por ele (até 2 min) — é de
|
|
502
|
+
# graça, e é o que separa voz de gente de voz de robô.
|
|
503
|
+
if not audio:
|
|
504
|
+
if not edge_lib.disponivel():
|
|
505
|
+
edge_lib.instalar(esperar=120)
|
|
506
|
+
if edge_lib.disponivel():
|
|
507
|
+
dados, edur = edge_lib.sintetizar(texto, voz)
|
|
508
|
+
if dados:
|
|
509
|
+
audio, dur, ext, motor_usado = dados, edur, ".mp3", "edge"
|
|
494
510
|
|
|
495
511
|
# 3) Piper: neural e offline, quando baixado e compatível. WAV.
|
|
496
512
|
if not audio:
|
|
@@ -503,15 +519,19 @@ def _gravar_narracao(arq_id):
|
|
|
503
519
|
sys.stderr.write("[voz] Piper cena %d falhou: %s\n" % (i + 1, e))
|
|
504
520
|
audio = None
|
|
505
521
|
if audio:
|
|
506
|
-
dur, ext = _duracao_wav(audio), ".wav"
|
|
522
|
+
dur, ext, motor_usado = _duracao_wav(audio), ".wav", "piper"
|
|
507
523
|
|
|
508
524
|
# 4) Google: última reserva. Robótica, mas responde. MP3 — a
|
|
509
525
|
# duração sai do tamanho (CBR de 24 kbps ≈ 3000 bytes/s).
|
|
526
|
+
# Vai MARCADA na cena (`narracaoMotor`): o agente lê e avisa a
|
|
527
|
+
# pessoa, em vez de ela descobrir ouvindo.
|
|
510
528
|
if not audio:
|
|
511
529
|
dados = _google_tts(texto)
|
|
512
530
|
if dados:
|
|
513
|
-
audio, ext = dados, ".mp3"
|
|
531
|
+
audio, ext, motor_usado = dados, ".mp3", "google"
|
|
514
532
|
dur = round(len(dados) / 3000.0, 2)
|
|
533
|
+
sys.stderr.write("[voz] RESERVA na cena %d: Google (edge-tts: %s)\n"
|
|
534
|
+
% (i + 1, edge_lib.ultimo_erro() or "não instalado"))
|
|
515
535
|
|
|
516
536
|
if not audio:
|
|
517
537
|
continue
|
|
@@ -520,6 +540,7 @@ def _gravar_narracao(arq_id):
|
|
|
520
540
|
fh.write(audio)
|
|
521
541
|
c["narracaoAudio"] = "/midia/" + nome
|
|
522
542
|
c["narracaoVoz"] = assinatura
|
|
543
|
+
c["narracaoMotor"] = motor_usado
|
|
523
544
|
# A duração medida vai junto: com ela o player sabe quanto a cena
|
|
524
545
|
# REALMENTE dura (a fala inteira + respiro) e a linha do tempo
|
|
525
546
|
# vira determinística — sem ela, o relógio congela na beirada da
|
|
@@ -540,6 +561,8 @@ def _gravar_narracao(arq_id):
|
|
|
540
561
|
and cenas[i].get("narracao") == c.get("narracao"):
|
|
541
562
|
cenas[i]["narracaoAudio"] = c["narracaoAudio"]
|
|
542
563
|
cenas[i]["narracaoVoz"] = c["narracaoVoz"]
|
|
564
|
+
if c.get("narracaoMotor"):
|
|
565
|
+
cenas[i]["narracaoMotor"] = c["narracaoMotor"]
|
|
543
566
|
if c.get("narracaoDur"):
|
|
544
567
|
cenas[i]["narracaoDur"] = c["narracaoDur"]
|
|
545
568
|
gravar(atual)
|
|
@@ -1473,6 +1496,11 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
1473
1496
|
# ElevenLabs porque é a melhor voz gratuita da corrente — vozes
|
|
1474
1497
|
# da Microsoft, muito acima do Google. Precisa de internet.
|
|
1475
1498
|
if not audio and motor in ("", "edge"):
|
|
1499
|
+
# Pacote ainda não instalado? ESPERA o pip (até 60 s) em vez de
|
|
1500
|
+
# cair para o Google. Quem pediu voz tem internet — é o que o Edge
|
|
1501
|
+
# precisa — e meio minuto é menos ruim que a voz de robô no vídeo.
|
|
1502
|
+
if not edge_lib.disponivel():
|
|
1503
|
+
edge_lib.instalar(esperar=60)
|
|
1476
1504
|
dados, _ = edge_lib.sintetizar(texto, {"voz": voz_pedida,
|
|
1477
1505
|
"grave": grave, "velocidade": velocidade})
|
|
1478
1506
|
if dados:
|
|
@@ -1505,17 +1533,25 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
1505
1533
|
except Exception as e:
|
|
1506
1534
|
sys.stderr.write("[voz] Voicebox falhou (%s); indo de Google\n" % e)
|
|
1507
1535
|
audio = b""
|
|
1508
|
-
# 3) Google: uma voz só, mas responde sempre.
|
|
1536
|
+
# 3) Google: uma voz só, mas responde sempre. É a EXCEÇÃO, e vai
|
|
1537
|
+
# marcada: o cabeçalho diz qual motor falou, para o player e o
|
|
1538
|
+
# agente saberem que a voz saiu pela reserva — em vez de a pessoa
|
|
1539
|
+
# descobrir ouvindo.
|
|
1540
|
+
motor_usado = "elevenlabs" if (audio and tipo == "audio/mpeg" and elevenlabs_lib.configurado()) else \
|
|
1541
|
+
("edge" if audio and tipo == "audio/mpeg" else ("piper" if audio and tipo == "audio/wav" else "voicebox" if audio else "google"))
|
|
1509
1542
|
if not audio:
|
|
1510
1543
|
tipo = "audio/mpeg"
|
|
1511
1544
|
try:
|
|
1512
1545
|
audio = b"".join(self._trecho_de_voz(t, idioma) for t in _picar(texto, 190))
|
|
1513
1546
|
except Exception as e: # rede, bloqueio, mudança do serviço
|
|
1514
1547
|
return self.js({"erro": "voz indisponível: %s" % e}, 502)
|
|
1548
|
+
sys.stderr.write("[voz] RESERVA: a narração saiu pelo Google (edge-tts: %s)\n"
|
|
1549
|
+
% (edge_lib.ultimo_erro() or "não instalado"))
|
|
1515
1550
|
if not audio:
|
|
1516
1551
|
return self.js({"erro": "voz não devolveu áudio"}, 502)
|
|
1517
1552
|
|
|
1518
1553
|
self.send_response(200)
|
|
1554
|
+
self.send_header("X-Voz-Motor", motor_usado)
|
|
1519
1555
|
self.send_header("Content-Type", tipo)
|
|
1520
1556
|
self.send_header("Content-Length", str(len(audio)))
|
|
1521
1557
|
self.send_header("Cache-Control", "no-store")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[studio] 2026-09-09T12:28:40+00:00 — iniciando na porta 7797
|
|
2
|
+
|
|
3
|
+
██ PrimoCode Studio
|
|
4
|
+
────────────────────────────────────────────────────────
|
|
5
|
+
Estúdio http://localhost:7797
|
|
6
|
+
Manual da IA http://localhost:7797/api/manual
|
|
7
|
+
Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
|
|
8
|
+
────────────────────────────────────────────────────────
|
|
9
|
+
Ctrl+C para parar
|
|
10
|
+
|
|
11
|
+
GET /api/arquivos
|
|
12
|
+
POST /api/arquivos
|
|
13
|
+
GET /api/arquivos/tela-motion-3ecaa
|
|
14
|
+
GET /api/voz/status
|
|
15
|
+
[studio] 2026-09-09T12:31:01+00:00 — iniciando na porta 7797
|
|
16
|
+
|
|
17
|
+
██ PrimoCode Studio
|
|
18
|
+
────────────────────────────────────────────────────────
|
|
19
|
+
Estúdio http://localhost:7797
|
|
20
|
+
Manual da IA http://localhost:7797/api/manual
|
|
21
|
+
Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
|
|
22
|
+
────────────────────────────────────────────────────────
|
|
23
|
+
Ctrl+C para parar
|
|
24
|
+
|
|
25
|
+
GET /api/arquivos
|
|
26
|
+
POST /api/arquivos
|
|
27
|
+
GET /api/arquivos/tela-motion-d6150
|
|
28
|
+
GET /api/voz/status
|