primocode 10.11.0 → 10.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PrimoCode v10.10.1
1
+ # PrimoCode v10.11.1
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 v10.10.1
12
+ ▐▛███▜▌ Primo Code v10.11.1
13
13
  ▝▜█████▛▘ Bem-vindo de volta, Joel
14
14
  ▘▘ ▝▝ ~/primocode · /dir <pasta> muda
15
15
  ```
@@ -17,6 +17,14 @@ está no limite de pedidos, o próximo atende. Você não vê o erro, e não pag
17
17
  Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
18
18
  veja `/mascot` e `/name`.
19
19
 
20
+ ## O que mudou na 10.11
21
+
22
+ - **Barulhinhos:** cada palavra, item e ponto que entra no motion faz um som pequeno, numa escada de notas, com tique, clique de mouse, blip, swoosh e sub na revelação. A cena pode pedir o som no quadro certo (`sons: [{t, som}]`).
23
+ - **Vídeo sem narração:** `semNarracao: true` (ou "sem voz" no pedido) gera só motion, trilha e efeitos, e a trilha não abaixa.
24
+ - **Efeitos de fábrica:** peça do site em 3D com impacto, brilho que varre, cor que inunda, risco em várias linhas, entrada com física e raios de luz. Quando nenhuma cena é escrita, o plano devolve o cardápio de efeitos.
25
+ - **3D com peso:** o boneco antecipa, respira, pula amassando e anda com o quadril.
26
+ - **Risco:** texto riscado que quebra em várias linhas ganha um traço por linha.
27
+
20
28
  ## O que mudou na 10.4
21
29
 
22
30
  - **A tela do OpenCode.** O PrimoCode abre em tela cheia: a conversa rola em
package/lib/catalogo.js CHANGED
@@ -94,7 +94,7 @@ const GRUPOS = [
94
94
  + 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
95
95
  + 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
96
96
  + 'componente:"componentes/x.jsx" numa parte = a cena que VOCÊ escreveu em React — é o caminho do motion (skill Motion de primeira): uma ideia desenhada por fala; a voz, a trilha, os sons e o fundo seguem do Estúdio; cena que não compila volta em cenasQuebradas. '
97
- + 'semNarracao:true no roteiro (ou pedido "sem narração") = vídeo só com motion, trilha e efeitos sonoros: sem estudio_narrar, cada parte dura o seu "duracao" (s) ou o tempo de ler a tela, e um som leve entra em cada corte e entrada. '
97
+ + 'semNarracao:true no roteiro (ou pedido "sem narração") = vídeo só com motion, trilha e efeitos sonoros: sem estudio_narrar, cada parte dura o seu "duracao" (s) ou o tempo de ler a tela, e um som leve entra em cada corte e entrada. Toda parte aceita sons:[{t, som}] (t na parte; pop, mouse, tique, digital, swoosh, sub, ponto1..ponto5) para o barulhinho cair no quadro em que a coisa acontece. '
98
98
  + 'estilo (deixe VAZIO: o gerativo, com as suas cenas em componente e o estiloVisual que você montou; outro só se a pessoa pedir pelo nome): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega — a COISA que a frase nomeia, 2 a 4 palavras, não a frase inteira; rota:"tipografia" em número/citação, "produto" quando a frase fala do produto; ancora:"palavra" — o clipe troca 0,2 s depois dela) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som — estiloVisual {humor, paleta, display, texto, mono, genero, ambiente, acento, material} é o estilo que VOCÊ monta pelo assunto (cardápio: estudio_midia acao:estilos; vazio, o PrimoCode sorteia); modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
99
99
  + 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D no visual de blocos/voxel de render de cinema — é a escolha para animação 3D, personagem, bicho, boneco, avatar, mascote, podcast, entrevista ou alguém FALANDO em 3D; cada parte é UMA fala {falante, texto, emocao?, destaque?, acao?, cenario?}, sem tipo). NADA É FIXO, monte o que a pessoa pediu: cena:"texto livre do lugar" (praia, cidade, floresta, parque, fazenda, neve, deserto, espaço, fundo do mar, escritório, sala, cozinha, escola, palco… + hora: "à noite", "ao pôr do sol") ou cena:{lugar, hora, elementos:["palmeira","barraca","violão",…]} — elemento fora do catálogo vira ícone 3D em blocos; parte.cenario muda o lugar a partir daquela fala. Até 4 falantes; personagens:{"nome":{tipo:pessoa|robo|animal|coisa, especie (animal: cachorro|gato|cavalo|vaca|porco|ovelha|raposa|lobo|urso|panda|coelho|leao|tigre|elefante|girafa|tartaruga|galinha|pato|passaro|papagaio|coruja|pinguim|peixe|tubarao|golfinho|dinossauro|sapo|macaco), icone (coisa: "pizza", "café", "polvo" — qualquer objeto/bicho que fala), pessoa: pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba; robo: cor}} — sem tipo, o nome decide (nome de bicho vira bicho, de IA vira o mascote laranja, de objeto vira coisa); figurantes:["cachorro","pessoa"] (quem está na cena sem falar); BICHO NÃO FALA: a fala de um bicho sai da narração e ele fica na cena — só fala se a pessoa pedir ("cachorro falante") ou personagens.x.fala:true; a voz de cada falante sai do GÊNERO do nome (personagens.x.genero:f|m manda), uma voz por personagem do começo ao fim; bigode:true e cabeca:"cubo" (o rosto de pixel antigo) em personagens; sem lugar no pedido, o set é a cobertura à noite (janela da cidade, cartões de interface na frente). acao em cada parte: acenar|apontar|pular|dancar|comemorar|pensar|andar|correr. emocao feliz|serio|surpreso|pensativo|animado|curioso; destaque:"texto curto"; paineis:["título",…] só no set de telas (o padrão quando o pedido não diz lugar); visual:"liso" só se pedirem o boneco liso antigo). '
100
100
  + 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
@@ -17,6 +17,8 @@ const fs = require('fs');
17
17
  const os = require('os');
18
18
  const path = require('path');
19
19
 
20
+ const EASING = ['linear', 'ease', 'quad', 'cubic', 'poly', 'sin', 'circle', 'exp', 'elastic', 'back', 'bounce', 'bezier', 'in', 'out', 'inOut', 'step0', 'step1'];
21
+ const TROCA = { expo: 'exp', sine: 'sin', circ: 'circle', cubicBezier: 'bezier', easeIn: 'in', easeOut: 'out', easeInOut: 'inOut', quart: 'poly' };
20
22
  const PERMITIDOS = /^(react|remotion|three|@remotion\/[\w-]+|\.{1,2}\/.+)$/;
21
23
 
22
24
  let esbuild;
@@ -33,6 +35,10 @@ function conferirCena(arquivo, { compilar = compilador() } = {}) {
33
35
  if (!/export\s+default\b/.test(codigo)) return { ok: false, erro: 'falta o export default da função da cena ({el, t, fps}).' };
34
36
  const fora = [...codigo.matchAll(/\bfrom\s+['"]([^'"]+)['"]|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g)].map((m) => m[1] || m[2]).filter((x) => !PERMITIDOS.test(x));
35
37
  if (fora.length) return { ok: false, erro: `import fora do permitido: ${[...new Set(fora)].join(', ')} (só react, remotion, @remotion/*, three).` };
38
+ /* Easing que não existe compila, mas mata o render inteiro (14/10, a
39
+ padaria: "Easing.expo" — easing is not a function, 3 renders perdidos). */
40
+ const easingErrado = [...new Set([...codigo.matchAll(/\bEasing\.(\w+)/g)].map((m) => m[1]))].filter((n) => !EASING.includes(n));
41
+ if (easingErrado.length) return { ok: false, erro: `Easing.${easingErrado[0]} não existe no Remotion${TROCA[easingErrado[0]] ? ` — use Easing.${TROCA[easingErrado[0]]}` : ''} (existem: ${EASING.join(', ')}).` };
36
42
  if (!compilar) return { ok: true, semCompilador: true };
37
43
  try {
38
44
  compilar.transformSync(codigo, { loader: /\.tsx$/i.test(arquivo) ? 'tsx' : 'jsx', jsx: 'automatic', logLevel: 'silent' });
@@ -409,9 +409,13 @@ async function ferramentaPlano(args) {
409
409
  /* SEM NARRAÇÃO: o som sobe de papel — um efeito leve em cada corte e em
410
410
  cada entrada marcante, variando (sem-narracao.js, sons-leves.js). */
411
411
  if (args.__semNarracao) {
412
- const n = semNarracao.sonorizarPlano(r, { tempos: args.__semNarracao.tempos, sons: sonsLeves.preparar(p.pasta) });
412
+ const n = semNarracao.sonorizarPlano(r, { tempos: args.__semNarracao.tempos, cues: args.__semNarracao.cues, sons: sonsLeves.preparar(p.pasta) });
413
413
  if (r.resumo) r.resumo.semNarracao = true;
414
- projeto.anotar(p.id, 'plano', `sem narração: ${n} efeitos leves nos cortes e entradas`);
414
+ projeto.anotar(p.id, 'plano', `sem narração: ${n} efeitos leves nos cortes, entradas e micros`);
415
+ } else if (args.__barulhinhos && r.plano) {
416
+ // Com voz: os micros e os sons pedidos pela cena, baixinho por baixo da fala.
417
+ const n = semNarracao.sonorizarPlano(r, { tempos: args.__barulhinhos.tempos, cues: args.__barulhinhos.cues, sons: sonsLeves.preparar(p.pasta), comVoz: true });
418
+ if (n) projeto.anotar(p.id, 'plano', `barulhinhos: ${n} micros por baixo da fala`);
415
419
  }
416
420
  const ic = await variarIcones(r.plano, original);
417
421
  if (ic.trocados) projeto.anotar(p.id, 'icones', `coleção ${ic.colecao}: ${ic.trocados} ícones`);
@@ -439,9 +443,18 @@ async function ferramentaPlano(args) {
439
443
  projeto.salvar(args.id, { estrutura: usada.estrutura || '-' });
440
444
  }
441
445
  const quebradas = args.__cenasQuebradas || [];
446
+ /* NENHUMA CENA ESCRITA (14/10, a padaria da 10.11.0 saiu toda no
447
+ gerativo): a regra morava só na skill, e regra escrita não é regra
448
+ obedecida. O plano volta com o cardápio na mão — a IA ainda decide. */
449
+ const pts = Array.isArray(original) ? original : (original && original.partes) || [];
450
+ const nenhuma = pts.length > 1 && !pts.some((pt) => pt && pt.componente) && !quebradas.length;
442
451
  return {
443
452
  ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
444
453
  ...(quebradas.length ? { cenasQuebradas: quebradas } : {}),
454
+ ...(nenhuma ? { nenhumaCenaEscrita: {
455
+ aviso: 'Todas as partes caíram no gerativo automático. Para motion de verdade, escreva ao menos uma cena (componente), partindo de um destes efeitos (estudio_midia usar-efeito) se servir, e mande o roteiro de novo. Cada parte pode pedir som: sons:[{t, som}] (pop, mouse, tique, digital, swoosh, sub, ponto1..ponto5). Se o automático é o que você quer, siga para o render.',
456
+ efeitos: efeitos.cardapio().slice(0, 6),
457
+ } } : {}),
445
458
  proximo: quebradas.length
446
459
  ? `${quebradas.length} cena(s) sua(s) não compilaram e ficaram no gerativo: conserte o arquivo (o erro está em cenasQuebradas) e mande o roteiro de novo antes do render.`
447
460
  : 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
@@ -1792,7 +1805,7 @@ async function prepararRoteiro(p, args) {
1792
1805
  texto da tela pede), e as palavras são repartidas na parte. Os sons
1793
1806
  leves entram depois do plano, em cada corte e entrada (sem-narracao.js). */
1794
1807
  tempos = semNarracao.tempos(partes, { falaDe: composicao.falaDe });
1795
- args.__semNarracao = { tempos };
1808
+ args.__semNarracao = { tempos, cues: semNarracao.cuesDe(partes, tempos) };
1796
1809
  const total = tempos.length ? tempos[tempos.length - 1].ate : 0;
1797
1810
  projeto.anotar(p.id, 'narracao', `sem narração: ${Math.round(total * 10) / 10}s em ${partes.length} partes, só motion e efeitos sonoros`);
1798
1811
  sons = estilo === 'gerativo' ? {} : ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
@@ -1870,6 +1883,7 @@ async function prepararRoteiro(p, args) {
1870
1883
  }
1871
1884
  }
1872
1885
  tempos = v.partes;
1886
+ args.__barulhinhos = { tempos, cues: semNarracao.cuesDe(partes, tempos) };
1873
1887
  narracao = path.relative(p.pasta, mp3).replace(/\\/g, '/');
1874
1888
  projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${partes.length} partes, voz ${v.voz}`);
1875
1889
  // O motion usa os efeitos leves; os estilos de corte, os cheios.
@@ -67,7 +67,10 @@ function revisar(plano, { semImagem = false } = {}) {
67
67
  const efeitos = ((plano.audio && plano.audio.efeitos) || []).slice().sort((a, b) => (a.t || 0) - (b.t || 0));
68
68
  if (efeitos.length) {
69
69
  const fica = [];
70
- for (const e of efeitos) {
70
+ /* O micro (o tique, a escada de notas) não conta para o teto: é a
71
+ textura das referências, 2 a 3 por segundo, baixinho (14/10). */
72
+ const micros = efeitos.filter((e) => e.micro);
73
+ for (const e of efeitos.filter((x) => !x.micro)) {
71
74
  const ult = fica[fica.length - 1];
72
75
  if (ult && e.src === ult.src && (e.t || 0) - (ult.t || 0) < 0.3) {
73
76
  if ((e.volume ?? 1) > (ult.volume ?? 1)) fica[fica.length - 1] = e;
@@ -88,7 +91,7 @@ function revisar(plano, { semImagem = false } = {}) {
88
91
  final = guardados.sort((a, b) => (a.t || 0) - (b.t || 0));
89
92
  consertos.push(`${fica.length} efeitos sonoros em ${r2(total)}s eram ruído — ficaram os ${final.length} mais fortes`);
90
93
  }
91
- plano.audio.efeitos = final;
94
+ plano.audio.efeitos = [...final, ...micros].sort((a, b) => (a.t || 0) - (b.t || 0));
92
95
  }
93
96
 
94
97
  // 4. Texto que não dá tempo de ler.
@@ -65,12 +65,14 @@ function tempos(partes, { falaDe = textoDaTela } = {}) {
65
65
 
66
66
  /* Os momentos que pedem som: os CORTES (começo de parte, de batida, de cena)
67
67
  e as ENTRADAS marcantes (o ícone, o item, a cena que a IA escreveu). */
68
- function momentos({ tempos: tps = [], plano = null, partitura = null } = {}) {
69
- const cortes = [], entradas = [];
68
+ function momentos({ tempos: tps = [], plano = null, partitura = null, cues = [] } = {}) {
69
+ const cortes = [], entradas = [], micros = [], revelas = [];
70
70
  const perto = (lista, t, d = 0.3) => lista.some((x) => Math.abs(x - t) < d);
71
71
  const corte = (t) => { if (t > 0.3 && !perto(cortes, t)) cortes.push(r3(t)); };
72
72
  const entrada = (t) => { if (t != null && t >= 0 && !perto(entradas, t, 0.35) && !perto(cortes, t, 0.2)) entradas.push(r3(t)); };
73
73
  for (const t of tps.slice(1)) corte(t.de);
74
+ // A cena escrita pela IA não tem batida: as palavras repartidas na parte são os micros dela.
75
+ for (const tp of tps) for (const pw of tp.palavras || []) if (pw && pw.de != null) micros.push(r3(pw.de));
74
76
  const bs = (partitura && partitura.batidas) || [];
75
77
  for (const b of bs) corte(b.de);
76
78
  // Vídeo de várias cenas (os outros estilos): o começo de cada cena é um corte.
@@ -80,25 +82,35 @@ function momentos({ tempos: tps = [], plano = null, partitura = null } = {}) {
80
82
  for (const b of bs) {
81
83
  if (b.icone3d && b.icone3d.t != null) entrada(b.icone3d.t);
82
84
  for (const l of b.linhas || []) if ((l.papel === 'item' || l.papel === 'numero') && l.t != null) entrada(l.t);
83
- if (b.comp === 'marca') entrada(b.de + 0.35);
85
+ if (b.comp === 'marca') { entrada(b.de + 0.35); revelas.push(r3(b.de + 0.3)); }
86
+ /* O MICRO (14/10, as referências): cada linha e cada palavra que entra
87
+ faz um som pequeno — é isso que dá a sensação de "tudo tem som". */
88
+ for (const l of b.linhas || []) {
89
+ if (l.t != null) micros.push(r3(l.t));
90
+ for (const pw of l.palavras || []) if (pw && pw.t != null) micros.push(r3(pw.t));
91
+ }
84
92
  }
85
93
  for (const c of (plano && plano.cenas) || []) for (const el of c.elementos || []) if (el && el.tipo === 'componente' && el.de != null) entrada(el.de + 0.08);
86
- return { cortes: cortes.sort((a, b) => a - b), entradas: entradas.sort((a, b) => a - b) };
94
+ const ocupado = [...cortes, ...entradas];
95
+ const mic = [...new Set(micros)].sort((a, b) => a - b).filter((t, i, l) => t > 0.05 && !ocupado.some((x) => Math.abs(x - t) < 0.1) && (i === 0 || t - l[i - 1] >= 0.09));
96
+ return { cortes: cortes.sort((a, b) => a - b), entradas: entradas.sort((a, b) => a - b), micros: mic, revelas, cues: (cues || []).filter((c) => c && c.t != null) };
87
97
  }
88
98
 
89
99
  // O corte passa com ar; a entrada chega com um toque pequeno. Ordem de rodízio.
90
- const DO_CORTE = ['whoosh', 'sopro', 'toque', 'riser'];
91
- const DA_ENTRADA = ['pop', 'bolha', 'clique', 'brilho'];
100
+ const DO_CORTE = ['swoosh', 'whoosh', 'sopro', 'toque', 'riser'];
101
+ const DA_ENTRADA = ['pop', 'mouse', 'bolha', 'digital', 'clique', 'brilho'];
102
+ // O micro sobe numa escada (ponto1→ponto5) enquanto as coisas entram em sequência, e recomeça na pausa.
103
+ const ESCADA = ['ponto1', 'ponto2', 'ponto3', 'ponto4', 'ponto5'];
92
104
  // O whoosh e o sopro antecipam o corte (o ar chega antes da imagem); o riser sobe até ele.
93
- const ANTES = { whoosh: 0.18, sopro: 0.3, riser: 1.0, toque: 0 };
94
- const VOLUME = { whoosh: 0.5, sopro: 0.6, toque: 0.6, riser: 0.4, pop: 0.42, bolha: 0.45, clique: 0.4, brilho: 0.32 };
105
+ const ANTES = { whoosh: 0.18, swoosh: 0.12, sopro: 0.3, riser: 1.0, toque: 0 };
106
+ const VOLUME = { swoosh: 0.45, mouse: 0.4, digital: 0.3, tique: 0.3, sub: 0.7, whoosh: 0.5, sopro: 0.6, toque: 0.6, riser: 0.4, pop: 0.42, bolha: 0.45, clique: 0.4, brilho: 0.32 };
95
107
 
96
108
  /**
97
109
  * Soma aos efeitos do plano um som leve em cada corte e em cada entrada.
98
110
  * `sons` é o mapa nome → arquivo do sons-leves.preparar. Não encosta em som
99
111
  * que já existe (0,12 s) e nunca repete o mesmo som duas vezes seguidas.
100
112
  */
101
- function sonorizar(efeitos, { cortes = [], entradas = [], sons = {}, duracao = Infinity } = {}) {
113
+ function sonorizar(efeitos, { cortes = [], entradas = [], micros = [], revelas = [], cues = [], sons = {}, duracao = Infinity, volMicro = 0.24 } = {}) {
102
114
  const fora = (efeitos || []).slice();
103
115
  const eventos = [...cortes.map((t) => ({ t, tipo: 'corte' })), ...entradas.map((t) => ({ t, tipo: 'entrada' }))].sort((a, b) => a.t - b.t);
104
116
  const giro = { corte: 0, entrada: 0 };
@@ -121,20 +133,57 @@ function sonorizar(efeitos, { cortes = [], entradas = [], sons = {}, duracao = I
121
133
  novos.push({ t, src: sons[nome], volume: VOLUME[nome] || 0.4, som: nome });
122
134
  ultimo = nome;
123
135
  }
136
+ // A revelação (a marca no fim) ganha o sub por baixo do som da entrada.
137
+ for (const t of revelas) if (sons.sub && t < duracao - 0.1) novos.push({ t: r3(t), src: sons.sub, volume: VOLUME.sub, som: 'sub' });
138
+ // O som que a cena pediu (parte.sons: a IA sabe quando a bolinha chega).
139
+ for (const c of cues) {
140
+ const nome = sons[c.som] ? c.som : (sons.pop ? 'pop' : null);
141
+ if (nome && c.t < duracao - 0.05) novos.push({ t: r3(c.t), src: sons[nome], volume: Number(c.volume) || VOLUME[nome] || 0.35, som: nome, micro: true });
142
+ }
143
+ // Os micros: a escada sobe em sequência e recomeça depois de 0,6 s de pausa; um tique a cada tanto quebra a repetição.
144
+ let degrau = -1, antes = -9;
145
+ for (const t of micros) {
146
+ if (t > duracao - 0.05) continue;
147
+ degrau = t - antes > 0.6 ? 0 : Math.min(ESCADA.length - 1, degrau + 1);
148
+ antes = t;
149
+ const nome = degrau === ESCADA.length - 1 && sons.tique && micros.length > 8 && Math.round(t * 10) % 3 === 0 ? 'tique' : ESCADA[degrau];
150
+ if (!sons[nome] || novos.some((e) => Math.abs(e.t - t) < 0.08)) continue;
151
+ novos.push({ t, src: sons[nome], volume: volMicro, som: nome, micro: true });
152
+ }
124
153
  return { efeitos: [...fora, ...novos.map(({ som, ...e }) => e)].sort((a, b) => a.t - b.t), novos };
125
154
  }
126
155
 
127
156
  /** O plano pronto ganha os barulhinhos (o plano é mudado no lugar). Devolve quantos entraram. */
128
- function sonorizarPlano(r, { tempos: tps = [], sons = {} } = {}) {
157
+ function sonorizarPlano(r, { tempos: tps = [], sons = {}, cues = [], comVoz = false } = {}) {
129
158
  if (!r || !r.plano) return 0;
130
159
  const plano = r.plano;
131
160
  plano.audio = plano.audio || {};
132
161
  const duracao = (plano.cenas || []).reduce((n, c) => n + (Number(c.duracao) || 0), 0) || Infinity;
133
- const m = momentos({ tempos: tps, plano, partitura: r.partitura });
134
- const s = sonorizar(plano.audio.efeitos || [], { ...m, sons, duracao });
162
+ const m = momentos({ tempos: tps, plano, partitura: r.partitura, cues });
163
+ /* Com voz, os cortes e entradas já têm som (gerativo.js): entram só os
164
+ micros e os pedidos pela cena, baixinho, para não brigar com a fala. */
165
+ const s = comVoz
166
+ ? sonorizar(plano.audio.efeitos || [], { micros: m.micros, cues: m.cues, sons, duracao, volMicro: 0.14 })
167
+ : sonorizar(plano.audio.efeitos || [], { ...m, sons, duracao });
135
168
  plano.audio.efeitos = s.efeitos;
169
+ if (comVoz) return s.novos.length;
136
170
  delete plano.audio.narracao;
171
+ /* Sem voz não há o que abrir espaço: a trilha não abaixa entre os cortes
172
+ (o ducking era para a fala, e deixava o vídeo em -17 LUFS). */
173
+ const tr = plano.audio.trilha;
174
+ if (tr && Array.isArray(tr.volumes)) tr.volumes = tr.volumes.map((v) => (v.t > 0.3 && v.v < 0.85 ? { ...v, v: 0.85 } : v));
137
175
  return s.novos.length;
138
176
  }
139
177
 
140
- module.exports = { ativo, textoDaTela, duracaoDe, tempos, momentos, sonorizar, sonorizarPlano, PEDIDO };
178
+ /** parte.sons [{t, som, volume}] (t na parte) → no tempo do vídeo. */
179
+ function cuesDe(partes = [], tps = []) {
180
+ const out = [];
181
+ (partes || []).forEach((pt, i) => {
182
+ const de = tps[i] && tps[i].de;
183
+ if (de == null || !pt || !Array.isArray(pt.sons)) return;
184
+ for (const c of pt.sons) if (c && Number.isFinite(Number(c.t))) out.push({ t: r3(de + Number(c.t)), som: String(c.som || 'pop'), volume: c.volume });
185
+ });
186
+ return out;
187
+ }
188
+
189
+ module.exports = { cuesDe, ativo, textoDaTela, duracaoDe, tempos, momentos, sonorizar, sonorizarPlano, PEDIDO };
@@ -40,6 +40,21 @@ const EFEITOS = {
40
40
  impacto: ['-f', 'lavfi', '-i', "aevalsrc='0.95*sin(2*PI*(62-26*t)*t)*exp(-4.5*t)+0.45*(random(0)*2-1)*exp(-38*t)':s=44100:d=1.1", '-af', 'lowpass=f=2600,volume=1.4'],
41
41
  riser: ['-f', 'lavfi', '-i', "aevalsrc='(0.32*sin(2*PI*(170+620*t*t)*t)+0.22*(random(0)*2-1))*pow(t/1.1,2)':s=44100:d=1.1", '-af', 'highpass=f=260,afade=t=out:st=1.04:d=0.06,volume=1.3'],
42
42
  pop: ['-f', 'lavfi', '-i', "aevalsrc='0.8*sin(2*PI*(980-640*t)*t)*exp(-28*t)*(1-exp(-700*t))':s=44100:d=0.18"],
43
+ /* OS BARULHINHOS DAS REFERÊNCIAS (14/10, "eu quero meus barulhinhos", com o
44
+ motion1.mp4: 29 sons em 15 s, cada bolinha e cada ponto que aparece faz
45
+ um). Pequenos e secos: o tique da interface, o clique do mouse, o blip
46
+ digital, o swoosh que desce, o sub da revelação e a escada de notas. */
47
+ tique: ['-f', 'lavfi', '-i', "aevalsrc='0.45*sin(2*PI*3200*t)*exp(-140*t)+0.12*(random(0)*2-1)*exp(-220*t)':s=44100:d=0.05", '-af', 'highpass=f=900'],
48
+ mouse: ['-f', 'lavfi', '-i', "aevalsrc='0.6*(random(0)*2-1)*exp(-260*t)+0.45*(random(0)*2-1)*exp(-260*(t-0.07))*gte(t,0.07)':s=44100:d=0.14", '-af', 'bandpass=f=2600:width_type=o:w=1.6,volume=1.6'],
49
+ digital: ['-f', 'lavfi', '-i', "aevalsrc='0.28*sgn(sin(2*PI*(1500+900*gte(t,0.03))*t))*exp(-38*t)':s=44100:d=0.08", '-af', 'lowpass=f=5200'],
50
+ swoosh: ['-f', 'lavfi', '-i', 'anoisesrc=d=0.55:c=white:a=0.8', '-af',
51
+ 'bandpass=f=2600:width_type=o:w=3,afade=t=in:st=0:d=0.12:curve=exp,afade=t=out:st=0.12:d=0.43:curve=exp,volume=1.6'],
52
+ sub: ['-f', 'lavfi', '-i', "aevalsrc='0.9*sin(2*PI*(55-20*t)*t)*exp(-3*t)*(1-exp(-300*t))':s=44100:d=1.2", '-af', 'lowpass=f=220,volume=1.5'],
53
+ ponto1: ['-f', 'lavfi', '-i', "aevalsrc='0.55*sin(2*PI*784.0*t)*exp(-34*t)*(1-exp(-900*t))+0.15*sin(2*PI*1568.0*t)*exp(-50*t)':s=44100:d=0.16"],
54
+ ponto2: ['-f', 'lavfi', '-i', "aevalsrc='0.55*sin(2*PI*880.0*t)*exp(-34*t)*(1-exp(-900*t))+0.15*sin(2*PI*1760.0*t)*exp(-50*t)':s=44100:d=0.16"],
55
+ ponto3: ['-f', 'lavfi', '-i', "aevalsrc='0.55*sin(2*PI*987.8*t)*exp(-34*t)*(1-exp(-900*t))+0.15*sin(2*PI*1975.6*t)*exp(-50*t)':s=44100:d=0.16"],
56
+ ponto4: ['-f', 'lavfi', '-i', "aevalsrc='0.55*sin(2*PI*1174.7*t)*exp(-34*t)*(1-exp(-900*t))+0.15*sin(2*PI*2349.4*t)*exp(-50*t)':s=44100:d=0.16"],
57
+ ponto5: ['-f', 'lavfi', '-i', "aevalsrc='0.55*sin(2*PI*1318.5*t)*exp(-34*t)*(1-exp(-900*t))+0.15*sin(2*PI*2637.0*t)*exp(-50*t)':s=44100:d=0.16"],
43
58
  };
44
59
 
45
60
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.11.0",
3
+ "version": "10.11.1",
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": {