primocode 10.0.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. package/studio/voz_kokoro.py +98 -0
@@ -82,6 +82,8 @@ async function temPillow() {
82
82
 
83
83
  /** As camadas de texto da janela: {índiceDaFaixa: arquivo .ffconcat}. */
84
84
  async function camadasDeTexto(pasta, tl, { from, to, escala }) {
85
+ const avisosPos = [];
86
+ const posicao = require('./gerados.js').posicoesDeLegenda(pasta, tl, avisosPos);
85
87
  const faixas = tl.tracks.map((t, i) => ({ t, i })).filter(({ t }) => t.type === 'text' && t.clips.length && !t.hidden);
86
88
  if (!faixas.length) return { camadas: {}, avisos: [] };
87
89
  if (!(await temPillow())) {
@@ -96,7 +98,7 @@ async function camadasDeTexto(pasta, tl, { from, to, escala }) {
96
98
  const estados = [];
97
99
  const camadas = {};
98
100
  for (const { t, i } of faixas) {
99
- const { segmentos, desenhos } = TX.estadosDaFaixa(tl, t, { from, to, escala });
101
+ const { segmentos, desenhos } = TX.estadosDaFaixa(tl, t, { from, to, escala, posicao });
100
102
  if (!Object.keys(desenhos).length) continue;
101
103
  for (const [chave, items] of Object.entries(desenhos)) {
102
104
  if (!fs.existsSync(arquivoDe(chave)) && !estados.some((e) => e.chave === chave)) estados.push({ chave, file: arquivoDe(chave), items });
@@ -112,13 +114,13 @@ async function camadasDeTexto(pasta, tl, { from, to, escala }) {
112
114
  try { fs.unlinkSync(job); } catch {}
113
115
  if (!r.ok) return { camadas: {}, avisos: [`os textos não foram desenhados: ${r.error}`] };
114
116
  }
115
- return { camadas, avisos: [], estados: estados.length };
117
+ return { camadas, avisos: [...new Set(avisosPos)], estados: estados.length };
116
118
  }
117
119
 
118
120
  // ── rodar o ffmpeg ───────────────────────────────────────────────────────
119
121
 
120
- async function rodarGrafo(pasta, g, saida) {
121
- const arq = path.join(pasta, 'cache', `grafo-${process.pid}.txt`);
122
+ async function rodarGrafo(pasta, g, saida, sufixo = '') {
123
+ const arq = path.join(pasta, 'cache', `grafo-${process.pid}${sufixo}.txt`);
122
124
  fs.mkdirSync(path.dirname(arq), { recursive: true });
123
125
  fs.writeFileSync(arq, g.grafo);
124
126
  // O grafo vai por arquivo: com cinquenta clipes ele passa do teto de
@@ -130,29 +132,80 @@ async function rodarGrafo(pasta, g, saida) {
130
132
  return { ...r, grafo: arq };
131
133
  }
132
134
 
133
- async function exportar(pasta, tl, { out, from = 0, to, preview = false, crf = 20 } = {}) {
135
+ /* O EXPORT EM PARTES: o grafo do ffmpeg é quase todo de um núcleo só (cada
136
+ filtro roda numa thread), e um vídeo com pessoa recortada, fundo novo,
137
+ 3D e legenda passa a maior parte do tempo nele. Então o VÍDEO sai em
138
+ pedaços de quadros, cada um num ffmpeg, ao mesmo tempo, e junta por
139
+ concat sem recodificar; o ÁUDIO sai inteiro, numa passada só (é barato,
140
+ e cortar AAC em pedaços deixa estalo na emenda). As emendas caem em
141
+ quadro inteiro: o quadro N sai igual com 1 ou com 9 pedaços. */
142
+ async function exportar(pasta, tl, { out, from = 0, to, preview = false, crf = 20, partes } = {}) {
134
143
  const c = conferir();
135
144
  if (!c.ok) return c;
145
+ const ger = await require('./gerados.js').garantir(pasta, tl);
146
+ if (!ger.ok) return ger;
136
147
  const escala = preview ? 0.5 : 1;
137
148
  const ate = to === undefined ? T.duracao(tl) : Number(to);
138
- const tx = await camadasDeTexto(pasta, tl, { from, to: ate, escala });
139
- const g = F.montarGrafo(tl, { from, to: ate, camadasDeTexto: tx.camadas, escala });
140
149
  const destino = path.resolve(out || path.join(pasta, 'saida', `${tl.nome || 'video'}${preview ? '-preview' : ''}.mp4`));
141
150
  fs.mkdirSync(path.dirname(destino), { recursive: true });
142
151
  const inicio = Date.now();
143
- const r = await rodarGrafo(pasta, g, [
144
- '-map', g.saidaV, '-map', g.saidaA, '-t', F.num(g.segundos), '-r', String(tl.fps),
145
- ...argsDoCodec(codecDeVideo(), crf), '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', destino,
146
- ]);
147
- if (!r.ok) return { ok: false, error: 'o ffmpeg falhou', detalhe: r.erro.slice(-600), grafo: r.grafo };
148
- return { ok: true, file: destino, s: T.r2(g.segundos), size: `${g.W}x${g.H}`, mb: T.r2(fs.statSync(destino).size / 1048576),
149
- tookS: Math.round((Date.now() - inicio) / 100) / 10, ...(tx.avisos.length ? { avisos: tx.avisos } : {}) };
152
+ const PAR = require('./paralelo.js');
153
+ const total = ate - from;
154
+ const n = partes !== undefined ? Math.max(1, Number(partes)) : Math.max(1, Math.min(PAR.quantos(), Math.floor(total / (tl.fps * 2))));
155
+ const codec = argsDoCodec(codecDeVideo(), crf);
156
+ const extra = ger.feitos ? { gerados: ger.feitos, geradosS: ger.tookS } : {};
157
+ if (n === 1) {
158
+ const tx = await camadasDeTexto(pasta, tl, { from, to: ate, escala });
159
+ const g = F.montarGrafo(tl, { from, to: ate, camadasDeTexto: tx.camadas, escala });
160
+ const r = await rodarGrafo(pasta, g, [
161
+ '-map', g.saidaV, '-map', g.saidaA, '-t', F.num(g.segundos), '-r', String(tl.fps),
162
+ ...codec, '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', destino,
163
+ ]);
164
+ if (!r.ok) return { ok: false, error: 'o ffmpeg falhou', detalhe: r.erro.slice(-600), grafo: r.grafo };
165
+ return { ok: true, file: destino, s: T.r2(g.segundos), size: `${g.W}x${g.H}`, mb: T.r2(fs.statSync(destino).size / 1048576),
166
+ tookS: Math.round((Date.now() - inicio) / 100) / 10, partes: 1, ...extra, ...(tx.avisos.length ? { avisos: tx.avisos } : {}) };
167
+ }
168
+ const tmp = path.join(pasta, 'cache', `export-${process.pid}`);
169
+ fs.rmSync(tmp, { recursive: true, force: true });
170
+ fs.mkdirSync(tmp, { recursive: true });
171
+ const cortes = Array.from({ length: n + 1 }, (_, i) => from + Math.round((total * i) / n));
172
+ // Os PNGs do texto antes, em fila: dois pedaços desenhando o mesmo estado ao mesmo tempo se atropelariam.
173
+ const avisos = new Set();
174
+ const pedacos = [];
175
+ for (let i = 0; i < n; i++) {
176
+ const tx = await camadasDeTexto(pasta, tl, { from: cortes[i], to: cortes[i + 1], escala });
177
+ tx.avisos.forEach((a) => avisos.add(a));
178
+ pedacos.push({ de: cortes[i], ate: cortes[i + 1], tx, arq: path.join(tmp, `v${String(i).padStart(3, '0')}.mp4`) });
179
+ }
180
+ const res = await PAR.emParalelo([
181
+ ...pedacos.map((p, i) => () => {
182
+ const g = F.montarGrafo(tl, { from: p.de, to: p.ate, camadasDeTexto: p.tx.camadas, escala, audio: false });
183
+ return rodarGrafo(pasta, g, ['-map', g.saidaV, '-frames:v', String(p.ate - p.de), '-r', String(tl.fps), ...codec, '-pix_fmt', 'yuv420p', p.arq], `-v${i}`);
184
+ }),
185
+ () => {
186
+ const g = F.montarGrafo(tl, { from, to: ate, escala, video: false });
187
+ return rodarGrafo(pasta, g, ['-map', g.saidaA, '-t', F.num(g.segundos), '-c:a', 'aac', '-b:a', '192k', path.join(tmp, 'audio.m4a')], '-a');
188
+ },
189
+ ], n + 1);
190
+ const ruim = res.find((r) => !r.ok);
191
+ if (ruim) return { ok: false, error: 'o ffmpeg falhou', detalhe: ruim.erro.slice(-600), grafo: ruim.grafo };
192
+ const lista = path.join(tmp, 'lista.txt');
193
+ fs.writeFileSync(lista, pedacos.map((p) => `file '${p.arq.replace(/'/g, "'\\''")}'`).join('\n') + '\n');
194
+ const j = await video.rodar(video.ffmpeg(), ['-hide_banner', '-loglevel', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', lista,
195
+ '-i', path.join(tmp, 'audio.m4a'), '-map', '0:v', '-map', '1:a', '-c', 'copy', '-movflags', '+faststart', destino]);
196
+ if (!j.ok) return { ok: false, error: 'não juntei os pedaços do export', detalhe: j.erro.slice(-400) };
197
+ fs.rmSync(tmp, { recursive: true, force: true });
198
+ return { ok: true, file: destino, s: T.r2(total / tl.fps), size: `${T.par(tl.width * escala)}x${T.par(tl.height * escala)}`,
199
+ mb: T.r2(fs.statSync(destino).size / 1048576), tookS: Math.round((Date.now() - inicio) / 100) / 10, partes: n, ...extra,
200
+ ...(avisos.size ? { avisos: [...avisos] } : {}) };
150
201
  }
151
202
 
152
203
  /** Os quadros pedidos, com a grade 0–1 e o número do quadro, numa folha só. */
153
204
  async function inspecionar(pasta, tl, { frames, escala = 0.5, out } = {}) {
154
205
  const c = conferir();
155
206
  if (!c.ok) return c;
207
+ const ger = await require('./gerados.js').garantir(pasta, tl);
208
+ if (!ger.ok) return ger;
156
209
  const dur = T.duracao(tl);
157
210
  const lista = [...new Set(frames.map((f) => Math.max(0, Math.min(dur - 1, Math.round(Number(f))))))].slice(0, 12);
158
211
  const dir = path.join(pasta, 'cache', 'inspect');
@@ -36,7 +36,7 @@ function palavrasDo(c) {
36
36
  }
37
37
 
38
38
  /** O estado de UM clipe de texto no quadro local `lf` — ou null (não aparece). */
39
- function estado(tl, c, lf) {
39
+ function estado(tl, c, lf, pos = null) {
40
40
  const st = { ...ESTILO_PADRAO, ...(c.text.style || {}) };
41
41
  const d = c.durationFrames;
42
42
  const anim = c.text.animation || 'none';
@@ -85,7 +85,7 @@ function estado(tl, c, lf) {
85
85
  const W = tl.width;
86
86
  const H = tl.height;
87
87
  const item = {
88
- cx: r3(c.transform.cx * W), cy: r3(c.transform.cy * H), w: Math.round(c.transform.w * W),
88
+ cx: r3((pos ? pos.cx : c.transform.cx) * W), cy: r3((pos ? pos.cy : c.transform.cy) * H), w: Math.round(c.transform.w * W),
89
89
  size: Math.round(st.size * H), font: st.font, color: st.color, align: st.align,
90
90
  stroke: st.stroke, sw: Math.round(st.strokeWidth * st.size * H), bg: st.bg,
91
91
  scale: r3(scale), alpha: Math.round(alpha * 100) / 100, words,
@@ -97,8 +97,10 @@ function estado(tl, c, lf) {
97
97
  /**
98
98
  * Os estados de uma faixa na janela [from, to): segmentos consecutivos com o
99
99
  * mesmo desenho viram um só. Devolve {segmentos:[{dur, chave}], desenhos}.
100
+ * `posicao(clipe, quadro)` → {cx, cy} | null: a legenda que SEGUE alguém
101
+ * (a cabeça) troca o lugar do transform pelo que ela devolver, quadro a quadro.
100
102
  */
101
- function estadosDaFaixa(tl, faixa, { from = 0, to, escala = 1 } = {}) {
103
+ function estadosDaFaixa(tl, faixa, { from = 0, to, escala = 1, posicao = null } = {}) {
102
104
  const ate = to === undefined ? T.duracao(tl) : to;
103
105
  const clips = faixa.clips.filter((c) => !c.hidden && T.fim(c) > from && c.startFrame < ate);
104
106
  const escalar = (it) => (escala === 1 ? it : {
@@ -114,7 +116,7 @@ function estadosDaFaixa(tl, faixa, { from = 0, to, escala = 1 } = {}) {
114
116
  for (let j = k; j < clips.length && clips[j].startFrame <= f; j++) {
115
117
  const c = clips[j];
116
118
  if (f >= T.fim(c)) continue;
117
- const it = estado(tl, c, f - c.startFrame);
119
+ const it = estado(tl, c, f - c.startFrame, posicao ? posicao(c, f) : null);
118
120
  if (it) itens.push(escalar(it));
119
121
  }
120
122
  const json = JSON.stringify(itens);
@@ -267,6 +267,7 @@ function compacto(tl, c, track) {
267
267
  if (Object.keys(tf).length) o.xf = tf;
268
268
  if (Object.values(c.crop).some((v) => v)) o.crop = c.crop;
269
269
  if (c.fit !== 'cover') o.fit = c.fit;
270
+ if (c.papel) o.role = c.papel;
270
271
  const kf = Object.keys(c.keyframes || {}).filter((k) => c.keyframes[k].length);
271
272
  if (kf.length) o.kf = kf;
272
273
  if (c.effects.length) o.fx = c.effects.map((e) => e.type);
@@ -289,15 +290,22 @@ function vista(tl, { from = 0, to = Infinity } = {}) {
289
290
  });
290
291
  const f = { i, type: t.type, n: t.clips.length, clips };
291
292
  if (t.muted) f.muted = true;
293
+ if (t.role) f.role = t.role;
292
294
  return f;
293
295
  });
294
296
  const assets = {};
295
297
  for (const a of Object.values(tl.assets)) {
296
298
  assets[a.id] = { nome: a.nome, kind: a.kind, s: r2(a.seconds || 0) };
297
299
  if (a.width) assets[a.id].size = `${a.width}x${a.height}`;
298
- if (!a.hasAudio && a.kind === 'video') assets[a.id].mudo = true;
300
+ if (!a.hasAudio && a.kind === 'video' && !a.gerado) assets[a.id].mudo = true;
301
+ if (a.gerado) assets[a.id].gerado = a.gerado.tipo;
299
302
  }
300
303
  const out = { fps: tl.fps, size: `${tl.width}x${tl.height}`, dur: duracao(tl), durS: r2(segundos(tl, duracao(tl))), assets, tracks };
304
+ if (tl.fx) {
305
+ // As receitas dos efeitos, em segundos da ORIGEM (o que o rastro mediu).
306
+ out.fx = {};
307
+ for (const [k, v] of Object.entries(tl.fx)) out.fx[k] = { objetos: (v.objetos || []).map((o) => `${o.id}:${o.modelo}@${o.mao} ${o.de}-${o.ate}s`), ...(v.fusao ? { fusao: v.fusao } : {}) };
308
+ }
301
309
  if (tl.markers.length) out.markers = tl.markers.filter((m) => m.frame >= from && m.frame < to).slice(0, 60);
302
310
  return out;
303
311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.0.0",
3
+ "version": "10.1.0",
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/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/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.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/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.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 && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.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/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/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.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/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.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 && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/hibrido.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -25,4 +25,7 @@ superlativo sem número.
25
25
  Número concreto vence adjetivo: "responde em 2 segundos" vale mais que
26
26
  "ultrarrápido".
27
27
 
28
+ **Revisando um texto pronto**: aponte o trecho que soa errado e reescreva só
29
+ ele. O resto, que já funciona, fica como está.
30
+
28
31
  Não use emoji para dar emoção a um texto que não tem nenhuma.
@@ -12,7 +12,8 @@ a correção. Primeiro o que impede alguém de usar; acabamento vai no fim.
12
12
  - O que é clicável é `button` ou `a`, com hover, estado pressionado e foco
13
13
  visível (`:focus-visible`).
14
14
  - Alvo de toque de pelo menos 44×44px. Nada importante escondido só no hover.
15
- - Ação que demora mostra que está trabalhando e não pode ser enviada duas vezes.
15
+ - Todo clique responde em até 200 ms. Ação que demora mostra que está
16
+ trabalhando e não pode ser enviada duas vezes.
16
17
  - Ação que apaga pede confirmação ou oferece desfazer.
17
18
  - O endereço guarda o estado (aba, filtro, página): o voltar do navegador e o
18
19
  link compartilhado levam ao mesmo lugar.
@@ -32,6 +33,7 @@ novo) e cheio (texto longo quebra ou termina em `…`; mil itens não travam).
32
33
  **Layout**
33
34
  - De 360px a 1440px sem rolagem para o lado; respeita a área segura do celular
34
35
  (`env(safe-area-inset-bottom)`).
36
+ - Tela cheia é `100dvh`, nunca `100vh` (no celular, `vh` some sob a barra).
35
37
  - Imagem com `width`/`height` ou `aspect-ratio`, para a página não pular.
36
38
  - Número em coluna com `tabular-nums`; título com `text-wrap: balance`.
37
39
 
@@ -43,5 +45,9 @@ rolagem acompanham) e contraste de 4.5:1 nos dois temas. Animação só em
43
45
  `transform` e `opacity`, curta (150 a 300ms) e desligada em
44
46
  `prefers-reduced-motion`. Nunca `transition: all`.
45
47
 
48
+ **Pior caso** (teste com o dado ruim): Texto enorme (`min-width: 0` e
49
+ `overflow-wrap: anywhere`); Lista vazia; largura de 320px; Rede lenta (DevTools,
50
+ Slow 3G); Mil itens (paginar ou virtualizar).
51
+
46
52
  **Feche com a contagem**: quantos impedem o uso, quantos atrapalham, quantos são
47
53
  acabamento. E conserte nessa mesma ordem.
@@ -54,13 +54,14 @@ Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
54
54
  - **`forma: "traco"`**: forma livre por `caminho` (`d` de SVG, caixa 0 0 100
55
55
  100, M/L/C/Q/Z). `caminho` e `cor` animam:
56
56
  `"caminho": [{"t":1.6,"v":BOLA},{"t":2.5,"v":FOGUETE,"e":"elastico"}]`.
57
- - **`tipo: "grupo"`**: elementos que andam juntos, ou a tela dentro da tela
58
- (`"recorte": true`). Filhos contam tempo e posição a partir do grupo.
59
57
  - **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
60
58
  app nasce do tamanho e da COR do ícone.
61
59
 
62
- Por roteiro, o estilo segue o assunto: `motion` (padrão), `vox` (explicar; dê `destaque` e `fonte`),
63
- `marca`, `lancamento`; `visual` troca a peça de uma parte.
60
+ Por roteiro, o padrão é `hibrido` (dê `busca` em inglês por parte); `gerativo` (o motion), `vox` (explicar; dê
61
+ `destaque` e `fonte`), `marca`, `lancamento` pelo nome; `visual` troca a peça de uma parte.
62
+
63
+ Pedido de motion (ou sem referências): `estilo: "gerativo"`, só o roteiro
64
+ (`fala`, `destaque`); `roteiro.semente` sorteia outro, `modelo3d` traz o 3D.
64
65
 
65
66
  ## Cor e tipo
66
67
 
@@ -27,6 +27,12 @@ Tudo pelo `gh` (GitHub CLI) e pelo `git`, rodando no terminal do projeto.
27
27
  4. `gh pr create --fill` (ou `--title` e `--body` dizendo o que e por quê).
28
28
  Diga o link do PR.
29
29
 
30
+ **Segredo:** `.env` só existe na máquina. Nunca crie, edite ou renomeie
31
+ `.env` pelo site do GitHub (renomear `.env.example` para `.env` lá já vazou
32
+ chave em repositório público). O `.env.example` leva só os nomes, sem valor.
33
+ Antes do push: `git ls-files | grep '\.env$'` tem de vir vazio. Vazou? Troque a
34
+ chave na hora; apagar o arquivo não basta, ela fica no histórico.
35
+
30
36
  **Nunca** faça `push --force` em `main`, nunca apague branch dos outros, nunca
31
37
  commite `.env`. Conflito: resolva lendo os dois lados, não escolhendo um às
32
38
  cegas.
@@ -31,6 +31,9 @@ ela quem entra na conta dela.
31
31
  **3. Confira:** abra a URL publicada com browser_open e leia a página. Só diga
32
32
  que está no ar se ela abriu com o conteúdo certo. Termine dizendo o LINK.
33
33
 
34
+ **Link compartilhável:** `og:image` com URL absoluta (`https://.../capa.png`),
35
+ nunca `/capa.png`; sem isso o link sai sem imagem no WhatsApp.
36
+
34
37
  **Nunca** publique `.env`, chave ou senha: confira se `.gitignore` e o
35
38
  `.vercelignore`/`.netlifyignore` os deixam de fora. Variáveis secretas vão no
36
39
  painel do serviço (ou `vercel env add`), não no código.
@@ -26,6 +26,8 @@ tudo com a mesma duração; `transition: all`; texto piscando; nada que responda
26
26
  que acompanha o scroll (parallax, barra de progresso, imagem que cresce), use
27
27
  `animation-timeline: view()` e deixe a página parada onde isso não existe (`@supports`).
28
28
  - **Só `transform` e `opacity`.** Animar `top`, `width`, `height` ou `box-shadow` trava no celular.
29
+ Confira com grep: zero `transition: all` no CSS.
30
+ - **Seção de tela cheia** usa `100dvh`, e não `100vh`, pra não ficar atrás da barra do celular.
29
31
  - **Troca de página ou de estado:** a View Transitions API (`document.startViewTransition`), com o
30
32
  caminho sem animação quando o navegador não tiver.
31
33
  - **Biblioteca só se precisar.** CSS e um pouco de JS resolvem quase tudo. Timeline longa ou texto
package/studio/narrar.py CHANGED
@@ -256,13 +256,16 @@ def comando_falar(args):
256
256
  # tempo de CADA PALAVRA volta junto (WordBoundary do edge-tts): é o que
257
257
  # sincroniza a tipografia com a voz.
258
258
  REGISTRO = {
259
- "gancho": {"ritmo": "+6%", "tom": "+3Hz", "respiro": 0.35},
260
- "numero": {"ritmo": "-7%", "tom": "-1Hz", "respiro": 0.45},
261
- "contraste": {"ritmo": "+2%", "tom": "+1Hz", "respiro": 0.4},
262
- "lista": {"ritmo": "+0%", "tom": "+0Hz", "respiro": 0.35},
263
- "citacao": {"ritmo": "-10%", "tom": "-2Hz", "respiro": 0.5},
264
- "cta": {"ritmo": "+4%", "tom": "+2Hz", "respiro": 0.2},
265
- "normal": {"ritmo": "+0%", "tom": "+0Hz", "respiro": 0.3},
259
+ # 10.0: "os vídeos estão muito lentos" (29/09). Ritmo de anúncio por
260
+ # padrão — ninguém fala devagar num reels — e o respiro entre as partes
261
+ # caiu para o de uma vírgula: o corte seco é que dá energia.
262
+ "gancho": {"ritmo": "+18%", "tom": "+4Hz", "respiro": 0.12},
263
+ "numero": {"ritmo": "+8%", "tom": "+1Hz", "respiro": 0.18},
264
+ "contraste": {"ritmo": "+14%", "tom": "+2Hz", "respiro": 0.15},
265
+ "lista": {"ritmo": "+14%", "tom": "+2Hz", "respiro": 0.12},
266
+ "citacao": {"ritmo": "+6%", "tom": "+0Hz", "respiro": 0.2},
267
+ "cta": {"ritmo": "+16%", "tom": "+4Hz", "respiro": 0.1},
268
+ "normal": {"ritmo": "+14%", "tom": "+2Hz", "respiro": 0.12},
266
269
  }
267
270
 
268
271