primocode 8.9.1 → 8.10.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.
@@ -0,0 +1,113 @@
1
+ {
2
+ "id": "traco-estatico-traco-25872",
3
+ "ferramenta": "traco",
4
+ "titulo": "estatico-traco",
5
+ "doc": {
6
+ "tema": "meia-noite",
7
+ "largura": 1920,
8
+ "altura": 1080,
9
+ "cenas": [
10
+ {
11
+ "layout": "livre",
12
+ "fundo": "pontos",
13
+ "elementos": [
14
+ {
15
+ "id": "ped",
16
+ "tipo": "no",
17
+ "texto": "Pedido aberto",
18
+ "x": 120,
19
+ "y": 480,
20
+ "l": 320,
21
+ "a": 140,
22
+ "destaque": true,
23
+ "anim": "nenhuma"
24
+ },
25
+ {
26
+ "id": "val",
27
+ "tipo": "no",
28
+ "texto": "Validação\nfiscal",
29
+ "x": 620,
30
+ "y": 480,
31
+ "l": 320,
32
+ "a": 140,
33
+ "anim": "nenhuma"
34
+ },
35
+ {
36
+ "id": "ger",
37
+ "tipo": "no",
38
+ "texto": "Aprovação\ngerência",
39
+ "x": 1120,
40
+ "y": 300,
41
+ "l": 320,
42
+ "a": 140,
43
+ "anim": "nenhuma"
44
+ },
45
+ {
46
+ "id": "dir",
47
+ "tipo": "no",
48
+ "texto": "Aprovação\ndiretoria",
49
+ "x": 1120,
50
+ "y": 660,
51
+ "l": 320,
52
+ "a": 140,
53
+ "anim": "nenhuma"
54
+ },
55
+ {
56
+ "id": "ok",
57
+ "tipo": "no",
58
+ "texto": "Compra\nemitida",
59
+ "x": 1560,
60
+ "y": 480,
61
+ "l": 280,
62
+ "a": 140,
63
+ "destaque": true,
64
+ "anim": "nenhuma"
65
+ },
66
+ {
67
+ "tipo": "conector",
68
+ "de": "ped",
69
+ "para": "val",
70
+ "anim": "nenhuma"
71
+ },
72
+ {
73
+ "tipo": "conector",
74
+ "de": "val",
75
+ "para": "ger",
76
+ "texto": "até R$ 20 mil",
77
+ "anim": "nenhuma"
78
+ },
79
+ {
80
+ "tipo": "conector",
81
+ "de": "val",
82
+ "para": "dir",
83
+ "texto": "acima de R$ 20 mil",
84
+ "anim": "nenhuma"
85
+ },
86
+ {
87
+ "tipo": "conector",
88
+ "de": "ger",
89
+ "para": "ok",
90
+ "anim": "nenhuma"
91
+ },
92
+ {
93
+ "tipo": "conector",
94
+ "de": "dir",
95
+ "para": "ok",
96
+ "anim": "nenhuma"
97
+ },
98
+ {
99
+ "tipo": "texto",
100
+ "x": 120,
101
+ "y": 120,
102
+ "l": 900,
103
+ "a": 120,
104
+ "texto": "Compras · fluxo vigente",
105
+ "papel": "chapeu",
106
+ "anim": "nenhuma"
107
+ }
108
+ ]
109
+ }
110
+ ]
111
+ },
112
+ "atualizado": "2026-08-16T15:08:28+00:00"
113
+ }
@@ -0,0 +1,102 @@
1
+ {
2
+ "id": "traco-sweep-traco-0bcf4",
3
+ "ferramenta": "traco",
4
+ "titulo": "sweep-traco",
5
+ "doc": {
6
+ "tema": "meia-noite",
7
+ "largura": 1920,
8
+ "altura": 1080,
9
+ "cenas": [
10
+ {
11
+ "layout": "livre",
12
+ "fundo": "pontos",
13
+ "elementos": [
14
+ {
15
+ "id": "ped",
16
+ "tipo": "no",
17
+ "texto": "Pedido aberto",
18
+ "x": 120,
19
+ "y": 480,
20
+ "l": 320,
21
+ "a": 140,
22
+ "destaque": true
23
+ },
24
+ {
25
+ "id": "val",
26
+ "tipo": "no",
27
+ "texto": "Validação\nfiscal",
28
+ "x": 620,
29
+ "y": 480,
30
+ "l": 320,
31
+ "a": 140
32
+ },
33
+ {
34
+ "id": "ger",
35
+ "tipo": "no",
36
+ "texto": "Aprovação\ngerência",
37
+ "x": 1120,
38
+ "y": 300,
39
+ "l": 320,
40
+ "a": 140
41
+ },
42
+ {
43
+ "id": "dir",
44
+ "tipo": "no",
45
+ "texto": "Aprovação\ndiretoria",
46
+ "x": 1120,
47
+ "y": 660,
48
+ "l": 320,
49
+ "a": 140
50
+ },
51
+ {
52
+ "id": "ok",
53
+ "tipo": "no",
54
+ "texto": "Compra\nemitida",
55
+ "x": 1560,
56
+ "y": 480,
57
+ "l": 280,
58
+ "a": 140,
59
+ "destaque": true
60
+ },
61
+ {
62
+ "tipo": "conector",
63
+ "de": "ped",
64
+ "para": "val"
65
+ },
66
+ {
67
+ "tipo": "conector",
68
+ "de": "val",
69
+ "para": "ger",
70
+ "texto": "até R$ 20 mil"
71
+ },
72
+ {
73
+ "tipo": "conector",
74
+ "de": "val",
75
+ "para": "dir",
76
+ "texto": "acima de R$ 20 mil"
77
+ },
78
+ {
79
+ "tipo": "conector",
80
+ "de": "ger",
81
+ "para": "ok"
82
+ },
83
+ {
84
+ "tipo": "conector",
85
+ "de": "dir",
86
+ "para": "ok"
87
+ },
88
+ {
89
+ "tipo": "texto",
90
+ "x": 120,
91
+ "y": 120,
92
+ "l": 900,
93
+ "a": 120,
94
+ "texto": "Compras · fluxo vigente",
95
+ "papel": "chapeu"
96
+ }
97
+ ]
98
+ }
99
+ ]
100
+ },
101
+ "atualizado": "2026-08-16T15:07:33+00:00"
102
+ }
@@ -3,6 +3,68 @@
3
3
  No lugar de CapCut, Premiere e After Effects. Modelo `cena`, com `duracao` em cada
4
4
  cena, trilha sonora, transições e gravação em `.webm`.
5
5
 
6
+ <!-- O EXEMPLO FICA NO COMEÇO DE PROPÓSITO: quando o plano de tokens é
7
+ apertado, o agente recebe só os primeiros ~3500 caracteres desta spec. Um
8
+ vídeo inteiro saiu PRETO porque o formato do documento estava depois do
9
+ corte e o agente inventou a estrutura. Formato primeiro; detalhe depois. -->
10
+
11
+ ## Exemplo completo
12
+
13
+ ```json
14
+ {
15
+ "ferramenta": "corte",
16
+ "titulo": "Reels — por que sua conta de luz subiu",
17
+ "doc": {
18
+ "tema": "brasa",
19
+ "largura": 1080, "altura": 1920,
20
+ "acabamento": "grao",
21
+ "trilha": { "tipo": "gerada", "estilo": "pulso", "volume": 0.7 },
22
+ "cenas": [
23
+ { "layout": "centro", "duracao": 3, "fundo": "aurora", "transicao": "flash",
24
+ "elementos": [
25
+ { "tipo": "texto", "papel": "titulo", "texto": "Sua conta subiu\nde novo.",
26
+ "tamanho": 124, "estilo": "gradiente", "anim": "desfoque" }]},
27
+
28
+ { "layout": "centro", "duracao": 3, "fundo": "vinheta", "transicao": "limpar",
29
+ "elementos": [
30
+ { "tipo": "texto", "papel": "titulo", "texto": "E não foi o seu chuveiro.",
31
+ "tamanho": 78, "estilo": "marcador", "anim": "palavra" }]},
32
+
33
+ { "layout": "centro", "duracao": 4, "transicao": "zoom", "acabamento": "vinheta",
34
+ "fundo": { "imagem": "/midia/torre.jpg", "escurecer": 0.5 },
35
+ "elementos": [
36
+ { "tipo": "numeros", "contar": true, "anim": "saltar", "tamanho": 180,
37
+ "itens": ["+38% | bandeira tarifária em 12 meses"] }]},
38
+
39
+ { "layout": "livre", "duracao": 4, "transicao": "deslizar",
40
+ "elementos": [
41
+ { "tipo": "imagem", "src": "/midia/campo.jpg", "x": 0, "y": 0,
42
+ "l": 1080, "a": 1920, "raio": 0, "movimento": "aproximar", "filtro": "contraste" },
43
+ { "tipo": "forma", "forma": "retangulo", "x": 0, "y": 1180, "l": 1080, "a": 740,
44
+ "preenchimento": "rgba(10,5,6,.72)" },
45
+ { "tipo": "texto", "x": 90, "y": 1320, "l": 900, "a": 420,
46
+ "texto": "O custo está\nna transmissão", "tamanho": 104, "peso": 750,
47
+ "estilo": "sombra", "anim": "cortina" }]},
48
+
49
+ { "layout": "centro", "duracao": 5, "fundo": "raios", "transicao": "subir",
50
+ "elementos": [
51
+ { "tipo": "lista", "revelar": true, "tamanho": 52, "intervalo": 0.9,
52
+ "itens": ["Linhas saturadas no Nordeste",
53
+ "Usina gerando sem escoar",
54
+ "A conta chega em quem consome"] }]},
55
+
56
+ { "layout": "centro", "duracao": 3, "fundo": "brilho", "transicao": "fade",
57
+ "acabamento": "luz",
58
+ "elementos": [
59
+ { "tipo": "texto", "papel": "titulo", "texto": "Segue para\nentender a próxima.",
60
+ "tamanho": 96, "estilo": "contorno-tom", "anim": "elastico" }]}
61
+ ]
62
+ }
63
+ }
64
+ ```
65
+
66
+ ---
67
+
6
68
  ## Como pensar um vídeo curto
7
69
 
8
70
  Um Reels de 30 s são 7 a 9 cenas de 3 a 5 segundos. A regra é dura:
@@ -152,63 +214,6 @@ Em `lista`, `"revelar": true` distribui os itens dentro da duração da cena;
152
214
 
153
215
  ---
154
216
 
155
- ## Exemplo completo
156
-
157
- ```json
158
- {
159
- "ferramenta": "corte",
160
- "titulo": "Reels — por que sua conta de luz subiu",
161
- "doc": {
162
- "tema": "brasa",
163
- "largura": 1080, "altura": 1920,
164
- "acabamento": "grao",
165
- "trilha": { "tipo": "gerada", "estilo": "pulso", "volume": 0.7 },
166
- "cenas": [
167
- { "layout": "centro", "duracao": 3, "fundo": "aurora", "transicao": "flash",
168
- "elementos": [
169
- { "tipo": "texto", "papel": "titulo", "texto": "Sua conta subiu\nde novo.",
170
- "tamanho": 124, "estilo": "gradiente", "anim": "desfoque" }]},
171
-
172
- { "layout": "centro", "duracao": 3, "fundo": "vinheta", "transicao": "limpar",
173
- "elementos": [
174
- { "tipo": "texto", "papel": "titulo", "texto": "E não foi o seu chuveiro.",
175
- "tamanho": 78, "estilo": "marcador", "anim": "palavra" }]},
176
-
177
- { "layout": "centro", "duracao": 4, "transicao": "zoom", "acabamento": "vinheta",
178
- "fundo": { "imagem": "/midia/torre.jpg", "escurecer": 0.5 },
179
- "elementos": [
180
- { "tipo": "numeros", "contar": true, "anim": "saltar", "tamanho": 180,
181
- "itens": ["+38% | bandeira tarifária em 12 meses"] }]},
182
-
183
- { "layout": "livre", "duracao": 4, "transicao": "deslizar",
184
- "elementos": [
185
- { "tipo": "imagem", "src": "/midia/campo.jpg", "x": 0, "y": 0,
186
- "l": 1080, "a": 1920, "raio": 0, "movimento": "aproximar", "filtro": "contraste" },
187
- { "tipo": "forma", "forma": "retangulo", "x": 0, "y": 1180, "l": 1080, "a": 740,
188
- "preenchimento": "rgba(10,5,6,.72)" },
189
- { "tipo": "texto", "x": 90, "y": 1320, "l": 900, "a": 420,
190
- "texto": "O custo está\nna transmissão", "tamanho": 104, "peso": 750,
191
- "estilo": "sombra", "anim": "cortina" }]},
192
-
193
- { "layout": "centro", "duracao": 5, "fundo": "raios", "transicao": "subir",
194
- "elementos": [
195
- { "tipo": "lista", "revelar": true, "tamanho": 52, "intervalo": 0.9,
196
- "itens": ["Linhas saturadas no Nordeste",
197
- "Usina gerando sem escoar",
198
- "A conta chega em quem consome"] }]},
199
-
200
- { "layout": "centro", "duracao": 3, "fundo": "brilho", "transicao": "fade",
201
- "acabamento": "luz",
202
- "elementos": [
203
- { "tipo": "texto", "papel": "titulo", "texto": "Segue para\nentender a próxima.",
204
- "tamanho": 96, "estilo": "contorno-tom", "anim": "elastico" }]}
205
- ]
206
- }
207
- }
208
- ```
209
-
210
- ---
211
-
212
217
  ## No modo Abrir
213
218
 
214
219
  Toca sozinho ao carregar, com barra de transporte embaixo:
package/studio/server.py CHANGED
@@ -9,6 +9,7 @@ Só biblioteca padrão do Python. Nada para instalar.
9
9
  """
10
10
 
11
11
  import base64
12
+ import hashlib
12
13
  import functools
13
14
  import json
14
15
  import mimetypes
@@ -30,6 +31,19 @@ FERRAMENTAS = os.path.join(RAIZ, "ferramentas")
30
31
  MIDIA = os.path.join(RAIZ, "midia")
31
32
  PORTA = int(sys.argv[1]) if len(sys.argv) > 1 else 7777
32
33
 
34
+ def _assinatura():
35
+ """SHA-1 do próprio arquivo, lido NA SUBIDA. Se o npm trocar o código no
36
+ disco depois, o processo no ar continua respondendo a assinatura antiga —
37
+ e é exatamente essa diferença que o CLI usa para saber que precisa
38
+ reiniciar o Studio em vez de servir a versão velha para sempre."""
39
+ try:
40
+ with open(os.path.abspath(__file__), "rb") as fh:
41
+ return hashlib.sha1(fh.read()).hexdigest()[:12]
42
+ except Exception:
43
+ return "?"
44
+
45
+ ASSINATURA = _assinatura()
46
+
33
47
  # 60 MB por arquivo: cobre faixa longa em mp3 e foto de câmera sem susto
34
48
  LIMITE_MIDIA = 60 * 1024 * 1024
35
49
 
@@ -244,6 +258,17 @@ def criar(dados):
244
258
  if doc is not None and not isinstance(doc, dict):
245
259
  raise ValueError("doc precisa ser um objeto")
246
260
 
261
+ # ── O doc torto não pode virar arquivo ───────────────────────────────
262
+ # Quem cria é um agente de IA, e quando ele não viu a spec ele INVENTA a
263
+ # estrutura. Antes o arquivo era gravado assim mesmo: o player não achava
264
+ # cena nenhuma e o "vídeo" saía uma tela preta — o usuário viu exatamente
265
+ # isso. Recusar com o formato certo na mensagem transforma o erro mudo em
266
+ # correção na rodada seguinte.
267
+ if doc:
268
+ problema = validar_doc(POR_SLUG[ferramenta]["modelo"], doc)
269
+ if problema:
270
+ raise ValueError(problema)
271
+
247
272
  arq_id = ferramenta + "-" + slugify(titulo) + "-" + uuid.uuid4().hex[:5]
248
273
  return gravar({
249
274
  "id": arq_id,
@@ -253,6 +278,41 @@ def criar(dados):
253
278
  })
254
279
 
255
280
 
281
+ def validar_doc(modelo, doc):
282
+ """None quando o doc serve; senão, a frase que ENSINA o formato certo.
283
+
284
+ A mensagem repete a estrutura mínima de propósito: ela volta para o agente
285
+ como resultado da tool, e é a única chance de ele se corrigir sem o
286
+ usuário ver um vídeo preto."""
287
+ if modelo == "cena":
288
+ cenas = doc.get("cenas")
289
+ if not isinstance(cenas, list) or not cenas:
290
+ return ('doc sem "cenas". O formato é: {"tema":"...", "cenas":[{"layout":"centro",'
291
+ '"duracao":3,"elementos":[{"tipo":"texto","texto":"..."}]}]}. '
292
+ 'Leia studio_spec da ferramenta e refaça.')
293
+ for i, c in enumerate(cenas):
294
+ if not isinstance(c, dict):
295
+ return "cena %d não é um objeto" % (i + 1)
296
+ els = c.get("elementos")
297
+ if not isinstance(els, list) or not els:
298
+ return ('cena %d sem "elementos". Cada cena precisa de elementos:[...] — '
299
+ 'ex.: {"tipo":"texto","texto":"..."}. Leia studio_spec e refaça.' % (i + 1))
300
+ for j, e in enumerate(els):
301
+ if not isinstance(e, dict) or not e.get("tipo"):
302
+ return 'elemento %d da cena %d sem "tipo" (texto, imagem, lista, numeros, forma...)' % (j + 1, i + 1)
303
+ elif modelo == "documento":
304
+ blocos = doc.get("blocos")
305
+ if not isinstance(blocos, list) or not blocos:
306
+ return ('doc sem "blocos". O formato é: {"tema":"papel","blocos":[{"tipo":"titulo",'
307
+ '"texto":"..."},{"tipo":"paragrafo","texto":"..."}]}. Leia studio_spec e refaça.')
308
+ elif modelo == "planilha":
309
+ abas = doc.get("abas")
310
+ if not isinstance(abas, list) or not abas:
311
+ return ('doc sem "abas". O formato é: {"abas":[{"nome":"...","colunas":[...],'
312
+ '"linhas":[[...]]}]}. Leia studio_spec e refaça.')
313
+ return None
314
+
315
+
256
316
  def exemplo(ferramenta):
257
317
  """Documento inicial de cada ferramenta — o mínimo que já fica bonito."""
258
318
  if ferramenta == "prosa":
@@ -460,6 +520,9 @@ class Handler(SimpleHTTPRequestHandler):
460
520
  texto = fh.read()
461
521
  return self.txt(texto or json.dumps(info, ensure_ascii=False, indent=2))
462
522
 
523
+ if p == "/api/versao":
524
+ return self.js({"assinatura": ASSINATURA})
525
+
463
526
  if p == "/api/voz":
464
527
  return self.voz(urlparse(self.path).query)
465
528
  if p == "/api/voz/status":
@@ -706,6 +769,16 @@ class Handler(SimpleHTTPRequestHandler):
706
769
  corpo, status = guardar_midia(self.corpo())
707
770
  return self.js(corpo, status)
708
771
 
772
+ if rota == "/api/sair":
773
+ # Só o CLI chama, e só quando o código no disco mudou (update do
774
+ # npm). Sem isto o processo antigo ficava no ar servindo o bug que
775
+ # o update tinha acabado de corrigir. Responde antes de morrer,
776
+ # para quem pediu não ver erro de conexão.
777
+ self.js({"ok": True, "tchau": ASSINATURA})
778
+ import threading
779
+ threading.Thread(target=lambda: (time.sleep(0.3), os._exit(0)), daemon=True).start()
780
+ return
781
+
709
782
  if rota == "/api/arquivos":
710
783
  try:
711
784
  a = criar(self.corpo())