primocode 10.0.0 → 10.0.9

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 (55) hide show
  1. package/README.md +2 -2
  2. package/bin/primocode.js +69 -1
  3. package/lib/act.js +118 -19
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +1 -1
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/entrada.js +73 -9
  9. package/lib/estudio/cinema.js +27 -9
  10. package/lib/estudio/diretor.js +13 -1
  11. package/lib/estudio/ende.js +16 -5
  12. package/lib/estudio/estilo.js +23 -8
  13. package/lib/estudio/gerativo.js +1453 -0
  14. package/lib/estudio/hibrido.js +165 -0
  15. package/lib/estudio/index.js +244 -36
  16. package/lib/estudio/kit/gerativo.jsx +980 -0
  17. package/lib/estudio/kit/index.jsx +3 -1
  18. package/lib/estudio/kit/tres.jsx +308 -0
  19. package/lib/estudio/kit.js +7 -1
  20. package/lib/estudio/lancamento.js +28 -0
  21. package/lib/estudio/referencias.js +105 -0
  22. package/lib/estudio/remotion.js +53 -9
  23. package/lib/estudio/ritmo.js +2 -2
  24. package/lib/estudio/sons-leves.js +38 -1
  25. package/lib/estudio/variedade.js +105 -0
  26. package/lib/estudio/vox.js +37 -8
  27. package/lib/mcp.js +156 -0
  28. package/lib/personas.js +67 -0
  29. package/lib/skills.js +36 -0
  30. package/lib/tools.js +22 -3
  31. package/lib/ui.js +41 -6
  32. package/lib/video/auto.js +329 -0
  33. package/lib/video/camadas.js +207 -0
  34. package/lib/video/cli.js +1 -1
  35. package/lib/video/filters.js +39 -6
  36. package/lib/video/fx3d/Camadas.jsx +64 -0
  37. package/lib/video/fx3d/Objetos.jsx +128 -0
  38. package/lib/video/fx3d/Root.jsx +18 -0
  39. package/lib/video/fx3d/modelos.jsx +304 -0
  40. package/lib/video/fx3d.js +108 -0
  41. package/lib/video/gerados.js +119 -0
  42. package/lib/video/objetos.js +111 -0
  43. package/lib/video/ops-fx.js +453 -0
  44. package/lib/video/ops.js +30 -5
  45. package/lib/video/paralelo.js +50 -0
  46. package/lib/video/pedido.js +191 -0
  47. package/lib/video/rastrear.js +95 -0
  48. package/lib/video/rastro.js +276 -0
  49. package/lib/video/render.js +67 -14
  50. package/lib/video/texto.js +6 -4
  51. package/lib/video/timeline.js +9 -1
  52. package/package.json +2 -2
  53. package/skills/motion-de-primeira/SKILL.md +5 -4
  54. package/studio/narrar.py +10 -7
  55. package/studio/rastrear.py +399 -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.0.9",
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/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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -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
 
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