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.
- package/bin/primocode.js +10 -0
- package/lib/studio.js +61 -7
- package/package.json +2 -2
- package/studio/arquivos/corte-estatico-corte-a5bab.json +150 -0
- package/studio/arquivos/corte-founders-ai-teste-do-player-2de1d.json +65 -0
- package/studio/arquivos/corte-minimo-2a829.json +24 -0
- package/studio/arquivos/corte-ok-00a3b.json +18 -0
- package/studio/arquivos/corte-sweep-corte-cfb29.json +147 -0
- package/studio/arquivos/corte-visual-completo-78f50.json +35 -0
- package/studio/arquivos/grade-sweep-grade-0c2f1.json +119 -0
- package/studio/arquivos/prisma-estatico-prisma-d8740.json +182 -0
- package/studio/arquivos/prisma-sweep-prisma-4c58c.json +166 -0
- package/studio/arquivos/prisma-y-b0785.json +52 -0
- package/studio/arquivos/prosa-sweep-prosa-f29cf.json +80 -0
- package/studio/arquivos/tela-estatico-tela-c7daf.json +90 -0
- package/studio/arquivos/tela-sweep-tela-0d2bf.json +84 -0
- package/studio/arquivos/traco-estatico-traco-25872.json +113 -0
- package/studio/arquivos/traco-sweep-traco-0bcf4.json +102 -0
- package/studio/ferramentas/corte.md +62 -57
- package/studio/server.py +73 -0
|
@@ -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())
|