primocode 10.7.0 → 10.8.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.
@@ -47,6 +47,7 @@ const assets = require('./assets.js');
47
47
  const video = require('./video.js');
48
48
  const batida = require('./batida.js');
49
49
  const revisor = require('./revisor.js');
50
+ const conferencia = require('./conferencia.js');
50
51
  const margem = require('./margem.js');
51
52
  const assinatura = require('./assinatura.js');
52
53
  const identidade = require('./identidade.js');
@@ -1910,7 +1911,7 @@ async function prepararRoteiro(p, args) {
1910
1911
  return r;
1911
1912
  }
1912
1913
  if (estilo === 'personagem' && modo === 'video') {
1913
- const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
1914
+ const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt, voz: args.voz || roteiro.voz }, largura, altura, tempos, narracao, sons, trilha });
1914
1915
  if (r.ok) {
1915
1916
  /* O que não é peça do catálogo vira ícone 3D da biblioteca, em blocos
1916
1917
  (kit/mundo.jsx): "uma pizza que fala", "um violão na praia". Sem
@@ -2106,6 +2107,12 @@ async function ferramentaRender(args, ctx = {}) {
2106
2107
  projeto.salvar(args.id, { renders: [...(p.renders || []), { quando: new Date().toISOString(), arquivo: saida, bytes }] });
2107
2108
  projeto.anotar(args.id, 'render', `${(bytes / 1048576).toFixed(1)} MB, ${conferida.duracao}s`);
2108
2109
 
2110
+ /* A CONFERÊNCIA (conferencia.js): o arquivo que saiu, medido. Som fora do
2111
+ alvo e imagem parada viram aviso; quem decide se refaz é o agente. */
2112
+ const conf = fs.existsSync(saida) ? await conferencia.conferir(saida, { duracao: conferida.duracao }) : { ok: false };
2113
+ const avisosDoArquivo = conf.ok ? conf.avisos : [];
2114
+ if (avisosDoArquivo.length) projeto.anotar(args.id, 'conferencia', avisosDoArquivo.join('; '));
2115
+
2109
2116
  // O vídeo pronto abre. É a única coisa que abre — a interface morreu, e
2110
2117
  // o que a pessoa pediu foi o vídeo.
2111
2118
  if (ctx.abrir) { try { ctx.abrir(saida); } catch { /* o caminho está na resposta */ } }
@@ -2114,8 +2121,9 @@ async function ferramentaRender(args, ctx = {}) {
2114
2121
  ok: true, arquivo: saida, bytes, segundos: conferida.duracao,
2115
2122
  faltaram: faltaram.length ? faltaram : undefined,
2116
2123
  revisao: revisao.consertos.length || revisao.avisos.length ? { consertado: revisao.consertos, avisos: revisao.avisos } : undefined,
2124
+ conferencia: conf.ok ? { som: conf.som, parados: conf.parados, avisos: conf.avisos } : undefined,
2117
2125
  aviso: 'ABERTO para o usuário assistir. Chame finish dizendo o que foi feito'
2118
- + (revisao.avisos.length ? ' — e cite os avisos da revisão que valem um novo plano.' : '.'),
2126
+ + (revisao.avisos.length || avisosDoArquivo.length ? ' — e cite os avisos da revisão e da conferência que valem um novo plano.' : '.'),
2119
2127
  };
2120
2128
  }
2121
2129
 
@@ -281,6 +281,11 @@ function leiaute(P) {
281
281
  ela ficava VAZIA até a voz chegar na frase (teste de 08/10). */
282
282
  const p0 = pr ? pr.palavras[0].em : b.de;
283
283
  const entT = [Math.max(ent[0], p0 - 0.4), Math.max(ent[1], p0 + 0.1)];
284
+ /* Mas a pílula de ANTES ficava: o texto dela saía e a forma esperava
285
+ vazia até a voz (0,6 s no café de 11/10, de 7,2 s a 7,9 s). Quando
286
+ a voz demora, a forma encolhe num ponto e cresce já com a frase. */
287
+ // Começa em b.de, quando o texto de antes já saiu (antes disso a forma laranja ficava atrás dele).
288
+ if (entT[0] >= b.de + 0.4) vai(b.de, b.de + 0.4, forma(tx, ty, 16 * u, 16 * u, { solid: 1, ac: 1 }));
284
289
  if (umaLinha) vai(entT[0], entT[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
285
290
  else vai(entT[0], entT[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
286
291
  }
@@ -706,13 +706,13 @@ function texturaCartaoUso(titulo, linhas, k, uso) {
706
706
  return cv;
707
707
  }
708
708
  g.fillStyle = '#ff6a2b'; g.beginPath(); g.arc(46, 50, 9, 0, Math.PI * 2); g.fill();
709
- g.fillStyle = '#f2f2f7'; g.font = `600 28px ${F}`; g.fillText(`Claude · sessão ${k + 1}`, 66, 60);
709
+ g.fillStyle = '#f2f2f7'; g.font = `600 28px ${F}`; g.fillText(`Primo · sessão ${k + 1}`, 66, 60); // a marca é a do PrimoCode, nunca a da referência (12/10)
710
710
  g.fillStyle = '#ffffff'; g.font = `700 30px ${F}`; g.fillText(String(titulo).slice(0, 30), 34, 134);
711
711
  g.fillStyle = 'rgba(225,225,235,0.75)'; g.font = `400 24px ${F}`;
712
712
  (linhas || []).slice(0, 3).forEach((l, i) => g.fillText('· ' + String(l).slice(0, 34), 34, 178 + i * 36));
713
713
  g.fillStyle = '#ff6a2b'; g.font = `500 22px ${F}`; g.fillText(`● trabalhando · ${30 + k * 7}:${String(10 + k * 13).slice(0, 2)}`, 34, 312);
714
714
  g.fillStyle = 'rgba(255,255,255,0.06)'; g.beginPath(); g.roundRect(34, 336, 572, 40, 12); g.fill();
715
- g.fillStyle = 'rgba(225,225,235,0.45)'; g.font = `400 20px ${F}`; g.fillText('Pergunte ao Claude…', 50, 363);
715
+ g.fillStyle = 'rgba(225,225,235,0.45)'; g.font = `400 20px ${F}`; g.fillText('Peça qualquer coisa…', 50, 363);
716
716
  return cv;
717
717
  }
718
718
  function CartoesDeUso({ paineis = [], t }) {
@@ -831,6 +831,15 @@ const ESP = {
831
831
  macaco: { forma: 4, corpo: [0.36, 0.42, 0.42], perna: 0.36, cab: 0.36, foc: [0.22, 0.14, 0.08], orelha: 'redonda', rabo: 'longo', cor: '#7a5232', cor2: '#e6c9a0', focCor: '#e6c9a0' },
832
832
  };
833
833
 
834
+ /** Onde a cabeça do quatro-patas fica no chão, a partir do centro do bicho:
835
+ * o corpo vira 0,55 de lado e a cabeça vai na frente dele (o Quadrupede).
836
+ * A câmera mirava o meio do corpo e o rosto saía cortado na borda (13/10). */
837
+ export function frenteDoBicho(especie) {
838
+ const e = ESP[especie] || ESP.cachorro;
839
+ if (e.forma === 'ave' || e.forma === 'pinguim' || e.forma === 'peixe' || e.forma === 'dino') return { giro: 0, dist: 0 };
840
+ return { giro: 0.55, dist: e.corpo[2] / 2 + e.cab * 0.25 + (e.pescoco || 0) * 0.25 };
841
+ }
842
+
834
843
  /** A altura da cabeça (para a câmera mirar no rosto do bicho). */
835
844
  export function alturaDoBicho(especie) {
836
845
  const e = ESP[especie] || ESP.cachorro;
@@ -30,7 +30,7 @@ import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPa
30
30
  import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
31
31
  import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
32
32
  import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
33
- import { Mundo, Animal, Coisa, alturaDoBicho, useIcones } from './mundo.jsx';
33
+ import { Mundo, Animal, Coisa, alturaDoBicho, frenteDoBicho, useIcones } from './mundo.jsx';
34
34
 
35
35
  const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
36
36
  const lerp = (a, b, k) => a + (b - a) * k;
@@ -267,7 +267,8 @@ function RostoPixel({ p, est }) {
267
267
  "olha como quero o 3D": o rosto da referência é LOW-POLY — uma esfera de
268
268
  poucas faces com sombreado chapado, nariz de pirâmide, sobrancelha grossa,
269
269
  o cabelo cacheado feito de dezenas de pedrinhas facetadas. O corpo segue
270
- de blocos. `cabeca: 'cubo'` volta ao rosto de pixel da 10.5. */
270
+ de blocos. Desde 13/10 só sai com `cabeca: 'facetada'`: o padrão voltou a
271
+ ser o cubo com rosto de pixel da 10.5 (partitura sem o campo também). */
271
272
  const GEO_FACETA = new Map();
272
273
  function faceta(det) {
273
274
  if (!GEO_FACETA.has(det)) GEO_FACETA.set(det, new THREE.IcosahedronGeometry(1, det));
@@ -289,18 +290,32 @@ function CabeloFacetado({ estilo, cor }) {
289
290
  const u = hash(i * 3.7 + 1), v = hash(i * 9.1 + 4);
290
291
  const az = u * Math.PI * 2;
291
292
  const el = estilo === 'longo' ? -0.9 + v * 2.2 : 0.15 + v * 1.25;
292
- const atras = Math.cos(az) < 0.35 || el > 0.75;
293
- if (!atras) continue;
293
+ /* O rosto olha para +z (fz = 0.335 na CabecaFacetada). Até a 10.7 o
294
+ filtro guardava cos(az) < 0.35 — z positivo, a FRENTE — e os cachos
295
+ caíam no olho e na bochecha (12/10). Agora a pedra fica atrás da linha
296
+ das orelhas, ou no alto da cabeça, acima da testa. */
297
+ const z = -Math.cos(az) * Math.cos(el) * 0.34;
298
+ const y = 0.04 + Math.sin(el) * 0.38;
299
+ if (z > 0.05 && y < 0.3) continue;
300
+ if (z > 0.18) continue;
294
301
  const r = estilo === 'cacheado' ? 0.105 + hash(i) * 0.05 : 0.09 + hash(i) * 0.03;
295
- out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, 0.04 + Math.sin(el) * 0.38, -Math.cos(az) * Math.cos(el) * 0.34 + (Math.cos(az) > 0 ? 0.0 : -0.02)], r, k: i });
302
+ out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, y, z - 0.02], r, k: i });
296
303
  }
297
- if (estilo === 'coque') out.push({ p: [0, 0.36, -0.3], r: 0.16, k: 99 });
304
+ // O coque no alto e um pouco para trás: visível de frente (atrás, sumia e virava cabelo curto).
305
+ if (estilo === 'coque') out.push({ p: [0, 0.46, -0.12], r: 0.17, k: 99 });
298
306
  return out;
299
307
  }, [estilo]);
300
308
  // A franja: a frente do cabelo, um pouco acima da sobrancelha.
301
309
  return (
302
310
  <group>
303
311
  {estilo !== 'careca' && <Faceta p={[0, 0.2, -0.02]} s={[0.36, 0.26, 0.36]} c={cor} det={1} rough={0.9} />}
312
+ {/* O LONGO cai pelos lados até o ombro, por FORA da cabeça — as pedrinhas
313
+ ficavam dentro do crânio e, de frente, ela parecia de cabelo curto (12/10). */}
314
+ {estilo === 'longo' && <>
315
+ <Faceta p={[-0.33, -0.1, -0.04]} s={[0.11, 0.34, 0.24]} c={cor} det={1} rough={0.9} />
316
+ <Faceta p={[0.33, -0.1, -0.04]} s={[0.11, 0.34, 0.24]} c={cor} det={1} rough={0.9} />
317
+ <Faceta p={[0, -0.08, -0.26]} s={[0.34, 0.38, 0.13]} c={cor} det={1} rough={0.9} />
318
+ </>}
304
319
  {pedras.map((x) => <Faceta key={x.k} p={x.p} s={[x.r, x.r * 0.9, x.r]} rot={[hash(x.k) * 3, hash(x.k + 1) * 3, 0]} c={escurecer(cor, 0.85 + hash(x.k + 2) * 0.3)} det={0} rough={0.95} />)}
305
320
  </group>
306
321
  );
@@ -418,7 +433,7 @@ function PessoaBloco({ p, est }) {
418
433
  <BracoBloco lado={-1} g={est.gesto[0]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
419
434
  <BracoBloco lado={1} g={est.gesto[1]} cor={p.roupa} pele={p.pele} t={est.t} acao={a} k={ka} />
420
435
  <group position={[0, 1.8 + resp, 0]} rotation={[cp, cy, cr]}>
421
- {p.cabeca === 'cubo' ? (
436
+ {p.cabeca !== 'facetada' ? (
422
437
  <>
423
438
  <Bloco s={[CAB, CAB, CAB]} c={p.pele} r={0.04} rough={0.6} />
424
439
  <RostoPixel p={p} est={est} />
@@ -599,21 +614,44 @@ function alturaDaCabeca(p) {
599
614
  return 1.8;
600
615
  }
601
616
  /* Os figurantes ficam nas pontas do arco, um pouco atrás: estão na cena, mas não roubam o plano. */
602
- function lugarDoFigurante(el, fig, j) {
617
+ function lugarDoFigurante(el, fig, j, vert = false) {
603
618
  const n = Math.max(1, el.length);
604
- const borda = n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
619
+ /* No vertical o quadro é estreito: na ponta do arco (1,15) o gato pedido
620
+ ficava fora da tela o vídeo inteiro (12/10). Vem para perto, aos pés. */
621
+ const borda = vert ? (n === 1 ? 0.62 : n === 2 ? 1.1 : 1.6) : n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
605
622
  const lado = j % 2 ? 1 : -1;
606
623
  const baixo = fig.tipo === 'animal' || fig.tipo === 'coisa';
607
624
  return [lado * (borda + Math.floor(j / 2) * 0.9), 0, baixo ? 0.45 : -0.5];
608
625
  }
609
626
 
627
+ /* A CÂMERA NÃO ATRAVESSA NINGUÉM (13/10): no close de quem fala, outro do
628
+ elenco ficava entre a lente e o rosto, ou com a lente dentro dele (o close
629
+ do cachorro com o braço do outro cortando o quadro). `bate` diz se alguém
630
+ do elenco, um cilindro do chão até acima da cabeça, corta o caminho. */
631
+ function bate(el, quem, pos, alvo) {
632
+ const dx = alvo[0] - pos[0], dz = alvo[2] - pos[2], L2 = dx * dx + dz * dz || 1;
633
+ for (let j = 0; j < el.length; j++) {
634
+ if (j === quem) continue;
635
+ const [x, , z] = lugarDe(el, j);
636
+ const u = clamp(((x - pos[0]) * dx + (z - pos[2]) * dz) / L2);
637
+ const py = pos[1] + (alvo[1] - pos[1]) * u;
638
+ if (Math.hypot(pos[0] + dx * u - x, pos[2] + dz * u - z) < 0.55 && py < alturaDaCabeca(el[j]) + 0.3) return true;
639
+ }
640
+ return false;
641
+ }
642
+
610
643
  function cameraDe(pl, t, part, aspecto) {
611
644
  const vert = aspecto < 1;
612
645
  const el = part.elenco;
613
646
  const dois = el.length > 1;
614
647
  const quem = el[pl.alvo] ? pl.alvo : 0;
615
- const cabeca = (i) => { const p = lugarDe(el, i); return [p[0], alturaDaCabeca(el[i]), p[2]]; };
616
- const frente = (i) => [Math.sin(giroDe(el, i)), 0, Math.cos(giroDe(el, i))];
648
+ // O bicho de quatro patas tem a cabeça NA FRENTE do corpo, e o rosto virado para onde o corpo vira.
649
+ const doBicho = (i) => (el[i] && el[i].tipo === 'animal' ? frenteDoBicho(el[i].especie) : { giro: 0, dist: 0 });
650
+ const cabeca = (i) => {
651
+ const p = lugarDe(el, i), b = doBicho(i), g = giroDe(el, i) + b.giro;
652
+ return [p[0] + Math.sin(g) * b.dist, alturaDaCabeca(el[i]), p[2] + Math.cos(g) * b.dist];
653
+ };
654
+ const frente = (i) => { const g = giroDe(el, i) + doBicho(i).giro * 0.7; return [Math.sin(g), 0, Math.cos(g)]; };
617
655
  const k = clamp((t - pl.de) / Math.max(0.5, pl.ate - pl.de));
618
656
  const fovK = vert ? 1.45 : 1;
619
657
  let pos, alvo, fov;
@@ -642,11 +680,16 @@ function cameraDe(pl, t, part, aspecto) {
642
680
  const dist = (pl.tipo === 'detalhe' ? 1.25 : pl.tipo === 'close' ? 1.7 : 2.7) * (vert ? 1.25 : 1);
643
681
  fov = (pl.tipo === 'detalhe' ? 30 : pl.tipo === 'close' ? 32 : 34) * fovK;
644
682
  const h = cabeca(quem), f = frente(quem);
645
- const lado = pl.lado * 0.42;
646
- const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
647
683
  const baixo = pl.tipo === 'medio' ? 0.05 : -0.32;
648
- pos = [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
649
684
  alvo = [h[0], h[1] + (pl.tipo === 'medio' ? -0.35 : -0.02), h[2]];
685
+ const de = (ld) => {
686
+ const lado = ld * 0.42;
687
+ const dir = [f[0] * Math.cos(lado) - f[2] * Math.sin(lado), 0, f[0] * Math.sin(lado) + f[2] * Math.cos(lado)];
688
+ return [h[0] + dir[0] * dist, h[1] + baixo, h[2] + dir[2] * dist];
689
+ };
690
+ // O lado pedido; se alguém corta, o outro lado, de frente, mais aberto; senão, a câmera sobe por cima.
691
+ pos = [pl.lado, -pl.lado, 0, 1.6, -1.6].map(de).find((q) => !bate(el, quem, q, alvo));
692
+ if (!pos) { const q = de(pl.lado); pos = [q[0], q[1] + 0.9, q[2]]; }
650
693
  }
651
694
  // No vertical a legenda está no alto: a câmera mira acima, e o rosto desce no quadro.
652
695
  if (vert) alvo = [alvo[0], alvo[1] + (pl.tipo === 'aberto' || pl.tipo === 'largo' ? 0.45 : pl.tipo === 'medio' ? 0.3 : pl.tipo === 'ombro' ? 0.22 : 0.17), alvo[2]];
@@ -836,7 +879,7 @@ export function VoxelCena({ el, t }) {
836
879
  </group>
837
880
  ))}
838
881
  {figurantes.map((p, j) => (
839
- <group key={'f' + j} position={lugarDoFigurante(elenco, p, j)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
882
+ <group key={'f' + j} position={lugarDoFigurante(elenco, p, j, W < H)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
840
883
  {desenhar(p, estado(10 + j, true))}
841
884
  </group>
842
885
  ))}
@@ -29,6 +29,7 @@
29
29
 
30
30
  const { volumesDaTrilha } = require('./ritmo.js');
31
31
  const mundo = require('./mundo.js');
32
+ const falantes = require('./falantes.js');
32
33
  const colecao = require('./colecao.js');
33
34
 
34
35
  const FPS = 30;
@@ -110,6 +111,23 @@ function tipoDe(nome, pedido = {}) {
110
111
  return 'pessoa';
111
112
  }
112
113
 
114
+ /* QUEM APRESENTA TEM O GÊNERO DA VOZ (12/10: "Narradora: a Júlia" saiu com
115
+ voz de mulher e boneco de homem). O pedido manda primeiro ("narradora",
116
+ "apresentadora", o nome dela), depois a voz escolhida. */
117
+ const VOZES_F = /\b(kore|aoede|leda|zephyr|thalita|francisca|brenda|elza|giovanna|leila|leticia|manuela|yara|camila|vitoria)/i;
118
+ const VOZES_M = /\b(charon|puck|orus|fenrir|antonio|donato|fabio|humberto|julio|nicolau|valerio)/i;
119
+ function generoDoApresentador(roteiro = {}) {
120
+ const pedido = mundo.sem(roteiro.pedido || '');
121
+ if (/\b(narradora|apresentadora|locutora)\b/.test(pedido)) return 'f';
122
+ if (/\b(narrador|apresentador|locutor)\b/.test(pedido)) return 'm';
123
+ const voz = String(roteiro.voz || '');
124
+ if (VOZES_F.test(voz)) return 'f';
125
+ if (VOZES_M.test(voz)) return 'm';
126
+ return null;
127
+ }
128
+ const CABELOS_F = ['longo']; // o coque e o cacheado, de frente, liam como homem (12/10); só quando pedidos
129
+ const CABELOS_M = ['curto', 'topete', 'careca', 'cacheado'];
130
+
113
131
  function aparencia(nome, pedido = {}) {
114
132
  const k = (s) => hashDe(nome + ':' + s);
115
133
  const tipo = tipoDe(nome, pedido);
@@ -128,14 +146,18 @@ function aparencia(nome, pedido = {}) {
128
146
  return {
129
147
  nome, rotulo, tipo,
130
148
  pele: pedido.pele || pegar(PELES, k('pele')),
131
- cabelo: CABELOS.includes(pedido.cabelo) ? pedido.cabelo : pegar(CABELOS, k('cabelo')),
149
+ cabelo: CABELOS.includes(pedido.cabelo) ? pedido.cabelo
150
+ : pegar(pedido.genero === 'f' ? CABELOS_F : pedido.genero === 'm' ? CABELOS_M : CABELOS, k('cabelo')),
132
151
  corCabelo: pedido.corCabelo || pegar(CORES_CABELO, k('corCabelo')),
133
152
  roupa: pedido.roupa || pegar(ROUPAS, k('roupa')),
134
153
  oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
135
- barba: !!pedido.barba, // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
136
- bigode: !!pedido.bigode,
137
- // A cabeça facetada (low-poly) é o padrão; 'cubo' é o rosto de pixel da 10.5.
138
- ...(pedido.cabeca === 'cubo' ? { cabeca: 'cubo' } : {}),
154
+ barba: !!pedido.barba && pedido.genero !== 'f', // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
155
+ bigode: !!pedido.bigode && pedido.genero !== 'f',
156
+ ...(pedido.genero ? { genero: pedido.genero } : {}),
157
+ /* O cubo com rosto de pixel (o da 10.5) é o padrão de novo: o low-poly
158
+ da 10.6 ficou "feito com polígonos, bugado e horrível" (Joel, 13/10).
159
+ A facetada só sai pedida. */
160
+ cabeca: pedido.cabeca === 'facetada' ? 'facetada' : 'cubo',
139
161
  };
140
162
  }
141
163
 
@@ -316,7 +338,13 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
316
338
  return bicho ? { ...dado, tipo: 'animal', especie: bicho[1] } : dado;
317
339
  };
318
340
  // Até QUATRO falam no set ("três amigos no parque"); do quinto em diante dividem o lugar.
319
- const elenco = nomes.slice(0, 4).map((n, i) => ({ ...aparencia(n, pedidoDe(n)), lado: nomes.length > 1 ? (i < nomes.length / 2 ? -1 : 1) : 0, ordem: i }));
341
+ const comGenero = (n) => {
342
+ const d = pedidoDe(n);
343
+ if (d.genero) return d;
344
+ const g = n === 'apresentador' ? generoDoApresentador(roteiro) : falantes.generoDe(n, d);
345
+ return g === 'f' || g === 'm' ? { ...d, genero: g } : d;
346
+ };
347
+ const elenco = nomes.slice(0, 4).map((n, i) => ({ ...aparencia(n, comGenero(n)), lado: nomes.length > 1 ? (i < nomes.length / 2 ? -1 : 1) : 0, ordem: i }));
320
348
  const quemDe = (pt) => {
321
349
  const f = typeof pt.falante === 'string' && pt.falante.trim() ? pt.falante.trim().toLowerCase() : 'apresentador';
322
350
  return nomes.indexOf(f) % Math.max(1, elenco.length);
@@ -453,4 +481,4 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
453
481
  };
454
482
  }
455
483
 
456
- module.exports = { calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
484
+ module.exports = { generoDoApresentador, calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
package/lib/nuvem.js CHANGED
@@ -24,13 +24,12 @@
24
24
  * A alternativa era o servidor intermediar cada leitura e escrita, com uma
25
25
  * autorização própria. Seria um SEGUNDO modelo de permissão ao lado das
26
26
  * `firestore.rules`, e os dois divergiriam na primeira mudança delas. Aqui o
27
- * CLI troca o token do PrimoCode por uma sessão curta (custom token → token de
28
- * acesso) e usa a API REST: as regras do banco valem para o terminal
29
- * exatamente como valem para o app e para o celular.
27
+ * CLI usa o token de acesso da própria conta (lib/conta.js) na API REST: as
28
+ * regras do banco valem para o terminal exatamente como valem para o site.
30
29
  *
31
30
  * ── ZERO DEPENDÊNCIA ─────────────────────────────────────────────────────
32
31
  * Nada de `firebase-admin` nem do SDK web. O que se usa é o `crypto` e o
33
- * `https` do Node, contra a API REST do Firestore e a do Identity Toolkit.
32
+ * `https` do Node, contra a API REST do Firestore.
34
33
  */
35
34
 
36
35
  'use strict';
@@ -41,13 +40,12 @@ const http = require('http');
41
40
 
42
41
  const conta = require('./conta.js');
43
42
 
44
- /* Os dois endereços saem daqui em vez de estarem escritos no meio do código.
43
+ /* O endereço sai daqui em vez de estarem escritos no meio do código.
45
44
  Não é para trocar de nuvem: é para o teste conseguir pôr um Firestore de
46
45
  mentira no lugar e CONFERIR O QUE SOBE. Sem isso, a única forma de saber se
47
46
  um título vai em claro seria olhar o console do Firebase depois — que é
48
47
  exatamente tarde demais. */
49
48
  const FIRESTORE = process.env.PRIMOCODE_FIRESTORE || 'https://firestore.googleapis.com';
50
- const IDENTIDADE = process.env.PRIMOCODE_IDENTITY || 'https://identitytoolkit.googleapis.com';
51
49
 
52
50
  const VERSAO_CIFRA = 2; // o mesmo `v` do frontend/crypto.js
53
51
  const ITERACOES = 310000;
@@ -132,26 +130,17 @@ function httpsJson(url, metodo, corpo, cabecalhos = {}, timeout = 20000) {
132
130
 
133
131
  let _sessao = null; // { uid, idToken, expira, projeto }
134
132
 
135
- /* Abre (ou renova) a sessão. O token de acesso do Firebase dura uma hora;
136
- renovar cinco minutos antes evita a escrita que falha no meio de uma
137
- conversa longa — e falhar ali perde a mensagem, não só o pedido. */
133
+ /* Abre (ou renova) a sessão. O conta.sessao() já devolve o token de acesso
134
+ válido por mais 5 minutos no mínimo (renova sozinho): aqui só se guarda,
135
+ para não ler o plano a cada mensagem. Guardado por 4 minutos. */
138
136
  async function abrir() {
139
- if (_sessao && Date.now() < _sessao.expira - 300000) return _sessao;
137
+ if (_sessao && Date.now() < _sessao.expira) return _sessao;
140
138
  const s = await conta.sessao();
141
- if (!s.ok) return null;
142
- if (!s.apiKey || !s.projeto) {
143
- // O servidor não está configurado para isto (falta FIREBASE_API_KEY /
144
- // FIREBASE_PROJECT_ID no ambiente dele). Dizer é melhor que tentar e
145
- // falhar com "requisição inválida" mais adiante.
146
- return null;
147
- }
148
- const r = await httpsJson(
149
- `${IDENTIDADE}/v1/accounts:signInWithCustomToken?key=${s.apiKey}`,
150
- 'POST', { token: s.customToken, returnSecureToken: true });
139
+ if (!s.ok || !s.idToken) return null;
151
140
  _sessao = {
152
141
  uid: s.uid, projeto: s.projeto, plano: s.plano, pro: s.pro,
153
- idToken: r.idToken,
154
- expira: Date.now() + (Number(r.expiresIn || 3600) * 1000),
142
+ idToken: s.idToken,
143
+ expira: Date.now() + 240000,
155
144
  };
156
145
  return _sessao;
157
146
  }
package/lib/planos.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * planos.js — a tela /planos do terminal.
3
+ *
4
+ * "eles vão ter que assinar um plano obrigatoriamente pra poder usar …
5
+ * tem que ser por mês" (Joel, 12/10/2026)
6
+ *
7
+ * A lista de verdade vem do servidor (GET /api/planos, lib/planos.js de lá),
8
+ * com o link de checkout de cada plano. A cópia abaixo só aparece quando o
9
+ * servidor não responde, para a pessoa nunca ver uma tela vazia — e sem
10
+ * link de compra, porque um preço velho com botão de pagar é pior que nenhum.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const https = require('https');
16
+ const http = require('http');
17
+
18
+ const RESERVA = [
19
+ { id: 'primo', nome: 'Primo', preco: 69.9, inclui: ['app e site sem configurar nada', 'modelos do servidor', 'vídeo em 720p'] },
20
+ { id: 'pro', nome: 'Primo Pro', preco: 129.9, inclui: ['limite alto', 'estúdio de vídeo completo, em 1080p', 'cenários salvos'] },
21
+ { id: 'max', nome: 'Primo Max', preco: 299.9, inclui: ['sem limite por hora', 'prioridade na fila', 'os melhores modelos', 'vídeos longos'] },
22
+ { id: 'studio', nome: 'Primo Studio', preco: 599.9, inclui: ['para agência e empresa', 'várias pessoas', 'marca própria nos vídeos'] },
23
+ ];
24
+
25
+ function buscarPlanos(servidor, { timeout = 8000 } = {}) {
26
+ return new Promise((resolve) => {
27
+ let url;
28
+ try { url = new URL('/api/planos', servidor); } catch { return resolve({ ok: false, planos: RESERVA }); }
29
+ const lib = url.protocol === 'https:' ? https : http;
30
+ const req = lib.get(url, { timeout }, (res) => {
31
+ let t = '';
32
+ res.setEncoding('utf8');
33
+ res.on('data', (c) => { t += c; });
34
+ res.on('end', () => {
35
+ try {
36
+ const j = JSON.parse(t);
37
+ const lista = Array.isArray(j) ? j : j && j.planos;
38
+ if (res.statusCode === 200 && Array.isArray(lista) && lista.length) return resolve({ ok: true, planos: lista });
39
+ } catch { /* cai na reserva */ }
40
+ resolve({ ok: false, planos: RESERVA });
41
+ });
42
+ });
43
+ req.on('timeout', () => req.destroy());
44
+ req.on('error', () => resolve({ ok: false, planos: RESERVA }));
45
+ });
46
+ }
47
+
48
+ const reais = (n) => `R$ ${Number(n).toFixed(2).replace('.', ',')}`;
49
+
50
+ /**
51
+ * As linhas da tela. `atual` é o id do plano da pessoa (ou null).
52
+ * @param {{planos:object[], ok:boolean}} r
53
+ * @param {{cores:object, atual?:string, site:string}} op
54
+ */
55
+ function desenhar({ planos, ok }, { cores: c, atual = null, site }) {
56
+ const linhas = ['', c.white(' Planos do PrimoCode') + c.dim(' · por mês, cancele quando quiser'), ''];
57
+ for (const p of planos) {
58
+ const meu = atual && p.id === atual;
59
+ linhas.push(' ' + (meu ? c.ok('● ') : ' ') + c.brand(p.nome.padEnd(14)) + c.white(reais(p.preco).padStart(11))
60
+ + c.dim(' /mês') + (meu ? c.ok(' ← o seu') : ''));
61
+ linhas.push(' ' + c.muted((p.inclui || []).join(' · ')));
62
+ if (ok && !meu) linhas.push(' ' + (p.checkout ? c.dim('assinar: ') + c.brand(p.checkout) : c.dim('checkout em breve')));
63
+ linhas.push('');
64
+ }
65
+ if (!ok) linhas.push(c.dim(' (sem resposta do servidor: preços de referência, sem link de compra)'));
66
+ linhas.push(c.dim(' Também em ') + c.brand(`${site}/planos`) + c.dim(' · já assinou? ') + c.brand('/account') + c.dim(' atualiza.'));
67
+ linhas.push('');
68
+ return linhas;
69
+ }
70
+
71
+ module.exports = { buscarPlanos, desenhar, RESERVA, reais };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "10.7.0",
3
+ "version": "10.8.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 && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.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 && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.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/planos-tela.test.js && 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/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/assinatura.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 && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"