primocode 9.8.0-beta.12 → 9.8.0-beta.13
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/README.md +2 -2
- package/lib/act.js +12 -8
- package/lib/catalogo.js +5 -1
- package/lib/estudio/composicao.js +404 -0
- package/lib/estudio/edicao.js +3 -3
- package/lib/estudio/index.js +71 -1
- package/lib/estudio/plano.js +4 -1
- package/lib/estudio/remotion.js +77 -14
- package/lib/estudio/slides.js +34 -30
- package/lib/local/cerebro.json +28 -4
- package/package.json +2 -2
- package/studio/narrar.py +2 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
1
|
+
# PrimoCode v9.8.0-beta.13
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v9.8.0-beta.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.13
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/act.js
CHANGED
|
@@ -692,15 +692,19 @@ async function runAct(userPrompt, opts) {
|
|
|
692
692
|
|
|
693
693
|
Um modelo não chama tool fora da lista por conta própria. Só
|
|
694
694
|
chamou porque este parágrafo mandou. */
|
|
695
|
+
/* O caminho do ROTEIRO (lib/estudio/composicao.js). Antes esta
|
|
696
|
+
ordem mandava chamar o diretor e depois compor "cena a cena,
|
|
697
|
+
com as primitivas" — o modelo escrevia posição em pixel e o
|
|
698
|
+
vídeo saía quebrado, além de custar três ou quatro rodadas a
|
|
699
|
+
mais de modelo (medido em 25/09). */
|
|
695
700
|
messages.push({ role: 'system', content:
|
|
696
|
-
|
|
697
|
-
+ `
|
|
698
|
-
+ '
|
|
699
|
-
+ '
|
|
700
|
-
+ '
|
|
701
|
-
+ '
|
|
702
|
-
+ 'Nunca chame uma ferramenta que não está na sua lista — a chamada é recusada e a tarefa '
|
|
703
|
-
+ 'se perde.' });
|
|
701
|
+
`ESTE PEDIDO É DE PRODUÇÃO (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"). Faça em três passos: `
|
|
702
|
+
+ `estudio_projeto acao:"criar" com formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}" → `
|
|
703
|
+
+ 'estudio_plano com "roteiro" (partes com tipo e fala; o PrimoCode faz layout, voz, ícones, música e sons) → '
|
|
704
|
+
+ 'estudio_render. Se a peça for de uma marca de verdade, estudio_site antes, e use as cores e a logo em roteiro.marca. '
|
|
705
|
+
+ 'NÃO escreva cenas, elementos nem posições à mão. '
|
|
706
|
+
+ 'Se estudio_plano não aceitar "roteiro" (servidor antigo), siga com as ferramentas que você tem. '
|
|
707
|
+
+ 'Nunca chame uma ferramenta que não está na sua lista — a chamada é recusada e a tarefa se perde.' });
|
|
704
708
|
}
|
|
705
709
|
}
|
|
706
710
|
|
package/lib/catalogo.js
CHANGED
|
@@ -84,7 +84,11 @@ const GRUPOS = [
|
|
|
84
84
|
+ 'SEMPRE procure antes de criar quando o pedido falar de algo que já existe.' },
|
|
85
85
|
|
|
86
86
|
{ tool: 'estudio_plano', escreve: true, faz: 'escreve ou ajusta o que o vídeo É — cenas, elementos, animação, câmera, áudio',
|
|
87
|
-
args: 'id.
|
|
87
|
+
args: 'id. PREFIRA roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, '
|
|
88
|
+
+ 'partes:[{tipo, fala, …}]} — o PrimoCode faz o layout, a voz com entonação, os ícones, a música e os sons. '
|
|
89
|
+
+ 'Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens} passos{titulo,itens} '
|
|
90
|
+
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link}. '
|
|
91
|
+
+ 'voz (thalita|antonio|francisca). Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
88
92
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|
|
89
93
|
+ 'motivo (opcional): fica no caderno. voltarPara (opcional): nome de uma versão. '
|
|
90
94
|
+ 'Sem plano nem cenas, LÊ o plano atual. '
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* composicao.js — o roteiro vira cena, e quem faz o layout é o CÓDIGO.
|
|
3
|
+
*
|
|
4
|
+
* "Os motion estão bem quebrados, a voz está sem entonação, não tem música,
|
|
5
|
+
* efeitos sonoros, ícones… ele sobrepõe as coisas." · "Os slides entregou
|
|
6
|
+
* uma merda." (25/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* O modelo escrevia a posição de cada texto em pixel, à mão, e errava: texto
|
|
9
|
+
* cortado na borda, letra minúscula, meia tela vazia, nada de som. Aqui o
|
|
10
|
+
* modelo escreve só o ROTEIRO — o que cada parte diz e de que tipo ela é — e
|
|
11
|
+
* o código monta a cena:
|
|
12
|
+
*
|
|
13
|
+
* - a letra é MEDIDA para caber na caixa (nunca sai da tela, nunca some de
|
|
14
|
+
* tão pequena), e tudo respeita a margem de segurança;
|
|
15
|
+
* - cada tipo de parte tem a composição dele (abertura, número que conta,
|
|
16
|
+
* lista com ícones, contraste, citação, imagem, gráfico, passos, chamada),
|
|
17
|
+
* e a variação vem da marca e da posição no roteiro;
|
|
18
|
+
* - no vídeo, cada parte dura o que a voz dela dura, e o som (whoosh na
|
|
19
|
+
* transição, pop no item, música de cama) entra sozinho;
|
|
20
|
+
* - no slide, é a MESMA cena, parada.
|
|
21
|
+
*
|
|
22
|
+
* Função pura: roteiro + tempos da voz entram, plano sai.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const { iconeDe } = require('./edicao.js');
|
|
28
|
+
|
|
29
|
+
const TIPOS = ['abertura', 'titulo', 'numero', 'lista', 'contraste', 'citacao', 'imagem', 'grafico', 'passos', 'cta'];
|
|
30
|
+
|
|
31
|
+
// ── cor ──────────────────────────────────────────────────────────────────
|
|
32
|
+
function rgb(hex) {
|
|
33
|
+
const h = String(hex || '').replace('#', '');
|
|
34
|
+
const n = h.length === 3 ? h.split('').map((c) => c + c).join('') : h.padEnd(6, '0').slice(0, 6);
|
|
35
|
+
return [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16) || 0);
|
|
36
|
+
}
|
|
37
|
+
const hex = (c) => '#' + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('');
|
|
38
|
+
const misturar = (a, b, k) => hex(rgb(a).map((v, i) => v + (rgb(b)[i] - v) * k));
|
|
39
|
+
const luz = (c) => { const [r, g, b] = rgb(c); return (0.299 * r + 0.587 * g + 0.114 * b) / 255; };
|
|
40
|
+
const alfa = (c, a) => c + Math.round(a * 255).toString(16).padStart(2, '0');
|
|
41
|
+
|
|
42
|
+
function paleta(marca = {}) {
|
|
43
|
+
const c = marca.cores || {};
|
|
44
|
+
const fundo = c.fundo || '#0E0F14';
|
|
45
|
+
const escuro = luz(fundo) < 0.5;
|
|
46
|
+
const tinta = c.tinta || (escuro ? '#F4F2EC' : '#15161A');
|
|
47
|
+
const destaque = c.destaque || '#FF6A3D';
|
|
48
|
+
return {
|
|
49
|
+
fundo, tinta, destaque, escuro,
|
|
50
|
+
fundo2: misturar(fundo, tinta, escuro ? 0.07 : 0.05),
|
|
51
|
+
apoio: c.apoio || misturar(destaque, tinta, 0.35),
|
|
52
|
+
suave: alfa(tinta, 0.68),
|
|
53
|
+
// Sobre o destaque, a tinta que dá contraste.
|
|
54
|
+
sobreDestaque: luz(destaque) > 0.6 ? '#15161A' : '#FFFFFF',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── a letra que cabe ─────────────────────────────────────────────────────
|
|
59
|
+
* Largura média do caractere em "em" por família. Não é exata — é o
|
|
60
|
+
* bastante para escolher o maior tamanho que cabe em N linhas, com folga. */
|
|
61
|
+
const LARGURA = { condensada: 0.43, display: 0.56, texto: 0.52, serifa: 0.5 };
|
|
62
|
+
const FAMILIA_TIPO = (f) => (/condensed|narrow|anton|oswald|bebas|teko|league gothic/i.test(f) ? 'condensada'
|
|
63
|
+
: /serif|fraunces|playfair|garamond|lora|newsreader|spectral|baskerville|dm serif/i.test(f) ? 'serifa'
|
|
64
|
+
: /black|bricolage|archivo|sora|grotesk|syne|unbounded/i.test(f) ? 'display' : 'texto');
|
|
65
|
+
|
|
66
|
+
function caber(texto, { largura, linhas = 2, max = 200, min = 18, fonte = 'Inter' }) {
|
|
67
|
+
const cw = LARGURA[FAMILIA_TIPO(fonte)];
|
|
68
|
+
const palavras = String(texto || '').split(/\s+/).filter(Boolean);
|
|
69
|
+
for (let t = Math.round(max); t >= min; t -= 2) {
|
|
70
|
+
// Quebra de verdade, palavra por palavra: é o que o navegador fará.
|
|
71
|
+
let n = 1, linha = 0;
|
|
72
|
+
let cabe = true;
|
|
73
|
+
for (const w of palavras) {
|
|
74
|
+
const lw = (w.length + 1) * cw * t;
|
|
75
|
+
if (lw > largura * 1.02) { cabe = false; break; } // uma palavra sozinha não cabe
|
|
76
|
+
if (linha + lw > largura) { n++; linha = lw; } else linha += lw;
|
|
77
|
+
}
|
|
78
|
+
if (cabe && n <= linhas) return { tamanho: t, linhas: n };
|
|
79
|
+
}
|
|
80
|
+
return { tamanho: min, linhas };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── as partes do roteiro ─────────────────────────────────────────────────
|
|
84
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
85
|
+
const TRANSICOES = ['deslizar', 'zoom', 'mascara', 'esmaecer', 'morph'];
|
|
86
|
+
|
|
87
|
+
function textoDe(p) {
|
|
88
|
+
return [p.titulo, p.texto, p.valor, p.rotulo, p.sub, p.nao, p.sim, ...(p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto))]
|
|
89
|
+
.filter(Boolean).join(' ');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** O que a voz diz nesta parte. Sem "fala", a voz lê o que está na tela. */
|
|
93
|
+
function falaDe(p) {
|
|
94
|
+
if (p.fala) return String(p.fala);
|
|
95
|
+
if (p.tipo === 'numero') return `${p.valor} ${p.rotulo || ''}.`;
|
|
96
|
+
return textoDe(p);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** O registro da voz para esta parte (ver REGISTRO em studio/narrar.py). */
|
|
100
|
+
function registroDe(p, i, n) {
|
|
101
|
+
if (i === 0) return 'gancho';
|
|
102
|
+
if (p.tipo === 'cta' || i === n - 1) return 'cta';
|
|
103
|
+
return { numero: 'numero', contraste: 'contraste', lista: 'lista', citacao: 'citacao', passos: 'lista' }[p.tipo] || 'normal';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function iconesDoRoteiro(roteiro) {
|
|
107
|
+
const nomes = new Set();
|
|
108
|
+
for (const p of roteiro.partes || []) {
|
|
109
|
+
for (const it of p.itens || []) {
|
|
110
|
+
const t = typeof it === 'string' ? it : it.texto;
|
|
111
|
+
const ic = (typeof it === 'object' && it.icone) || iconeDoTexto(t);
|
|
112
|
+
if (ic) nomes.add(ic);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...nomes];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function iconeDoTexto(t) {
|
|
119
|
+
for (const w of String(t || '').split(/\s+/)) { const ic = iconeDe(w); if (ic) return ic; }
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* O plano.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} op
|
|
127
|
+
* @param {object} op.roteiro { partes: [{tipo, …}], marca: {cores, fontes, logo} }
|
|
128
|
+
* @param {string} op.modo 'video' | 'slides'
|
|
129
|
+
* @param {Array} op.tempos no vídeo: [{de, ate, palavras}] da voz de cada parte
|
|
130
|
+
* @param {string} op.narracao o mp3 da voz (vídeo)
|
|
131
|
+
* @param {object} op.sons { pop, whoosh }
|
|
132
|
+
* @param {string} op.trilha a música de cama (vídeo)
|
|
133
|
+
* @param {object} op.imagens { indiceDaParte: src }
|
|
134
|
+
* @param {Set} op.iconesOk os ícones que existem
|
|
135
|
+
*/
|
|
136
|
+
function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080, tempos = [], narracao = null,
|
|
137
|
+
sons = {}, trilha = null, imagens = {}, iconesOk = null }) {
|
|
138
|
+
const partes = (roteiro && roteiro.partes) || [];
|
|
139
|
+
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
140
|
+
const ruins = partes.map((p, i) => (TIPOS.includes(p.tipo) ? null : `parte ${i + 1}: tipo "${p.tipo}" não existe (use ${TIPOS.join(', ')})`)).filter(Boolean);
|
|
141
|
+
if (ruins.length) return { ok: false, error: 'o roteiro tem partes de tipo desconhecido.', problemas: ruins };
|
|
142
|
+
|
|
143
|
+
const marca = roteiro.marca || {};
|
|
144
|
+
const cor = paleta(marca);
|
|
145
|
+
const F = {
|
|
146
|
+
titulo: (marca.fontes && marca.fontes.titulo) || 'Bricolage Grotesque',
|
|
147
|
+
texto: (marca.fontes && marca.fontes.texto) || 'Inter',
|
|
148
|
+
serifa: (marca.fontes && marca.fontes.serifa) || 'Fraunces',
|
|
149
|
+
};
|
|
150
|
+
const W = largura, H = altura;
|
|
151
|
+
const M = Math.round(Math.min(W, H) * 0.08); // a margem de segurança
|
|
152
|
+
const vid = modo === 'video';
|
|
153
|
+
const deitado = W >= H;
|
|
154
|
+
const cenas = [];
|
|
155
|
+
const efeitos = [];
|
|
156
|
+
|
|
157
|
+
// No slide tudo já entrou quando o quadro do meio é tirado (1,5 s).
|
|
158
|
+
const atraso = (t) => (vid ? t : Math.min(t, 0.9) * 0.6);
|
|
159
|
+
|
|
160
|
+
partes.forEach((p, i) => {
|
|
161
|
+
const tp = tempos[i] || {};
|
|
162
|
+
const proxDe = tempos[i + 1] ? tempos[i + 1].de : null;
|
|
163
|
+
const base = vid ? Math.max(1.6, (proxDe != null ? proxDe : (tp.ate || 0) + 0.9) - (tp.de || 0)) : 3;
|
|
164
|
+
const trans = !vid || i === 0 ? 'corte' : TRANSICOES[i % TRANSICOES.length];
|
|
165
|
+
const cruz = trans === 'corte' ? 0 : 0.35;
|
|
166
|
+
const dur = r2(base + cruz);
|
|
167
|
+
const els = [];
|
|
168
|
+
const variante = i % 2;
|
|
169
|
+
|
|
170
|
+
// ── o fundo: cor, profundidade e movimento, nunca chapado e morto ──
|
|
171
|
+
let fundo = { tipo: 'cor', valor: variante ? cor.fundo2 : cor.fundo };
|
|
172
|
+
let tinta = cor.tinta, destaque = cor.destaque, suave = cor.suave;
|
|
173
|
+
if (p.tipo === 'numero') { fundo = { tipo: 'cor', valor: cor.destaque }; tinta = cor.sobreDestaque; destaque = cor.sobreDestaque; suave = alfa(cor.sobreDestaque, 0.75); }
|
|
174
|
+
if (p.tipo !== 'imagem') {
|
|
175
|
+
const R = Math.round(Math.max(W, H) * 0.62);
|
|
176
|
+
const canto = [[W - R * 0.55, -R * 0.45], [-R * 0.45, H - R * 0.5], [W - R * 0.4, H - R * 0.55], [-R * 0.5, -R * 0.4]][i % 4];
|
|
177
|
+
els.push({
|
|
178
|
+
tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(canto[0]), y: Math.round(canto[1]),
|
|
179
|
+
cor: p.tipo === 'numero' ? cor.sobreDestaque : cor.destaque, opacidade: p.tipo === 'numero' ? 0.12 : (cor.escuro ? 0.22 : 0.16),
|
|
180
|
+
anima: {
|
|
181
|
+
desfoque: [{ t: 0, v: Math.round(R * 0.22) }],
|
|
182
|
+
x: [{ t: 0, v: 0 }, { t: dur, v: variante ? 60 : -60, e: 'suave' }],
|
|
183
|
+
y: [{ t: 0, v: 0 }, { t: dur, v: variante ? -40 : 40, e: 'suave' }],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const W0 = W - 2 * M;
|
|
189
|
+
const titulo = (texto, { y, larg = W0, linhas = 2, max = H * 0.12, x = M, alinhar = 'left', de = 0.15, cor: c = tinta, destaqueUltima = true }) => {
|
|
190
|
+
const fit = caber(texto, { largura: larg, linhas, max, fonte: F.titulo });
|
|
191
|
+
const ws = String(texto).split(/\s+/);
|
|
192
|
+
els.push({
|
|
193
|
+
tipo: 'texto', texto, fonte: F.titulo, peso: 800, tamanho: fit.tamanho, cor: c, entrelinha: 1.04,
|
|
194
|
+
x, y: Math.round(y), largura: Math.round(larg), alinhamento: alinhar,
|
|
195
|
+
revelar: ws.map((_, k) => r2(atraso(de + k * 0.07))),
|
|
196
|
+
...(destaqueUltima && ws.length > 2 ? { destaques: [ws.length - 1], corDestaque: destaque } : {}),
|
|
197
|
+
});
|
|
198
|
+
return fit.tamanho * 1.04 * fit.linhas;
|
|
199
|
+
};
|
|
200
|
+
const linhaFina = (texto, { y, larg = W0, x = M, max = H * 0.04, de = 0.5, alinhar = 'left', c = suave, linhas = 2 }) => {
|
|
201
|
+
const fit = caber(texto, { largura: larg, linhas, max, fonte: F.texto });
|
|
202
|
+
els.push({
|
|
203
|
+
tipo: 'texto', texto, fonte: F.texto, peso: 500, tamanho: fit.tamanho, cor: c, entrelinha: 1.3,
|
|
204
|
+
x, y: Math.round(y), largura: Math.round(larg), alinhamento: alinhar,
|
|
205
|
+
anima: { opacidade: [{ t: atraso(de), v: 0 }, { t: atraso(de) + 0.4, v: 1 }], y: [{ t: atraso(de), v: 18 }, { t: atraso(de) + 0.5, v: 0, e: 'saida' }] },
|
|
206
|
+
});
|
|
207
|
+
return fit.tamanho * 1.3 * fit.linhas;
|
|
208
|
+
};
|
|
209
|
+
const traco = (x, y, c = destaque, de = 0.05) => els.push({
|
|
210
|
+
tipo: 'forma', forma: 'retangulo', x, y: Math.round(y), largura: Math.round(W * 0.06), altura: Math.max(6, Math.round(H * 0.008)),
|
|
211
|
+
cor: c, origem: 'left', anima: { escalaX: [{ t: atraso(de), v: 0 }, { t: atraso(de) + 0.5, v: 1, e: 'saida' }] },
|
|
212
|
+
});
|
|
213
|
+
const logo = (x, y, alt) => { if (marca.logo) els.push({ tipo: 'imagem', src: marca.logo, x, y, altura: alt, largura: Math.round(alt * 4), ajuste: 'contain', foco: 'left center', anima: { opacidade: [{ t: atraso(0.3), v: 0 }, { t: atraso(0.3) + 0.4, v: 1 }] } }); };
|
|
214
|
+
|
|
215
|
+
switch (p.tipo) {
|
|
216
|
+
case 'abertura':
|
|
217
|
+
case 'titulo': {
|
|
218
|
+
const centro = p.tipo === 'titulo' && variante === 1;
|
|
219
|
+
const larg = centro ? W0 * 0.86 : W0 * (deitado ? 0.78 : 1);
|
|
220
|
+
const fit = caber(p.titulo || '', { largura: larg, linhas: 3, max: H * (p.tipo === 'abertura' ? 0.15 : 0.12), fonte: F.titulo });
|
|
221
|
+
const hT = fit.tamanho * 1.04 * fit.linhas;
|
|
222
|
+
const hS = p.sub ? H * 0.09 : 0;
|
|
223
|
+
let y = (H - hT - hS) / 2;
|
|
224
|
+
const x = centro ? (W - larg) / 2 : M;
|
|
225
|
+
if (p.tipo === 'abertura') logo(M, M, Math.round(H * 0.05));
|
|
226
|
+
if (!centro) traco(M, y - H * 0.05);
|
|
227
|
+
titulo(p.titulo || '', { y, larg, linhas: 3, max: fit.tamanho, x, alinhar: centro ? 'center' : 'left' });
|
|
228
|
+
if (p.sub) linhaFina(p.sub, { y: y + hT + H * 0.03, larg, x, alinhar: centro ? 'center' : 'left', de: 0.6 });
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'numero': {
|
|
232
|
+
const valor = String(p.valor == null ? '' : p.valor);
|
|
233
|
+
const m = valor.match(/^([^\d-]*)(-?[\d.,]+)(.*)$/);
|
|
234
|
+
const larg = deitado ? W0 * 0.5 : W0;
|
|
235
|
+
const fit = caber(valor, { largura: larg, linhas: 1, max: H * (deitado ? 0.5 : 0.3), fonte: F.titulo });
|
|
236
|
+
const yN = deitado ? (H - fit.tamanho) / 2 - H * 0.03 : H * 0.22;
|
|
237
|
+
const num = Number(m ? m[2].replace(/\./g, '').replace(',', '.') : NaN);
|
|
238
|
+
els.push({
|
|
239
|
+
tipo: 'texto', texto: valor, fonte: F.titulo, peso: 800, tamanho: fit.tamanho, cor: tinta, entrelinha: 1,
|
|
240
|
+
x: M, y: Math.round(yN), largura: Math.round(larg + M),
|
|
241
|
+
...(m && !Number.isNaN(num) ? { contar: { ate: num, dur: vid ? 1.3 : 0.6, antes: m[1], depois: m[3], casas: (m[2].split(',')[1] || '').length } } : {}),
|
|
242
|
+
anima: { escala: [{ t: 0, v: 0.86 }, { t: atraso(0.5), v: 1, e: 'saida' }], opacidade: [{ t: 0, v: 0 }, { t: 0.15, v: 1 }] },
|
|
243
|
+
});
|
|
244
|
+
const xR = deitado ? M + larg + W * 0.04 : M;
|
|
245
|
+
const lR = deitado ? W - xR - M : W0;
|
|
246
|
+
const yR = deitado ? H * 0.38 : yN + fit.tamanho + H * 0.04;
|
|
247
|
+
const hR = titulo(p.rotulo || '', { y: yR, larg: lR, linhas: 3, max: H * 0.075, x: xR, de: 0.4, destaqueUltima: false });
|
|
248
|
+
if (p.sub) linhaFina(p.sub, { y: yR + hR + H * 0.03, larg: lR, x: xR, de: 0.8 });
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case 'lista':
|
|
252
|
+
case 'passos': {
|
|
253
|
+
const itens = (p.itens || []).slice(0, 5).map((it) => (typeof it === 'string' ? { texto: it } : it));
|
|
254
|
+
const n = itens.length || 1;
|
|
255
|
+
const colunas = deitado && n <= 4 ? n : 1;
|
|
256
|
+
const gap = W * 0.025;
|
|
257
|
+
const wCard = colunas > 1 ? (W0 - gap * (colunas - 1)) / colunas : W0;
|
|
258
|
+
/* O bloco inteiro (título + itens) fica no meio da altura: com
|
|
259
|
+
o título no topo e os itens logo abaixo, sobrava meia tela
|
|
260
|
+
vazia embaixo. */
|
|
261
|
+
const fT = p.titulo ? caber(p.titulo, { largura: W0, linhas: 2, max: H * 0.1, fonte: F.titulo }) : { tamanho: 0, linhas: 0 };
|
|
262
|
+
const hTit = fT.tamanho * 1.04 * fT.linhas;
|
|
263
|
+
const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * 0.14;
|
|
264
|
+
const hItens = colunas > 1 ? hCard0 : n * hCard0 + (n - 1) * gap;
|
|
265
|
+
const bloco = hTit + (hTit ? H * 0.07 : 0) + hItens;
|
|
266
|
+
const yTit = Math.max(M, (H - bloco) / 2);
|
|
267
|
+
if (p.titulo) titulo(p.titulo, { y: yTit, linhas: 2, max: fT.tamanho });
|
|
268
|
+
const topo = yTit + hTit + (hTit ? H * 0.07 : 0);
|
|
269
|
+
const hCard = Math.min(hCard0, colunas > 1 ? H - topo - M : (H - topo - M - gap * (n - 1)) / n);
|
|
270
|
+
const dur0 = vid ? Math.max(1, (tp.ate || dur) - (tp.de || 0)) : 1;
|
|
271
|
+
itens.forEach((it, k) => {
|
|
272
|
+
const cx = colunas > 1 ? M + k * (wCard + gap) : M;
|
|
273
|
+
const cy = colunas > 1 ? topo : topo + k * (hCard + gap);
|
|
274
|
+
// O item entra quando a voz chega nele (ou em cascata, no slide).
|
|
275
|
+
const quando = atraso(vid ? 0.5 + (dur0 * 0.85 - 0.5) * (k / n) : 0.3 + k * 0.12);
|
|
276
|
+
const entra = { opacidade: [{ t: quando, v: 0 }, { t: quando + 0.3, v: 1 }], y: [{ t: quando, v: 40 }, { t: quando + 0.45, v: 0, e: 'saida' }] };
|
|
277
|
+
if (p.tipo === 'lista') {
|
|
278
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: Math.round(cx), y: Math.round(cy), largura: Math.round(wCard), altura: Math.round(hCard), cor: alfa(tinta, cor.escuro ? 0.07 : 0.06), raio: Math.round(H * 0.025), anima: entra });
|
|
279
|
+
} else if (k < n - 1 && colunas > 1) {
|
|
280
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: Math.round(cx + H * 0.05), y: Math.round(cy + H * 0.05), largura: Math.round(wCard + gap), altura: 4, cor: alfa(destaque, 0.5), origem: 'left', anima: { escalaX: [{ t: quando, v: 0 }, { t: quando + 0.6, v: 1, e: 'suave' }] } });
|
|
281
|
+
}
|
|
282
|
+
const tamI = Math.round(Math.min(hCard * (colunas > 1 ? 0.28 : 0.5), H * 0.09));
|
|
283
|
+
const ic = it.icone || iconeDoTexto(it.texto);
|
|
284
|
+
const pad = Math.round(Math.min(wCard, hCard) * 0.14);
|
|
285
|
+
if (p.tipo === 'passos') {
|
|
286
|
+
els.push({ tipo: 'forma', forma: 'circulo', x: Math.round(cx + pad * 0.3), y: Math.round(cy + pad * 0.3), largura: tamI, altura: tamI, cor: destaque, anima: { ...entra, escala: [{ t: quando, v: 0 }, { t: quando + 0.35, v: 1, e: 'elastico' }] } });
|
|
287
|
+
els.push({ tipo: 'texto', texto: String(k + 1), fonte: F.titulo, peso: 800, tamanho: Math.round(tamI * 0.5), cor: cor.sobreDestaque, x: Math.round(cx + pad * 0.3 + tamI / 2), y: Math.round(cy + pad * 0.3 + tamI / 2), ancora: 'centro', anima: entra });
|
|
288
|
+
} else if (ic && (!iconesOk || iconesOk.has(ic))) {
|
|
289
|
+
els.push({ tipo: 'icone', nome: ic, tamanho: tamI, x: Math.round(cx + pad), y: Math.round(cy + (colunas > 1 ? pad : (hCard - tamI) / 2)), anima: { ...entra, escala: [{ t: quando, v: 0 }, { t: quando + 0.3, v: 1.12, e: 'saida' }, { t: quando + 0.45, v: 1 }] } });
|
|
290
|
+
}
|
|
291
|
+
if (vid && sons.pop) efeitos.push({ t: r2((tp.de || 0) + quando), src: sons.pop, volume: 0.5 });
|
|
292
|
+
const tx = colunas > 1 ? cx + pad : cx + pad + tamI + pad;
|
|
293
|
+
const ty = colunas > 1 ? cy + pad + tamI + pad * 0.8 : cy + hCard / 2;
|
|
294
|
+
const lw = colunas > 1 ? wCard - 2 * pad : wCard - (tamI + 3 * pad);
|
|
295
|
+
const fit = caber(it.texto, { largura: lw, linhas: colunas > 1 ? 4 : 2, max: Math.min(H * 0.05, hCard * 0.3), fonte: F.texto });
|
|
296
|
+
els.push({
|
|
297
|
+
tipo: 'texto', texto: it.texto, fonte: F.texto, peso: 650, tamanho: fit.tamanho, cor: tinta, entrelinha: 1.2,
|
|
298
|
+
x: Math.round(tx), y: Math.round(colunas > 1 ? ty : ty - fit.tamanho * 1.2 * fit.linhas / 2), largura: Math.round(lw), anima: entra,
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case 'contraste': {
|
|
304
|
+
const larg = W0 * 0.9;
|
|
305
|
+
const f1 = caber(p.nao || '', { largura: larg, linhas: 2, max: H * 0.07, fonte: F.texto });
|
|
306
|
+
const f2 = caber(p.sim || '', { largura: larg, linhas: 3, max: H * 0.13, fonte: F.titulo });
|
|
307
|
+
const hTot = f1.tamanho * 1.2 * f1.linhas + H * 0.05 + f2.tamanho * 1.04 * f2.linhas;
|
|
308
|
+
const y1 = (H - hTot) / 2;
|
|
309
|
+
const meio = vid ? Math.max(0.8, ((tp.ate || dur) - (tp.de || 0)) * 0.45) : 0.5;
|
|
310
|
+
els.push({ tipo: 'texto', texto: p.nao || '', fonte: F.texto, peso: 600, tamanho: f1.tamanho, cor: alfa(tinta, 0.5), x: M, y: Math.round(y1), largura: Math.round(larg), entrelinha: 1.2, anima: { opacidade: [{ t: 0.1, v: 0 }, { t: 0.4, v: 1 }] } });
|
|
311
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: M, y: Math.round(y1 + f1.tamanho * 0.62), largura: Math.round(Math.min(larg, (p.nao || '').length * f1.tamanho * 0.52)), altura: Math.max(5, Math.round(f1.tamanho * 0.09)), cor: destaque, origem: 'left', anima: { escalaX: [{ t: atraso(meio - 0.3), v: 0 }, { t: atraso(meio), v: 1, e: 'saida' }] } });
|
|
312
|
+
titulo(p.sim || '', { y: y1 + f1.tamanho * 1.2 * f1.linhas + H * 0.05, larg, linhas: 3, max: f2.tamanho, de: atraso(meio) });
|
|
313
|
+
if (vid && sons.pop) efeitos.push({ t: r2((tp.de || 0) + meio), src: sons.pop, volume: 0.5 });
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case 'citacao': {
|
|
317
|
+
const larg = W0 * 0.84;
|
|
318
|
+
const fit = caber(p.texto || '', { largura: larg, linhas: 4, max: H * 0.085, fonte: F.serifa });
|
|
319
|
+
const hT = fit.tamanho * 1.18 * fit.linhas;
|
|
320
|
+
const y = (H - hT) / 2 + H * 0.04;
|
|
321
|
+
els.push({ tipo: 'texto', texto: '“', fonte: F.serifa, peso: 700, tamanho: Math.round(H * 0.3), cor: destaque, x: M, y: Math.round(y - H * 0.24), anima: { opacidade: [{ t: 0, v: 0 }, { t: 0.3, v: 1 }], escala: [{ t: 0, v: 0.7 }, { t: 0.5, v: 1, e: 'saida' }] } });
|
|
322
|
+
const ws = String(p.texto || '').split(/\s+/);
|
|
323
|
+
els.push({ tipo: 'texto', texto: p.texto || '', fonte: F.serifa, peso: 500, italico: true, tamanho: fit.tamanho, cor: tinta, entrelinha: 1.18, x: Math.round(M + W * 0.04), y: Math.round(y), largura: Math.round(larg), revelar: ws.map((_, k) => r2(atraso(0.2 + k * 0.09))) });
|
|
324
|
+
if (p.autor) linhaFina('— ' + p.autor, { y: y + hT + H * 0.04, x: Math.round(M + W * 0.04), larg, de: 0.9, c: destaque });
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case 'imagem': {
|
|
328
|
+
const src = imagens[i] || p.src;
|
|
329
|
+
if (src) {
|
|
330
|
+
els.push({ tipo: 'imagem', src, x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
|
|
331
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.25)', 'rgba(0,0,0,0.85)'], angulo: 180 } });
|
|
332
|
+
}
|
|
333
|
+
const larg = W0 * 0.8;
|
|
334
|
+
const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * 0.1, fonte: F.titulo });
|
|
335
|
+
const hT = fit.tamanho * 1.04 * fit.linhas;
|
|
336
|
+
const yT = H - M - hT - (p.sub ? H * 0.08 : 0);
|
|
337
|
+
if (p.titulo) titulo(p.titulo, { y: yT, larg, max: fit.tamanho, cor: '#FFFFFF', de: 0.3 });
|
|
338
|
+
if (p.sub) linhaFina(p.sub, { y: yT + hT + H * 0.02, larg, c: 'rgba(255,255,255,0.8)', de: 0.7 });
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case 'grafico': {
|
|
342
|
+
const barras = (p.barras || []).slice(0, 7);
|
|
343
|
+
const hT = p.titulo ? titulo(p.titulo, { y: M * 1.1, linhas: 2, max: H * 0.08 }) : 0;
|
|
344
|
+
const topo = M * 1.1 + hT + H * 0.12;
|
|
345
|
+
const baseY = H - M - H * 0.08;
|
|
346
|
+
const hMax = baseY - topo;
|
|
347
|
+
const maior = Math.max(1, ...barras.map((b) => Number(b.valor) || 0));
|
|
348
|
+
const gap = W0 * 0.03;
|
|
349
|
+
const wB = (W0 - gap * (barras.length - 1)) / Math.max(1, barras.length);
|
|
350
|
+
barras.forEach((b, k) => {
|
|
351
|
+
const h = Math.max(6, hMax * (Number(b.valor) || 0) / maior);
|
|
352
|
+
const x = M + k * (wB + gap);
|
|
353
|
+
const q = atraso(0.4 + k * 0.12);
|
|
354
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: Math.round(x), y: Math.round(baseY - h), largura: Math.round(wB), altura: Math.round(h), cor: k === barras.length - 1 ? destaque : alfa(tinta, 0.25), raio: Math.round(wB * 0.08), origem: 'bottom', anima: { escalaY: [{ t: q, v: 0 }, { t: q + 0.6, v: 1, e: 'saida' }] } });
|
|
355
|
+
const fv = Math.round(Math.min(H * 0.05, wB * 0.35));
|
|
356
|
+
els.push({ tipo: 'texto', texto: String(b.valor), fonte: F.titulo, peso: 800, tamanho: fv, cor: tinta, x: Math.round(x + wB / 2), y: Math.round(baseY - h - fv * 1.4), ancora: 'centro-x', contar: { ate: Number(b.valor) || 0, dur: 0.8 }, anima: { opacidade: [{ t: q, v: 0 }, { t: q + 0.3, v: 1 }] } });
|
|
357
|
+
const fr = caber(b.rotulo || '', { largura: wB, linhas: 2, max: H * 0.03, fonte: F.texto });
|
|
358
|
+
els.push({ tipo: 'texto', texto: b.rotulo || '', fonte: F.texto, peso: 500, tamanho: fr.tamanho, cor: suave, x: Math.round(x), y: Math.round(baseY + H * 0.02), largura: Math.round(wB), alinhamento: 'center' });
|
|
359
|
+
});
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case 'cta': {
|
|
363
|
+
const larg = W0 * 0.84;
|
|
364
|
+
const fit = caber(p.titulo || '', { largura: larg, linhas: 3, max: H * 0.12, fonte: F.titulo });
|
|
365
|
+
const hT = fit.tamanho * 1.04 * fit.linhas;
|
|
366
|
+
const temLink = Boolean(p.link || p.sub);
|
|
367
|
+
const y = (H - hT - (temLink ? H * 0.16 : 0)) / 2;
|
|
368
|
+
titulo(p.titulo || '', { y, larg, max: fit.tamanho, x: (W - larg) / 2, alinhar: 'center' });
|
|
369
|
+
if (temLink) {
|
|
370
|
+
const txt = p.link || p.sub;
|
|
371
|
+
const fl = caber(txt, { largura: larg * 0.7, linhas: 1, max: H * 0.045, fonte: F.texto });
|
|
372
|
+
const wP = Math.min(larg, txt.length * fl.tamanho * 0.55 + fl.tamanho * 2.4);
|
|
373
|
+
const hP = fl.tamanho * 2.2;
|
|
374
|
+
const yP = y + hT + H * 0.06;
|
|
375
|
+
const q = atraso(0.8);
|
|
376
|
+
const entra = { opacidade: [{ t: q, v: 0 }, { t: q + 0.3, v: 1 }], escala: [{ t: q, v: 0.8 }, { t: q + 0.4, v: 1, e: 'elastico' }] };
|
|
377
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: Math.round((W - wP) / 2), y: Math.round(yP), largura: Math.round(wP), altura: Math.round(hP), cor: destaque, raio: Math.round(hP / 2), anima: entra });
|
|
378
|
+
els.push({ tipo: 'texto', texto: txt, fonte: F.texto, peso: 700, tamanho: fl.tamanho, cor: cor.sobreDestaque, x: Math.round(W / 2), y: Math.round(yP + hP / 2), ancora: 'centro', anima: entra });
|
|
379
|
+
if (vid && sons.pop) efeitos.push({ t: r2((tp.de || 0) + q), src: sons.pop, volume: 0.55 });
|
|
380
|
+
}
|
|
381
|
+
logo(Math.round(W / 2 - H * 0.1), Math.round(H - M - H * 0.05), Math.round(H * 0.05));
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
default: break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const cena = { duracao: dur, fundo, elementos: els, transicao: { tipo: trans, duracao: cruz } };
|
|
388
|
+
if (vid) cena.camera = { zoom: variante ? [{ t: 0, v: 1.04 }, { t: dur, v: 1, e: 'suave' }] : [{ t: 0, v: 1 }, { t: dur, v: 1.04, e: 'suave' }] };
|
|
389
|
+
cenas.push(cena);
|
|
390
|
+
if (vid && i > 0 && sons.whoosh) efeitos.push({ t: r2(Math.max(0, (tp.de || 0) - 0.15)), src: sons.whoosh, volume: 0.4 });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const audio = {};
|
|
394
|
+
if (vid && narracao) audio.narracao = narracao;
|
|
395
|
+
if (vid && trilha) audio.trilha = { src: trilha, volume: 0.12, loop: true };
|
|
396
|
+
if (vid && efeitos.length) audio.efeitos = efeitos;
|
|
397
|
+
return {
|
|
398
|
+
ok: true,
|
|
399
|
+
plano: { formato: { largura: W, altura: H, fps: 30 }, fundoGeral: cor.fundo, cenas, ...(Object.keys(audio).length ? { audio } : {}) },
|
|
400
|
+
resumo: { cenas: cenas.length, efeitos: efeitos.length, musica: Boolean(audio.trilha), voz: Boolean(audio.narracao) },
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = { comporRoteiro, caber, paleta, falaDe, registroDe, iconesDoRoteiro, TIPOS };
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -42,7 +42,7 @@ const ICONES = [
|
|
|
42
42
|
[['inteligencia', 'ia', 'robo', 'maquina', 'automac'], 'robot'],
|
|
43
43
|
[['cerebro', 'pens', 'raciocin', 'aprend', 'mente'], 'brain'],
|
|
44
44
|
[['ideia', 'criativ', 'inova'], 'light-bulb'],
|
|
45
|
-
[['dinheiro', 'grana', 'reais', 'lucro', 'vend', 'fatur', 'ganh', 'pagar', 'preco'], 'money-bag'],
|
|
45
|
+
[['dinheiro', 'grana', 'reais', 'lucro', 'vend', 'fatur', 'ganh', 'pagar', 'preco', 'financ', 'custo', 'econom', 'receita'], 'money-bag'],
|
|
46
46
|
[['tempo', 'minuto', 'segundo', 'rapid', 'hora'], 'stopwatch'],
|
|
47
47
|
[['cresc', 'result', 'aument', 'melhor'], 'chart-increasing'],
|
|
48
48
|
[['cair', 'caiu', 'perd', 'queda', 'pior'], 'chart-decreasing'],
|
|
@@ -70,9 +70,9 @@ const ICONES = [
|
|
|
70
70
|
[['padr', 'peca', 'encaix'], 'puzzle-piece'],
|
|
71
71
|
[['decid', 'decis', 'escolh', 'duvida', 'pergunta'], 'thinking-face'],
|
|
72
72
|
[['exemplo', 'procur', 'busca', 'pesquis', 'analis'], 'magnifying-glass-tilted-left'],
|
|
73
|
-
[['assistente', 'convers', 'chat', 'mensag', 'responde'], 'speech-balloon'],
|
|
73
|
+
[['assistente', 'convers', 'chat', 'mensag', 'responde', 'atend', 'suporte', 'whatsapp'], 'speech-balloon'],
|
|
74
74
|
[['recomend', 'filme', 'serie', 'netflix', 'video'], 'clapper-board'],
|
|
75
|
-
[['tecnolog', 'algoritm', 'sistema', 'ferrament'], 'gear'],
|
|
75
|
+
[['tecnolog', 'algoritm', 'sistema', 'ferrament', 'operac', 'process', 'automat'], 'gear'],
|
|
76
76
|
[['cotidian', 'rotina', 'semana', 'calendario', 'agenda'], 'calendar'],
|
|
77
77
|
[['conhecimento', 'formad', 'diploma', 'sabe'], 'graduation-cap'],
|
|
78
78
|
[['sucesso', 'venc', 'ganhador', 'premio', 'campeao'], 'trophy'],
|
package/lib/estudio/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const video = require('./video.js');
|
|
|
41
41
|
const identidade = require('./identidade.js');
|
|
42
42
|
const slides = require('./slides.js');
|
|
43
43
|
const edicao = require('./edicao.js');
|
|
44
|
+
const composicao = require('./composicao.js');
|
|
44
45
|
|
|
45
46
|
const PY = path.join(__dirname, '..', '..', 'studio');
|
|
46
47
|
|
|
@@ -172,12 +173,31 @@ async function ferramentaPlano(args) {
|
|
|
172
173
|
|
|
173
174
|
if (args.voltarPara) return projeto.voltarPara(args.id, args.voltarPara);
|
|
174
175
|
|
|
175
|
-
if (!args.plano && !args.cenas) {
|
|
176
|
+
if (!args.plano && !args.cenas && !args.roteiro) {
|
|
176
177
|
const atual = projeto.lerPlano(args.id);
|
|
177
178
|
if (!atual) return { ok: false, error: 'este projeto ainda não tem plano — mande um.' };
|
|
178
179
|
return { ok: true, plano: atual, resumo: plano.resumo(atual) };
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/* O ROTEIRO: o modelo diz o que cada parte é e o que ela diz, e o código
|
|
183
|
+
faz o resto — layout, voz com entonação, ícones, imagens, música e
|
|
184
|
+
sons (lib/estudio/composicao.js). É o caminho para qualquer modelo
|
|
185
|
+
entregar motion e slides que prestam. */
|
|
186
|
+
if (args.roteiro) {
|
|
187
|
+
const r = await prepararRoteiro(p, args);
|
|
188
|
+
if (!r.ok) return r;
|
|
189
|
+
const conferida = plano.conferir(r.plano, { pasta: p.pasta });
|
|
190
|
+
if (!conferida.ok) {
|
|
191
|
+
return { ok: false, error: 'o roteiro virou um plano com problemas (defeito do PrimoCode, não seu).', problemas: conferida.problemas.slice(0, 12) };
|
|
192
|
+
}
|
|
193
|
+
projeto.salvarPlano(args.id, r.plano, args.motivo || 'montado do roteiro');
|
|
194
|
+
projeto.salvar(args.id, { duracao: conferida.duracao });
|
|
195
|
+
return {
|
|
196
|
+
ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
|
|
197
|
+
proximo: 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
181
201
|
// Um plano inteiro, ou só as cenas (que é o caso comum).
|
|
182
202
|
let novo = args.plano || { formato: args.formato, cenas: args.cenas, audio: args.audio };
|
|
183
203
|
|
|
@@ -500,6 +520,56 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
500
520
|
return { ok: false, error: `não conheço a ação "${acao}".` };
|
|
501
521
|
}
|
|
502
522
|
|
|
523
|
+
const PROPORCOES = { '16:9': [1920, 1080], '9:16': [1080, 1920], '1:1': [1080, 1080], '4:5': [1080, 1350] };
|
|
524
|
+
|
|
525
|
+
/** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
|
|
526
|
+
async function prepararRoteiro(p, args) {
|
|
527
|
+
const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
|
|
528
|
+
if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
|
|
529
|
+
const partes = roteiro.partes || [];
|
|
530
|
+
const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
|
|
531
|
+
const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'slides' ? PROPORCOES['16:9'] : PROPORCOES['16:9']);
|
|
532
|
+
|
|
533
|
+
const iconesOk = new Set();
|
|
534
|
+
for (const nome of composicao.iconesDoRoteiro(roteiro)) {
|
|
535
|
+
try { const r = await assets.icone(nome); if (r && r.ok) iconesOk.add(nome); } catch { /* sem */ }
|
|
536
|
+
}
|
|
537
|
+
const imagens = {};
|
|
538
|
+
for (const [i, parte] of partes.entries()) {
|
|
539
|
+
if (parte.tipo !== 'imagem' || parte.src || !parte.busca) continue;
|
|
540
|
+
try {
|
|
541
|
+
const b = await assets.procurarImagens(parte.busca, { quantas: 4 });
|
|
542
|
+
for (const item of (b.ok ? b.imagens : [])) {
|
|
543
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'cena-' + (i + 1) });
|
|
544
|
+
if (d.ok) { imagens[i] = d.relativo; break; }
|
|
545
|
+
}
|
|
546
|
+
} catch { /* a cena sai sem a foto, com o título */ }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let tempos = [], narracao = null, sons = {}, trilha = null;
|
|
550
|
+
if (modo === 'video') {
|
|
551
|
+
const lista = partes.map((pt, i) => ({ texto: composicao.falaDe(pt), registro: composicao.registroDe(pt, i, partes.length) }));
|
|
552
|
+
const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
|
|
553
|
+
fs.mkdirSync(path.dirname(arq), { recursive: true });
|
|
554
|
+
fs.writeFileSync(arq, JSON.stringify(lista));
|
|
555
|
+
const mp3 = path.join(p.pasta, 'midia', `narracao-${Date.now().toString(36)}.mp3`);
|
|
556
|
+
const v = await rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz', String(args.voz || roteiro.voz || 'thalita')], { timeout: 10 * 60000 });
|
|
557
|
+
if (!v.ok) return v;
|
|
558
|
+
tempos = v.partes;
|
|
559
|
+
narracao = path.relative(p.pasta, mp3).replace(/\\/g, '/');
|
|
560
|
+
projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${partes.length} partes, voz ${v.voz}`);
|
|
561
|
+
sons = await edicao.prepararSons(p.pasta, video);
|
|
562
|
+
try {
|
|
563
|
+
const b = await assets.procurarSom(roteiro.musica || args.trilha || 'inspiring corporate background', { quantas: 6, duracaoMin: 45 });
|
|
564
|
+
for (const item of (b.ok ? b.sons : [])) {
|
|
565
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
|
|
566
|
+
if (d.ok) { trilha = d.relativo; break; }
|
|
567
|
+
}
|
|
568
|
+
} catch { /* sem música */ }
|
|
569
|
+
}
|
|
570
|
+
return composicao.comporRoteiro({ roteiro, modo, largura, altura, tempos, narracao, sons, trilha, imagens, iconesOk });
|
|
571
|
+
}
|
|
572
|
+
|
|
503
573
|
/* Sem o tempo de cada palavra, cada trecho é repartido pelas palavras dele,
|
|
504
574
|
proporcional ao tamanho de cada uma. */
|
|
505
575
|
function repartirTrechos(trechos) {
|
package/lib/estudio/plano.js
CHANGED
|
@@ -165,7 +165,10 @@ function conferirElemento(caminho, el, pasta, problemas, duracao) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
conferirCor(`${caminho}.cor`, el.cor, problemas);
|
|
168
|
-
|
|
168
|
+
// O fundo do elemento é cor, ou o mesmo objeto do fundo da cena (um
|
|
169
|
+
// gradiente sobre a foto, por exemplo).
|
|
170
|
+
if (el.fundo && typeof el.fundo === 'object') conferirFundo(`${caminho}.fundo`, el.fundo, pasta, problemas);
|
|
171
|
+
else conferirCor(`${caminho}.fundo`, el.fundo, problemas);
|
|
169
172
|
conferirAnima(`${caminho}.anima`, el.anima, problemas);
|
|
170
173
|
|
|
171
174
|
// `de` e `ate` recortam o elemento no tempo DA CENA. Um elemento que
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -234,7 +234,39 @@ function Elemento({ el, t, dados }) {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
switch (el.tipo) {
|
|
237
|
-
case 'texto':
|
|
237
|
+
case 'texto': {
|
|
238
|
+
/* O NÚMERO QUE CONTA: "contar": {ate: 9, dur: 1.2} faz o texto subir
|
|
239
|
+
de 0 até o valor, com a curva de saída. Casas decimais e o que vem
|
|
240
|
+
antes/depois do número (R$, %, mil) ficam como estão. */
|
|
241
|
+
let conteudo = el.texto;
|
|
242
|
+
if (el.contar) {
|
|
243
|
+
const alvo = Number(el.contar.ate) || 0;
|
|
244
|
+
const dur = el.contar.dur || 1.2;
|
|
245
|
+
const k = CURVAS.saida(Math.min(1, Math.max(0, tLocal / dur)));
|
|
246
|
+
const casas = el.contar.casas || 0;
|
|
247
|
+
const v = (alvo * k).toLocaleString('pt-BR', { minimumFractionDigits: casas, maximumFractionDigits: casas });
|
|
248
|
+
conteudo = (el.contar.antes || '') + v + (el.contar.depois || '');
|
|
249
|
+
}
|
|
250
|
+
/* O TEXTO QUE SE REVELA: "revelar": [t0, t1, …] é o instante (no tempo
|
|
251
|
+
do elemento) em que cada palavra entra — o da narração. A palavra
|
|
252
|
+
sobe e aparece; as que ainda não foram ditas esperam invisíveis, e o
|
|
253
|
+
bloco não muda de tamanho quando elas entram. */
|
|
254
|
+
if (Array.isArray(el.revelar) && typeof el.texto === 'string') {
|
|
255
|
+
const palavras = el.texto.split(' ');
|
|
256
|
+
conteudo = palavras.map((w, i) => {
|
|
257
|
+
const t0 = el.revelar[Math.min(i, el.revelar.length - 1)] ?? 0;
|
|
258
|
+
const k = Math.min(1, Math.max(0, (tLocal - t0) / 0.28));
|
|
259
|
+
const e = CURVAS.saida(k);
|
|
260
|
+
// O espaço fica FORA do span: é ali que a linha pode quebrar.
|
|
261
|
+
return (
|
|
262
|
+
<React.Fragment key={i}>
|
|
263
|
+
<span style={{ display: 'inline-block', opacity: e, transform: 'translateY(' + (1 - e) * 0.35 + 'em)',
|
|
264
|
+
color: el.destaques && el.destaques.includes(i) ? (el.corDestaque || el.cor) : undefined }}>{w}</span>
|
|
265
|
+
{i < palavras.length - 1 ? ' ' : null}
|
|
266
|
+
</React.Fragment>
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
238
270
|
return (
|
|
239
271
|
<div style={{
|
|
240
272
|
...estilo,
|
|
@@ -250,8 +282,13 @@ function Elemento({ el, t, dados }) {
|
|
|
250
282
|
background: el.gradienteTexto || undefined,
|
|
251
283
|
WebkitBackgroundClip: el.gradienteTexto ? 'text' : undefined,
|
|
252
284
|
WebkitTextFillColor: el.gradienteTexto ? 'transparent' : undefined,
|
|
253
|
-
|
|
285
|
+
fontStyle: el.italico ? 'italic' : undefined,
|
|
286
|
+
textDecoration: el.riscado ? 'line-through' : undefined,
|
|
287
|
+
textDecorationThickness: el.riscado ? '0.08em' : undefined,
|
|
288
|
+
wordBreak: 'normal', overflowWrap: 'break-word',
|
|
289
|
+
}}>{conteudo}</div>
|
|
254
290
|
);
|
|
291
|
+
}
|
|
255
292
|
|
|
256
293
|
case 'forma': {
|
|
257
294
|
const forma = el.forma || 'retangulo';
|
|
@@ -260,7 +297,7 @@ function Elemento({ el, t, dados }) {
|
|
|
260
297
|
...estilo,
|
|
261
298
|
width: medida(el.largura, '200px'),
|
|
262
299
|
height: forma === 'linha' ? medida(el.espessura, '4px') : medida(el.altura, '200px'),
|
|
263
|
-
background: el.fundo || el.cor || '#fff',
|
|
300
|
+
...(el.fundo && typeof el.fundo === 'object' ? fundoCSS(el.fundo) : { background: el.fundo || el.cor || '#fff' }),
|
|
264
301
|
borderRadius: forma === 'circulo' ? '50%' : medida(a.raio ?? el.raio, '0px'),
|
|
265
302
|
border: el.contorno || undefined,
|
|
266
303
|
}} />
|
|
@@ -375,10 +412,11 @@ function Elemento({ el, t, dados }) {
|
|
|
375
412
|
}
|
|
376
413
|
}
|
|
377
414
|
|
|
378
|
-
export function Cena({ cena, dados }) {
|
|
415
|
+
export function Cena({ cena, dados, tFixo }) {
|
|
379
416
|
const frame = useCurrentFrame();
|
|
380
417
|
const { fps } = useVideoConfig();
|
|
381
|
-
|
|
418
|
+
// A página parada é a cena num instante só: tFixo manda no tempo.
|
|
419
|
+
const t = tFixo != null ? tFixo : frame / fps;
|
|
382
420
|
|
|
383
421
|
const cam = animado(cena.camera, t);
|
|
384
422
|
const camTransform = [
|
|
@@ -439,6 +477,21 @@ function Transicao({ tipo, progresso, children }) {
|
|
|
439
477
|
return <AbsoluteFill style={estilo}>{children}</AbsoluteFill>;
|
|
440
478
|
}
|
|
441
479
|
|
|
480
|
+
/* AS PÁGINAS: um quadro por cena, cada uma congelada no meio dela. Os
|
|
481
|
+
* slides saem num render só — antes era um "remotion still" por página, e
|
|
482
|
+
* cada um empacotava o projeto inteiro de novo (onze páginas, onze
|
|
483
|
+
* empacotamentos: é a demora de 25 minutos medida em 25/09). */
|
|
484
|
+
export const Paginas = () => {
|
|
485
|
+
const { fps } = useVideoConfig();
|
|
486
|
+
const frame = useCurrentFrame();
|
|
487
|
+
const cena = plano.cenas[Math.min(frame, plano.cenas.length - 1)];
|
|
488
|
+
return (
|
|
489
|
+
<AbsoluteFill style={{ backgroundColor: plano.fundoGeral || '#000' }}>
|
|
490
|
+
<Cena cena={cena} dados={{ fps, icones }} tFixo={cena.duracao / 2} />
|
|
491
|
+
</AbsoluteFill>
|
|
492
|
+
);
|
|
493
|
+
};
|
|
494
|
+
|
|
442
495
|
export const Video = () => {
|
|
443
496
|
const { fps } = useVideoConfig();
|
|
444
497
|
const frame = useCurrentFrame();
|
|
@@ -495,7 +548,7 @@ function rootJsx(plano) {
|
|
|
495
548
|
const f = plano.formato || {};
|
|
496
549
|
return `import React from 'react';
|
|
497
550
|
import { Composition } from 'remotion';
|
|
498
|
-
import { Video } from './Video';
|
|
551
|
+
import { Video, Paginas } from './Video';
|
|
499
552
|
import plano from './plano.json';
|
|
500
553
|
import './fontes.css';
|
|
501
554
|
|
|
@@ -506,14 +559,24 @@ const quadros = Math.max(1, Math.round(
|
|
|
506
559
|
));
|
|
507
560
|
|
|
508
561
|
export const RemotionRoot = () => (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
562
|
+
<>
|
|
563
|
+
<Composition
|
|
564
|
+
id="Video"
|
|
565
|
+
component={Video}
|
|
566
|
+
durationInFrames={quadros}
|
|
567
|
+
fps={fps}
|
|
568
|
+
width={${Number(f.largura) || 1920}}
|
|
569
|
+
height={${Number(f.altura) || 1080}}
|
|
570
|
+
/>
|
|
571
|
+
<Composition
|
|
572
|
+
id="Paginas"
|
|
573
|
+
component={Paginas}
|
|
574
|
+
durationInFrames={plano.cenas.length}
|
|
575
|
+
fps={fps}
|
|
576
|
+
width={${Number(f.largura) || 1920}}
|
|
577
|
+
height={${Number(f.altura) || 1080}}
|
|
578
|
+
/>
|
|
579
|
+
</>
|
|
517
580
|
);
|
|
518
581
|
`;
|
|
519
582
|
}
|
package/lib/estudio/slides.js
CHANGED
|
@@ -48,41 +48,45 @@ const { spawn } = require('child_process');
|
|
|
48
48
|
function renderizarPaginas({ raiz, destino, plano, aoFalar = () => {} }) {
|
|
49
49
|
return new Promise((resolve) => {
|
|
50
50
|
fs.mkdirSync(destino, { recursive: true });
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
for (const f of fs.readdirSync(destino)) if (/\.(jpe?g|png)$/i.test(f)) fs.rmSync(path.join(destino, f));
|
|
52
|
+
const n = (plano.cenas || []).length;
|
|
53
|
+
const shell = process.platform === 'win32';
|
|
54
|
+
const cita = (a) => (shell && /\s/.test(a) ? `"${a}"` : a);
|
|
55
|
+
|
|
56
|
+
/* Projeto novo não tem o Remotion instalado: o caminho de slides
|
|
57
|
+
rodava o npx direto e morria em "could not determine executable". */
|
|
58
|
+
const passos = fs.existsSync(path.join(raiz, 'node_modules')) ? [] : [['npm', ['install', '--no-audit', '--no-fund']]];
|
|
59
|
+
// Todas as páginas num render só: a composição "Paginas" tem um quadro por cena.
|
|
60
|
+
/* O Remotion recusa pasta de sequência com ponto no caminho ("cannot
|
|
61
|
+
have an extension") — e ~/.primocode tem. A sequência sai numa pasta
|
|
62
|
+
temporária sem ponto e as páginas são movidas depois. */
|
|
63
|
+
const tmp = fs.mkdtempSync(path.join(require('os').tmpdir(), 'primo-paginas-'));
|
|
64
|
+
passos.push(['npx', ['remotion', 'render', 'Paginas', tmp, '--sequence',
|
|
65
|
+
'--image-format=jpeg', '--jpeg-quality=95', '--log=warn']]);
|
|
63
66
|
|
|
64
|
-
const paginas = [];
|
|
65
67
|
const correr = (i) => {
|
|
66
|
-
if (i >=
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
if (i >= passos.length) {
|
|
69
|
+
// Pelo NÚMERO: em ordem alfabética, element-10 vem antes de element-2.
|
|
70
|
+
const num = (f) => Number((f.match(/(\d+)\.jpe?g$/i) || [])[1] || 0);
|
|
71
|
+
const saidas = fs.readdirSync(tmp).filter((f) => /\.jpe?g$/i.test(f)).sort((a, b) => num(a) - num(b));
|
|
72
|
+
const paginas = saidas.map((f, k) => {
|
|
73
|
+
const final = path.join(destino, `pagina-${String(k + 1).padStart(3, '0')}.jpeg`);
|
|
74
|
+
fs.copyFileSync(path.join(tmp, f), final);
|
|
75
|
+
return final;
|
|
76
|
+
});
|
|
77
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* fica no temporário */ }
|
|
78
|
+
if (paginas.length !== n) return resolve({ ok: false, error: `saíram ${paginas.length} páginas de ${n}.` });
|
|
79
|
+
return resolve({ ok: true, paginas });
|
|
80
|
+
}
|
|
81
|
+
const [cmd, args] = passos[i];
|
|
82
|
+
aoFalar(cmd === 'npm' ? 'instalando o Remotion nesta pasta (uma vez só)' : `desenhando ${n} páginas`);
|
|
83
|
+
const p = spawn(cmd, args.map(cita), { cwd: raiz, shell });
|
|
77
84
|
let erro = '';
|
|
78
|
-
p.stderr.on('data', (d) => { erro += d.toString().slice(0,
|
|
79
|
-
const relogio = setTimeout(() => { try { p.kill(); } catch {} },
|
|
85
|
+
p.stderr.on('data', (d) => { erro += d.toString().slice(0, 2000); });
|
|
86
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} }, 15 * 60000);
|
|
80
87
|
p.on('close', (code) => {
|
|
81
88
|
clearTimeout(relogio);
|
|
82
|
-
if (code !== 0) {
|
|
83
|
-
return resolve({ ok: false, error: `a página ${i + 1} falhou: ${erro.slice(-400).trim()}` });
|
|
84
|
-
}
|
|
85
|
-
paginas.push(arquivo);
|
|
89
|
+
if (code !== 0) return resolve({ ok: false, error: `${cmd} saiu com ${code}: ${erro.slice(-400).trim()}` });
|
|
86
90
|
correr(i + 1);
|
|
87
91
|
});
|
|
88
92
|
p.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, error: e.message }); });
|
package/lib/local/cerebro.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"origem": "primocode-server 8.
|
|
2
|
+
"origem": "primocode-server 8.15.0 52f6257",
|
|
3
3
|
"SYSTEM_NUCLEO": "Você é o PrimoCode, agente de engenharia da ConectaPrimoAI.\n\nCONVERSA OU TRABALHO: cumprimento, agradecimento, pergunta sobre você ou papo\n(\"oi\", \"o que você faz?\") → responda em texto, 1 ou 2 frases, sem tool nenhuma,\ne não invente tarefa que ninguém pediu. Pedido de criar, alterar, ler, rodar ou\nconsertar → EXECUTE, até o fim, sem conversar no meio.\n\nREGRAS ABSOLUTAS (executando):\n1. NUNCA escreva código na resposta. Código vai SEMPRE dentro de uma tool.\n2. NUNCA pergunte ao usuário. Decida e execute.\n3. NUNCA invente ferramenta, serviço, porta ou API: as suas são EXATAMENTE as\n desta lista. E nunca diga que algo \"não está rodando\" para se esquivar — o\n que precisa subir, sobe sozinho quando você chama a tool. Prometer o que\n você não tem, ou culpar um serviço, é o pior erro possível.\n4. Caminhos RELATIVOS (\"index.html\", \"src/game.js\"). Você já está na pasta do\n projeto: nunca caminho absoluto, nunca \"../\".\n5. Termine SEMPRE com finish(\"frase curta do que foi feito\").\n6. Resposta em texto: no máximo 1 frase, sem markdown.\n7. NUNCA repita uma chamada que já fez. Reformular a mesma busca traz as mesmas\n fontes; spec e manual são FIXOS — leu uma vez, está lido. Pesquisou e leu a\n spec? O passo seguinte é CRIAR com o que já tem.\n\nESCOLHENDO A TOOL: arquivo novo → write_file; mudar parte → edit_file (nunca\nreescreva o arquivo inteiro para trocar poucas linhas); várias mudanças no mesmo\narquivo → multi_edit, numa chamada; não sabe onde está → grep (conteúdo) ou glob\n(nome); arquivo grande → read_file com offset/limit; servidor que fica de pé →\nrun_in_new_terminal.\n\nANTES DE EDITAR, LEIA — SEM EXCEÇÃO: edit_file exige o texto LITERAL do arquivo,\ne você não adivinha indentação. read_file (ou grep) PRIMEIRO, edit_file depois.\n\nQUANDO FALHA: leia o erro e conserte. Se edit_file disser \"trecho não\nencontrado\", ele devolve \"trechosParecidos\" com as linhas reais — copie de lá.\nNUNCA chame finish dizendo que fez algo que falhou: \"não consegui alterar X\nporque Y\" é melhor que um \"pronto!\" falso.\n\nPLANO: tarefa de 3+ passos começa com write_todos e vai atualizando o status (só\nUMA \"in_progress\" por vez). Tarefa curta não precisa.\n\nCOMO VOCÊ FALA ENQUANTO TRABALHA: antes de uma tool que muda o rumo (conserto,\nconversão, desvio), UMA frase em primeira pessoa com o quê e o porquê (\"o vídeo\nveio em .avi, vou converter para mp4\"); resolvido, \"feito, continuando\". Passo\nóbvio não precisa. NUNCA cite limite, cota, token, modelo, provedor, tentativa\nou erro interno: quem lê vê o trabalho, não o encanamento. Contornou uma\nfalha? Conte o contorno.\n\nMEMÓRIA: você recebe memory-geral.md (o usuário) e o memory.md do projeto.\nRespeite as preferências e decisões que estão lá, e não recomece do zero um\nprojeto que já tem arquivos — leia antes de escrever. O memory.md do projeto se\ngrava sozinho. Já o memory-geral.md só guarda o que você mandar com\nremember_general: nome, preferência que vale em qualquer projeto, decisão de\nlongo prazo. Sem essa chamada, nada disso sobrevive para a próxima vez.\n\nPROCESSO: ver o que existe (list_dir/glob) → entender (read_file/grep) → fazer\n(write_file/edit_file) → conferir → finish.",
|
|
4
4
|
"SECOES": {
|
|
5
5
|
"pesquisa": "PEDIDO QUE DEPENDE DE FATO DO MUNDO — PESQUISE PRIMEIRO:\nSe o que o usuário pediu envolve algo real (uma empresa, um produto, uma\npessoa, um mercado, um número, um acontecimento), chame pesquisar_referencia\nANTES de criar qualquer coisa, em qualquer esforço. \"Pesquise sobre a empresa X\ne faça slides\" é UM pedido só: pesquisa e depois cria, sem parar para perguntar\ne sem entregar o deck com dado inventado.\n\nUm deck bonito cheio de número inventado é pior que nenhum deck — o usuário\nleva aquilo para uma reunião. Se depois da pesquisa faltar um dado, escreva na\npeça o que você sabe e deixe o buraco explícito (\"faturamento: não divulgado\"),\nnunca um número plausível de sua cabeça.",
|
|
6
|
-
"estudio": "O ESTÚDIO — VÍDEO, MOTION, SLIDES E ARTE, FEITOS AQUI:\nCada peça é uma PASTA no computador do usuário, e o arquivo final sai pelo\nRemotion (.mp4 no vídeo, .pdf em slides e arte). Nada de link para abrir num\nsite: no fim, o arquivo abre sozinho para o usuário.\n\nFLUXO
|
|
6
|
+
"estudio": "O ESTÚDIO — VÍDEO, MOTION, SLIDES E ARTE, FEITOS AQUI:\nCada peça é uma PASTA no computador do usuário, e o arquivo final sai pelo\nRemotion (.mp4 no vídeo, .pdf em slides e arte). Nada de link para abrir num\nsite: no fim, o arquivo abre sozinho para o usuário.\n\nFLUXO (vídeo, motion, slides ou arte feitos do zero):\n1. estudio_projeto acao:\"criar\" (titulo, pedido, formato: video | slides | arte).\n2. Empresa ou marca de verdade → estudio_site ANTES, e use as cores, a fonte\n e a logo que ele devolve em roteiro.marca.\n3. estudio_plano com ROTEIRO: 6 a 12 partes, cada uma com o tipo certo\n (abertura, numero, lista, passos, contraste, citacao, imagem, grafico,\n titulo, cta) e a \"fala\" — a frase dita, que não repete a tela. O PrimoCode\n faz o layout, a voz com entonação, os ícones, a música e os sons. NÃO\n escreva cenas nem posições à mão.\n4. estudio_render. Para ajustar, mande o roteiro de novo com a mudança.\n\nGRAVAÇÃO DO USUÁRIO (\"edita meu vídeo\", um .mp4/.mov dele): estudio_projeto\ncriar → estudio_video acao:\"importar\" → estudio_video acao:\"editar\" (faz a\nedição inteira: cortes, legenda animada, ícones, zoom, sons) → estudio_render.\nNão monte as cenas à mão antes de tentar o \"editar\". Depois, ajuste só a cena\nque o usuário pedir (estudio_plano cena:N). NUNCA escreva .mp4/.mp3 com\nwrite_file e nunca monte vídeo com ffmpeg na mão.\n\nO ROTEIRO BOM: a primeira parte prende (abertura ou contraste), o miolo prova\n(numero, grafico, lista, imagem) e a última pede ação (cta). Varie os tipos —\ntrês \"titulo\" seguidos parecem slide parado. Título que diz algo (\"Receita\ntriplicou em 18 meses\"), não rótulo (\"Resultados\"). Número de verdade ou\nnenhum. A fala não repete a tela: a tela dá o título, a voz dá o argumento.\n\nSE O USUÁRIO CITAR UM APP (Canva, PowerPoint, CapCut, Premiere…), comece com:\n\"Posso abrir o(a) [app], mas para te entregar com mais qualidade vou usar\nminhas ferramentas.\" — avisa, não pergunta. Se ele insistir, desktop_open_app.\n\nDocumento, planilha e PDF não são do Estúdio: faça o arquivo (.docx, .xlsx,\n.pdf) com código, na pasta do projeto.",
|
|
7
7
|
"navegador": "CONTROLANDO NAVEGADOR E DESKTOP:\nVocê pode AGIR numa página real e no computador inteiro do usuário — qualquer\nprograma, e a própria área de trabalho:\n- Navegador: browser_open abre a página num Chrome controlado; depois\n browser_click (por texto/seletor), browser_type, browser_key, browser_read\n (conferir), browser_screenshot. Use para testar o que você criou, preencher\n formulários, navegar em sites.\n- No DESKTOP você enxerga por NOMES, não por pixels (você não vê imagens):\n desktop_windows lista as janelas abertas; desktop_focus traz uma para a\n frente; desktop_elements lê os botões, campos, menus e ícones dela (nome,\n tipo e posição); desktop_click_element clica pelo NOME.\n- FLUXO PADRÃO no desktop: desktop_open_app (ou desktop_focus se já está\n aberto) → desktop_elements → desktop_click_element → desktop_type /\n desktop_key. Depois de agir, confira: desktop_elements de novo (o estado\n mudou?) ou desktop_windows.\n- desktop_open_app abre qualquer programa pelo nome (\"chrome\", \"bloco de\n notas\", \"calculadora\"). A janela leva 1-2s: confirme com desktop_windows.\n- Área de trabalho (Windows): desktop_elements com window \"Program Manager\"\n lista os ícones; ícone abre com desktop_click_element {double: true}.\n- Atalhos: desktop_key aceita combinações — \"ctrl+c\", \"alt+tab\",\n \"ctrl+shift+t\", \"Enter\", \"win\" (menu Iniciar).\n- desktop_click/desktop_double_click por coordenada e desktop_screenshot são o\n último recurso, quando a janela não expõe elementos nomeados.\nPrefira sempre NOMES (texto/seletor no navegador, elemento no desktop) a\ncoordenadas fixas. Confira o resultado antes de dizer que terminou.\n\nPESQUISANDO ANTES DE ESCREVER (qualidade):\nVocê não sabe tudo de cor, e chutar assinatura de API é o erro que mais custa\ncaro. pesquisar_referencia abre documentação oficial, exemplos e respostas\nsobre o assunto e devolve o TEXTO delas para você ler. Use quando:\n- for usar biblioteca/framework/API que você não domina com certeza;\n- a mesma tool falhar duas vezes seguidas (pare de tentar, vá ler);\n- o usuário pedir qualidade, ou a tarefa for grande.\nLeia, entenda o padrão e ESCREVA o seu código. Não cole trecho literal de\nterceiros no projeto do usuário; e confira se a API que a fonte cita existe na\nversão que o projeto usa (veja o package.json).",
|
|
8
8
|
"delegar": "DELEGANDO A ESPECIALISTAS:\nSe a tarefa tem partes independentes, use spawn_agent para cada uma e diga\nQUEM faz em `especialista`: frontend (tela, HTML, CSS) · backend (API, dados\nno servidor, autenticação) · testes (escrever e rodar testes, reproduzir bug)\n· design (identidade visual, paleta, tipografia, ícones) · dados (CSV,\nplanilha, análise) · revisor (lê e aponta defeitos, não conserta) · ambiente\n(instalar dependências, configurar, fazer rodar). Cada um chega com o\nbriefing da profissão. Trabalho grande de código: um por camada (frontend +\nbackend + testes) e um revisor no fim, com o que ele apontou voltando para\nquem escreveu. O subagente trabalha sozinho e devolve só o resultado; escreva\no prompt dele completo — ele NÃO vê esta conversa. Tarefa curta você mesmo\nresolve.",
|
|
9
9
|
"computador": "CONTROLANDO NAVEGADOR E DESKTOP:\nVocê pode AGIR numa página real e no computador inteiro do usuário — qualquer\nprograma, e a própria área de trabalho:\n- Navegador: browser_open abre a página num Chrome controlado; depois\n browser_click (por texto/seletor), browser_type, browser_key, browser_read\n (conferir), browser_screenshot. Use para testar o que você criou, preencher\n formulários, navegar em sites.\n- No DESKTOP você enxerga por NOMES, não por pixels (você não vê imagens):\n desktop_windows lista as janelas abertas; desktop_focus traz uma para a\n frente; desktop_elements lê os botões, campos, menus e ícones dela (nome,\n tipo e posição); desktop_click_element clica pelo NOME.\n- FLUXO PADRÃO no desktop: desktop_open_app (ou desktop_focus se já está\n aberto) → desktop_elements → desktop_click_element → desktop_type /\n desktop_key. Depois de agir, confira: desktop_elements de novo (o estado\n mudou?) ou desktop_windows.\n- desktop_open_app abre qualquer programa pelo nome (\"chrome\", \"bloco de\n notas\", \"calculadora\"). A janela leva 1-2s: confirme com desktop_windows.\n- Área de trabalho (Windows): desktop_elements com window \"Program Manager\"\n lista os ícones; ícone abre com desktop_click_element {double: true}.\n- Atalhos: desktop_key aceita combinações — \"ctrl+c\", \"alt+tab\",\n \"ctrl+shift+t\", \"Enter\", \"win\" (menu Iniciar).\n- desktop_click/desktop_double_click por coordenada e desktop_screenshot são o\n último recurso, quando a janela não expõe elementos nomeados.\nPrefira sempre NOMES (texto/seletor no navegador, elemento no desktop) a\ncoordenadas fixas. Confira o resultado antes de dizer que terminou.\n\nPESQUISANDO ANTES DE ESCREVER (qualidade):\nVocê não sabe tudo de cor, e chutar assinatura de API é o erro que mais custa\ncaro. pesquisar_referencia abre documentação oficial, exemplos e respostas\nsobre o assunto e devolve o TEXTO delas para você ler. Use quando:\n- for usar biblioteca/framework/API que você não domina com certeza;\n- a mesma tool falhar duas vezes seguidas (pare de tentar, vá ler);\n- o usuário pedir qualidade, ou a tarefa for grande.\nLeia, entenda o padrão e ESCREVA o seu código. Não cole trecho literal de\nterceiros no projeto do usuário; e confira se a API que a fonte cita existe na\nversão que o projeto usa (veja o package.json).",
|
|
@@ -543,13 +543,25 @@
|
|
|
543
543
|
"type": "function",
|
|
544
544
|
"function": {
|
|
545
545
|
"name": "estudio_plano",
|
|
546
|
-
"description": "
|
|
546
|
+
"description": "PREFIRA \"roteiro\": voce diz o que cada parte E e o que ela diz, e o PrimoCode faz o layout, a voz com entonacao, os icones, a musica e os sons — video ou slides. roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, partes:[{tipo, fala, ...}]}. Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens[]} passos{titulo,itens[]} contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras[{rotulo,valor}]} cta{titulo,link}. So use plano/cenas para ajuste fino. Sem nada, LE o plano. Elemento: {tipo: texto|forma|imagem|video|icone|legenda|componente, x, y, cor, tamanho, anima: {prop: [{t,v,e}]}}. Cena: camera {zoom,x,y,rotacao}. Nao ha layout pronto nem tema.",
|
|
547
547
|
"parameters": {
|
|
548
548
|
"type": "object",
|
|
549
549
|
"properties": {
|
|
550
550
|
"id": {
|
|
551
551
|
"type": "string"
|
|
552
552
|
},
|
|
553
|
+
"roteiro": {
|
|
554
|
+
"type": "object",
|
|
555
|
+
"description": "{marca, proporcao, partes:[{tipo, fala, ...campos do tipo}]}"
|
|
556
|
+
},
|
|
557
|
+
"voz": {
|
|
558
|
+
"type": "string",
|
|
559
|
+
"enum": [
|
|
560
|
+
"thalita",
|
|
561
|
+
"antonio",
|
|
562
|
+
"francisca"
|
|
563
|
+
]
|
|
564
|
+
},
|
|
553
565
|
"plano": {
|
|
554
566
|
"type": "object",
|
|
555
567
|
"description": "{formato:{largura,altura,fps}, cenas, audio}"
|
|
@@ -1715,13 +1727,25 @@
|
|
|
1715
1727
|
"type": "function",
|
|
1716
1728
|
"function": {
|
|
1717
1729
|
"name": "estudio_plano",
|
|
1718
|
-
"description": "
|
|
1730
|
+
"description": "PREFIRA \"roteiro\": voce diz o que cada parte E e o que ela diz, e o PrimoCode faz o layout, a voz com entonacao, os icones, a musica e os sons — video ou slides. roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, partes:[{tipo, fala, ...}]}. Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens[]} passos{titulo,itens[]} contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras[{rotulo,valor}]} cta{titulo,link}. So use plano/cenas para ajuste fino. Sem nada, LE o plano. Elemento: {tipo: texto|forma|imagem|video|icone|legenda|componente, x, y, cor, tamanho, anima: {prop: [{t,v,e}]}}. Cena: camera {zoom,x,y,rotacao}. Nao ha layout pronto nem tema.",
|
|
1719
1731
|
"parameters": {
|
|
1720
1732
|
"type": "object",
|
|
1721
1733
|
"properties": {
|
|
1722
1734
|
"id": {
|
|
1723
1735
|
"type": "string"
|
|
1724
1736
|
},
|
|
1737
|
+
"roteiro": {
|
|
1738
|
+
"type": "object",
|
|
1739
|
+
"description": "{marca, proporcao, partes:[{tipo, fala, ...campos do tipo}]}"
|
|
1740
|
+
},
|
|
1741
|
+
"voz": {
|
|
1742
|
+
"type": "string",
|
|
1743
|
+
"enum": [
|
|
1744
|
+
"thalita",
|
|
1745
|
+
"antonio",
|
|
1746
|
+
"francisca"
|
|
1747
|
+
]
|
|
1748
|
+
},
|
|
1725
1749
|
"plano": {
|
|
1726
1750
|
"type": "object",
|
|
1727
1751
|
"description": "{formato:{largura,altura,fps}, cenas, audio}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.13",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
package/studio/narrar.py
CHANGED
|
@@ -330,7 +330,8 @@ def comando_roteiro(args):
|
|
|
330
330
|
roteiro_txt = Path(tmp) / "lista.txt"
|
|
331
331
|
roteiro_txt.write_text("".join(f"file '{w}'\n" for w in lista), encoding="utf-8")
|
|
332
332
|
r = subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
|
333
|
-
"-i", str(roteiro_txt), "-
|
|
333
|
+
"-i", str(roteiro_txt), "-af", "loudnorm=I=-14:TP=-1.5:LRA=9",
|
|
334
|
+
"-c:a", "libmp3lame", "-q:a", "3", str(args.saida)],
|
|
334
335
|
capture_output=True, timeout=180)
|
|
335
336
|
if r.returncode != 0:
|
|
336
337
|
return {"ok": False, "error": (r.stderr or b"").decode("utf-8", "ignore")[-300:]}
|