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.
@@ -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
- subprocess.run(
124
- [sys.executable, "-m", "pip", "install", "--user", "--quiet", "edge-tts"],
125
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=180)
126
- except Exception as e:
127
- sys.stderr.write("[edge] pip install falhou: %s\n" % e)
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
- """Tenta trazer o pacote em segundo plano, uma vez. Não bloqueia: quem
136
- pediu narração agora ouve o próximo motor da corrente, e a partir da
137
- próxima vez o Edge já responde."""
138
- global _instalando
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
- return
144
- _instalando = True
145
- threading.Thread(target=_rotina_instalar, daemon=True).start()
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. Uma cena com tema diferente no meio cria respiro —
190
- use uma vez, não cinco.
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 usuário, faixa
186
- livre de direitos, ou uma das trilhas geradas acima.
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
- `"estilo": "vidro"` em `cartoes` dá painel translúcido com bisel de luz em
429
- cima e sombra embaixo — o "liquid glass". Funciona em tema escuro; em tema
430
- claro use os cartões normais.
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", "sfx": "impacto" } // na cena: força um efeito
714
+ { "transicao": "deslizar", "som": "impacto" } // na cena: força um efeito
651
715
  ```
652
716
 
653
- Valores por cena: `"whoosh"` · `"impacto"` · `"click"` · `"nenhum"`.
654
- O padrão (sem declarar nada) já é o certo para a maioria dos vídeos.
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
- No elemento: `"repetir": true` roda sem parar, e `"suavizar": false` dá
688
- movimento constante em vez de acelerar e frear.
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() and not voz_lib.pronta():
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
- voz_lib.instalar()
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
- if not audio and edge_lib.disponivel():
491
- dados, edur = edge_lib.sintetizar(texto, voz)
492
- if dados:
493
- audio, dur, ext = dados, edur, ".mp3"
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