primocode 10.6.0 → 10.7.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.
@@ -67,6 +67,8 @@ const lancamento = require('./lancamento.js');
67
67
  const glifos = require('./glifos.js');
68
68
  const personagem = require('./personagem.js');
69
69
  const mundo = require('./mundo.js');
70
+ const cenarios = require('./cenarios.js');
71
+ const movimentos = require('./movimentos.js');
70
72
  const colecao = require('./colecao.js');
71
73
  const { escolherEstilo, LEVES } = require('./estilo.js');
72
74
  const sonsLeves = require('./sons-leves.js');
@@ -277,6 +279,28 @@ function projetoRecente() {
277
279
  return lista[0] || null;
278
280
  }
279
281
 
282
+ /* Sem id, o projeto é o que acabou de ser criado. Visto em 12/10: o agente
283
+ criou o projeto, chamou estudio_narrar sem id e o vídeo morreu ali. Só vale
284
+ para chamada SEM id (id errado continua sendo erro) e para projeto de até
285
+ 30 minutos, como no estudio_plano. */
286
+ function projetoOuRecente(id) {
287
+ if (id != null && String(id).trim() !== '') return projeto.ler(id);
288
+ return projetoRecente();
289
+ }
290
+
291
+ /* roteiro.objetos: [{nome, src}] com o PNG DENTRO da pasta do projeto (o que
292
+ o estudio_midia baixou ou recortou). Caminho de fora some: o render só
293
+ enxerga a pasta do projeto, e a IA não escolhe arquivo da máquina. */
294
+ function objetosDoRoteiro(objetos, pasta) {
295
+ const raiz = path.resolve(pasta);
296
+ return (Array.isArray(objetos) ? objetos : []).map((o) => {
297
+ if (!o || typeof o.src !== 'string') return null;
298
+ const abs = path.resolve(raiz, o.src);
299
+ if (!abs.startsWith(raiz + path.sep) || !/\.png$/i.test(abs) || !fs.existsSync(abs)) return null;
300
+ return { nome: o.nome, src: path.relative(raiz, abs).replace(/\\/g, '/') };
301
+ }).filter(Boolean);
302
+ }
303
+
280
304
  /** O roteiro é utilizável como veio? (objeto ou JSON, com partes de tipo conhecido) */
281
305
  function roteiroValido(r) {
282
306
  let x = r;
@@ -527,6 +551,19 @@ async function ferramentaMidia(args) {
527
551
  /* A BIBLIOTECA (colecao.js, 11/10): o cardápio inteiro, o ícone 3D e a
528
552
  animação pronta. Procurar não usa rede (o índice mora no pacote); com
529
553
  id, baixa para o projeto e devolve o elemento pronto. */
554
+ /* OS MOVIMENTOS (movimentos.js, 12/10): 100+ entradas, saídas, ênfases e loops com nome. */
555
+ if (acao === 'movimentos' || acao === 'movimento') {
556
+ return movimentos.procurar({ termo: args.termo, categoria: args.categoria, limite: Number(args.limite) || 30 });
557
+ }
558
+ /* OS CENÁRIOS (cenarios.js, 12/10): os lugares prontos + os que a IA criou
559
+ e a pessoa deixou guardar. Salvar só com o sim dela (autorizado:true). */
560
+ if (acao === 'cenarios' || acao === 'cenários') {
561
+ return { ok: true, prontos: Object.keys(mundo.LUGARES), salvos: cenarios.listar(),
562
+ comoCriar: 'na cena do estilo personagem: lugar (um pronto como base), ceu [cor de cima, cor do horizonte], chao {tipo, cor, cor2}, parede, luz, acento, hora e elementos. Ficou bom? Pergunte se pode salvar e use acao:"salvar_cenario".' };
563
+ }
564
+ if (acao === 'salvar_cenario' || acao === 'salvar_cenário') {
565
+ return cenarios.salvar({ nome: args.nome, cenario: args.cenario, autorizado: args.autorizado, bases: Object.keys(mundo.LUGARES) });
566
+ }
530
567
  if (acao === 'biblioteca' || acao === 'cardapio' || acao === 'cardápio') return colecao.cardapio();
531
568
  if (acao === 'icones3d' || acao === 'icone3d' || acao === 'animacoes' || acao === 'animacao' || acao === 'animação' || acao === 'animações') {
532
569
  const ehAnim = /^anima/.test(acao);
@@ -585,8 +622,9 @@ async function baixarEscolhido(lista, args, p) {
585
622
  async function ferramentaNarrar(args) {
586
623
  if (args.vozes) return rodarPython('narrar.py', ['vozes'], { timeout: 30000 });
587
624
 
588
- const p = projeto.ler(args.id);
625
+ const p = projetoOuRecente(args.id);
589
626
  if (!p) return semProjeto(args.id);
627
+ if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
590
628
  const texto = String(args.texto || '').trim();
591
629
  if (!texto) return { ok: false, error: 'não há texto para narrar.' };
592
630
 
@@ -616,8 +654,9 @@ async function ferramentaNarrar(args) {
616
654
 
617
655
  async function ferramentaVideo(args, ctx = {}) {
618
656
  const acao = String(args.acao || 'medir').toLowerCase();
619
- const p = projeto.ler(args.id);
657
+ const p = projetoOuRecente(args.id);
620
658
  if (!p) return semProjeto(args.id);
659
+ if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
621
660
 
622
661
  const pronto = video.conferir();
623
662
  if (!pronto.ok) return pronto;
@@ -1871,7 +1910,7 @@ async function prepararRoteiro(p, args) {
1871
1910
  return r;
1872
1911
  }
1873
1912
  if (estilo === 'personagem' && modo === 'video') {
1874
- const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
1913
+ const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt, voz: args.voz || roteiro.voz }, largura, altura, tempos, narracao, sons, trilha });
1875
1914
  if (r.ok) {
1876
1915
  /* O que não é peça do catálogo vira ícone 3D da biblioteca, em blocos
1877
1916
  (kit/mundo.jsx): "uma pizza que fala", "um violão na praia". Sem
@@ -1884,6 +1923,7 @@ async function prepararRoteiro(p, args) {
1884
1923
  if (b && b.ok) achados[nome] = b.src;
1885
1924
  }
1886
1925
  mundo.aplicarIcones(r.partitura, achados);
1926
+ mundo.adicionarObjetos(r.partitura, objetosDoRoteiro(roteiro.objetos, p.pasta));
1887
1927
  const icones = Object.keys(achados);
1888
1928
  projeto.anotar(p.id, 'plano', `estilo personagem: ${r.resumo.elenco.join(' e ')}${r.resumo.figurantes.length ? ` (+ ${r.resumo.figurantes.join(', ')})` : ''} em ${r.resumo.lugares.join(' → ')}, ${r.resumo.planos} planos, ${r.resumo.segundos}s${icones.length ? `; em blocos: ${icones.join(', ')}` : ''}`);
1889
1929
  }
@@ -1962,8 +2002,9 @@ async function transcrever(p, alvo, ctx) {
1962
2002
  // ── estudio_render ───────────────────────────────────────────────────────
1963
2003
 
1964
2004
  async function ferramentaRender(args, ctx = {}) {
1965
- const p = projeto.ler(args.id);
2005
+ const p = projetoOuRecente(args.id);
1966
2006
  if (!p) return semProjeto(args.id);
2007
+ if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
1967
2008
  const pl = projeto.lerPlano(args.id);
1968
2009
  if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
1969
2010
 
@@ -2209,7 +2250,7 @@ async function ferramentaSite(args) {
2209
2250
  }
2210
2251
 
2211
2252
  module.exports = {
2212
- vazamentoNoComeco, variarIcones,
2253
+ vazamentoNoComeco, variarIcones, objetosDoRoteiro,
2213
2254
  ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
2214
2255
  ferramentaVideo, ferramentaRender, ferramentaSite,
2215
2256
  projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
@@ -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 }) {
@@ -289,18 +289,32 @@ function CabeloFacetado({ estilo, cor }) {
289
289
  const u = hash(i * 3.7 + 1), v = hash(i * 9.1 + 4);
290
290
  const az = u * Math.PI * 2;
291
291
  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;
292
+ /* O rosto olha para +z (fz = 0.335 na CabecaFacetada). Até a 10.7 o
293
+ filtro guardava cos(az) < 0.35 — z positivo, a FRENTE — e os cachos
294
+ caíam no olho e na bochecha (12/10). Agora a pedra fica atrás da linha
295
+ das orelhas, ou no alto da cabeça, acima da testa. */
296
+ const z = -Math.cos(az) * Math.cos(el) * 0.34;
297
+ const y = 0.04 + Math.sin(el) * 0.38;
298
+ if (z > 0.05 && y < 0.3) continue;
299
+ if (z > 0.18) continue;
294
300
  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 });
301
+ out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, y, z - 0.02], r, k: i });
296
302
  }
297
- if (estilo === 'coque') out.push({ p: [0, 0.36, -0.3], r: 0.16, k: 99 });
303
+ // O coque no alto e um pouco para trás: visível de frente (atrás, sumia e virava cabelo curto).
304
+ if (estilo === 'coque') out.push({ p: [0, 0.46, -0.12], r: 0.17, k: 99 });
298
305
  return out;
299
306
  }, [estilo]);
300
307
  // A franja: a frente do cabelo, um pouco acima da sobrancelha.
301
308
  return (
302
309
  <group>
303
310
  {estilo !== 'careca' && <Faceta p={[0, 0.2, -0.02]} s={[0.36, 0.26, 0.36]} c={cor} det={1} rough={0.9} />}
311
+ {/* O LONGO cai pelos lados até o ombro, por FORA da cabeça — as pedrinhas
312
+ ficavam dentro do crânio e, de frente, ela parecia de cabelo curto (12/10). */}
313
+ {estilo === 'longo' && <>
314
+ <Faceta p={[-0.33, -0.1, -0.04]} s={[0.11, 0.34, 0.24]} c={cor} det={1} rough={0.9} />
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, -0.08, -0.26]} s={[0.34, 0.38, 0.13]} c={cor} det={1} rough={0.9} />
317
+ </>}
304
318
  {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
319
  </group>
306
320
  );
@@ -599,9 +613,11 @@ function alturaDaCabeca(p) {
599
613
  return 1.8;
600
614
  }
601
615
  /* 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) {
616
+ function lugarDoFigurante(el, fig, j, vert = false) {
603
617
  const n = Math.max(1, el.length);
604
- const borda = n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
618
+ /* No vertical o quadro é estreito: na ponta do arco (1,15) o gato pedido
619
+ ficava fora da tela o vídeo inteiro (12/10). Vem para perto, aos pés. */
620
+ const borda = vert ? (n === 1 ? 0.62 : n === 2 ? 1.1 : 1.6) : n === 1 ? 1.15 : n === 2 ? 1.75 : 2.5;
605
621
  const lado = j % 2 ? 1 : -1;
606
622
  const baixo = fig.tipo === 'animal' || fig.tipo === 'coisa';
607
623
  return [lado * (borda + Math.floor(j / 2) * 0.9), 0, baixo ? 0.45 : -0.5];
@@ -836,7 +852,7 @@ export function VoxelCena({ el, t }) {
836
852
  </group>
837
853
  ))}
838
854
  {figurantes.map((p, j) => (
839
- <group key={'f' + j} position={lugarDoFigurante(elenco, p, j)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
855
+ <group key={'f' + j} position={lugarDoFigurante(elenco, p, j, W < H)} rotation={[0, (j % 2 ? -1 : 1) * 0.5, 0]}>
840
856
  {desenhar(p, estado(10 + j, true))}
841
857
  </group>
842
858
  ))}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * movimentos.js — a biblioteca de MOVIMENTOS prontos: entradas, saídas,
3
+ * ênfases e loops, cada um com nome, que viram quadros-chave do `anima`.
4
+ *
5
+ * "preciso que você deixe os motions com + de 100 tipos de animações …
6
+ * além da IA poder criar os próprios e pesquisar" (Joel, 12/10)
7
+ *
8
+ * Até aqui todo movimento era escrito à mão pelo modelo, quadro a quadro
9
+ * (`anima: {opacidade: [{t, v}]}`). Modelo grátis escreve sempre os mesmos
10
+ * dois: esmaecer e subir. Com nome, ele PROCURA ("quique", "desfoque",
11
+ * "saída para a esquerda"), escolhe e escreve uma palavra:
12
+ *
13
+ * elemento.movimento = "quicar-de-baixo"
14
+ * elemento.movimento = {nome: "sair-girando", em: 2.4, duracao: 0.5}
15
+ * elemento.movimento = ["surgir-desfocado", {nome: "flutuar", em: 0.6}]
16
+ *
17
+ * O plano expande isso em `anima` antes de conferir (plano.js). O que a IA
18
+ * escreveu em `anima` vence o movimento pronto na mesma propriedade — e ela
19
+ * continua livre para inventar o seu, quadro a quadro.
20
+ *
21
+ * Os números: x/y/z são DESLOCAMENTOS em px (o render soma à posição), as
22
+ * rotações em graus, o desfoque em px, opacidade/escala/brilho/saturação em
23
+ * fração. Todo movimento de entrada termina no repouso (0 de deslocamento,
24
+ * escala 1, opacidade 1); toda saída começa nele.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ // Cada quadro: [fração do tempo 0..1, valor, suavização até ele].
30
+ const DIRECOES = {
31
+ 'de-baixo': { eixo: 'y', sinal: 1 }, 'de-cima': { eixo: 'y', sinal: -1 },
32
+ 'da-esquerda': { eixo: 'x', sinal: -1 }, 'da-direita': { eixo: 'x', sinal: 1 },
33
+ };
34
+ const PARA = { 'de-baixo': 'para-baixo', 'de-cima': 'para-cima', 'da-esquerda': 'para-a-esquerda', 'da-direita': 'para-a-direita' };
35
+
36
+ /* As entradas com direção: cada estilo × as quatro direções. */
37
+ const ESTILOS_DIRECAO = {
38
+ deslizar: { d: 0.6, desc: 'desliza e aparece', f: (e, s) => ({ [e]: [[0, 80 * s], [1, 0, 'saida']], opacidade: [[0, 0], [0.6, 1, 'suave']] }) },
39
+ 'deslizar-desfocado': { d: 0.7, desc: 'desliza saindo do desfoque', f: (e, s) => ({ [e]: [[0, 70 * s], [1, 0, 'saida']], opacidade: [[0, 0], [0.5, 1]], desfoque: [[0, 18], [1, 0, 'saida']] }) },
40
+ 'chegar-elastico': { d: 0.9, desc: 'chega passando do ponto e volta, como mola', f: (e, s) => ({ [e]: [[0, 220 * s], [1, 0, 'elastico']], opacidade: [[0, 0], [0.25, 1]] }) },
41
+ quicar: { d: 0.9, desc: 'cai e quica', f: (e, s) => ({ [e]: [[0, 260 * s], [1, 0, 'salto']], opacidade: [[0, 0], [0.2, 1]] }) },
42
+ varrer: { d: 0.8, desc: 'atravessa rápido de longe e freia', f: (e, s) => ({ [e]: [[0, 900 * s], [1, 0, 'arrasto']], opacidade: [[0, 0], [0.3, 1]] }) },
43
+ 'girar-entrando': { d: 0.8, desc: 'entra girando', f: (e, s) => ({ [e]: [[0, 160 * s], [1, 0, 'saida']], rotacao: [[0, -90 * s], [1, 0, 'saida']], opacidade: [[0, 0], [0.4, 1]] }) },
44
+ };
45
+
46
+ /* As entradas sem direção. */
47
+ const ENTRADAS = {
48
+ surgir: { d: 0.5, desc: 'só aparece (esmaecer)', f: () => ({ opacidade: [[0, 0], [1, 1, 'suave']] }) },
49
+ crescer: { d: 0.6, desc: 'cresce do nada', f: () => ({ escala: [[0, 0], [1, 1, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
50
+ 'crescer-elastico': { d: 0.9, desc: 'cresce como mola', f: () => ({ escala: [[0, 0], [1, 1, 'elastico']] }) },
51
+ 'pular-pra-dentro': { d: 0.6, desc: 'cresce, passa um pouco e assenta', f: () => ({ escala: [[0, 0], [0.7, 1.15, 'saida'], [1, 1, 'suave']], opacidade: [[0, 0], [0.3, 1]] }) },
52
+ 'encolher-pra-dentro': { d: 0.6, desc: 'vem grande e encolhe até o lugar', f: () => ({ escala: [[0, 1.8], [1, 1, 'saida']], opacidade: [[0, 0], [0.5, 1]] }) },
53
+ 'surgir-desfocado': { d: 0.7, desc: 'sai do desfoque', f: () => ({ desfoque: [[0, 24], [1, 0, 'saida']], opacidade: [[0, 0], [0.6, 1]] }) },
54
+ 'virar-de-lado': { d: 0.7, desc: 'vira em Y como carta', f: () => ({ rotacaoY: [[0, 90], [1, 0, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
55
+ 'virar-de-cima': { d: 0.7, desc: 'tomba em X, de cima para baixo', f: () => ({ rotacaoX: [[0, -90], [1, 0, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
56
+ 'girar-crescendo': { d: 0.8, desc: 'cresce dando meia volta', f: () => ({ rotacao: [[0, -180], [1, 0, 'saida']], escala: [[0, 0], [1, 1, 'saida']] }) },
57
+ 'vir-do-fundo': { d: 0.8, desc: 'avança da profundidade (3D)', f: () => ({ z: [[0, -700], [1, 0, 'saida']], opacidade: [[0, 0], [0.4, 1]] }) },
58
+ 'cair-do-alto': { d: 0.8, desc: 'cai do topo e assenta com um quique', f: () => ({ y: [[0, -500], [1, 0, 'salto']], rotacao: [[0, -12], [1, 0, 'saida']] }) },
59
+ 'estourar-de-luz': { d: 0.6, desc: 'acende estourado e assenta', f: () => ({ brilho: [[0, 3], [1, 1, 'saida']], opacidade: [[0, 0], [0.3, 1]] }) },
60
+ 'ganhar-cor': { d: 0.8, desc: 'sai do cinza para a cor', f: () => ({ saturacao: [[0, 0], [1, 1, 'suave']], opacidade: [[0, 0], [0.4, 1]] }) },
61
+ 'abrir-letras': { d: 0.8, desc: 'as letras chegam juntas de longe (texto)', f: () => ({ espacamento: [[0, 40], [1, 0, 'saida']], opacidade: [[0, 0], [0.5, 1]] }) },
62
+ 'esticar-de-lado': { d: 0.6, desc: 'estica na horizontal', f: () => ({ escalaX: [[0, 0], [1, 1, 'saida']] }) },
63
+ 'esticar-de-pe': { d: 0.6, desc: 'estica na vertical', f: () => ({ escalaY: [[0, 0], [1, 1, 'saida']] }) },
64
+ 'carimbo': { d: 0.35, desc: 'bate na tela como carimbo', f: () => ({ escala: [[0, 2.4], [1, 1, 'entrada']], opacidade: [[0, 0], [0.2, 1]] }) },
65
+ 'aproximar-editorial': { d: 0.9, desc: 'entra devagar, como revista', f: () => ({ y: [[0, 24], [1, 0, 'editorial']], escala: [[0, 0.96], [1, 1, 'editorial']], opacidade: [[0, 0], [1, 1, 'editorial']] }) },
66
+ };
67
+
68
+ /* As ênfases: o elemento já está lá e chama atenção (começam e terminam no repouso). */
69
+ const ENFASES = {
70
+ pulsar: { d: 0.5, desc: 'cresce e volta uma vez', f: () => ({ escala: [[0, 1], [0.5, 1.12, 'suave'], [1, 1, 'suave']] }) },
71
+ tremer: { d: 0.5, desc: 'treme de lado (erro, negação)', f: () => ({ x: [[0, 0], [0.15, -14], [0.3, 12], [0.45, -10], [0.6, 8], [0.75, -4], [1, 0]] }) },
72
+ balancar: { d: 0.8, desc: 'balança como pêndulo', f: () => ({ rotacao: [[0, 0], [0.25, 12, 'suave'], [0.5, -8, 'suave'], [0.75, 4, 'suave'], [1, 0, 'suave']] }) },
73
+ piscar: { d: 0.6, desc: 'pisca duas vezes', f: () => ({ opacidade: [[0, 1], [0.25, 0.1], [0.5, 1], [0.75, 0.1], [1, 1]] }) },
74
+ 'dar-uma-volta': { d: 0.8, desc: 'gira 360°', f: () => ({ rotacao: [[0, 0], [1, 360, 'suave']] }) },
75
+ 'virar-moeda': { d: 0.8, desc: 'gira em Y como moeda', f: () => ({ rotacaoY: [[0, 0], [1, 360, 'suave']] }) },
76
+ 'pular': { d: 0.6, desc: 'dá um pulinho', f: () => ({ y: [[0, 0], [0.4, -40, 'saida'], [1, 0, 'salto']] }) },
77
+ 'tada': { d: 0.9, desc: 'cresce e chacoalha (conquista)', f: () => ({ escala: [[0, 1], [0.2, 0.9], [0.4, 1.12], [0.9, 1.12], [1, 1]], rotacao: [[0, 0], [0.3, -4], [0.45, 4], [0.6, -4], [0.75, 4], [1, 0]] }) },
78
+ 'borracha': { d: 0.8, desc: 'amassa e estica como borracha', f: () => ({ escalaX: [[0, 1], [0.3, 1.25], [0.4, 0.75], [0.5, 1.15], [0.65, 0.95], [1, 1]], escalaY: [[0, 1], [0.3, 0.75], [0.4, 1.25], [0.5, 0.85], [0.65, 1.05], [1, 1]] }) },
79
+ 'gelatina': { d: 0.9, desc: 'treme mole', f: () => ({ escalaX: [[0, 1], [0.2, 1.1], [0.4, 0.92], [0.6, 1.04], [0.8, 0.98], [1, 1]], escalaY: [[0, 1], [0.2, 0.92], [0.4, 1.08], [0.6, 0.97], [0.8, 1.01], [1, 1]] }) },
80
+ 'acenar': { d: 0.8, desc: 'balança curto, como aceno', f: () => ({ rotacao: [[0, 0], [0.2, 14], [0.4, -8], [0.6, 14], [0.8, -4], [1, 0]] }) },
81
+ 'acender': { d: 0.6, desc: 'brilha e apaga uma vez', f: () => ({ brilho: [[0, 1], [0.4, 1.8, 'suave'], [1, 1, 'suave']] }) },
82
+ 'inclinar-3d': { d: 0.9, desc: 'inclina em 3D e volta', f: () => ({ rotacaoX: [[0, 0], [0.5, 18, 'suave'], [1, 0, 'suave']], rotacaoY: [[0, 0], [0.5, -14, 'suave'], [1, 0, 'suave']] }) },
83
+ 'cabecear': { d: 0.6, desc: 'acena com a cabeça (sim)', f: () => ({ rotacaoX: [[0, 0], [0.3, 20], [0.6, -6], [1, 0]] }) },
84
+ 'empurrao': { d: 0.4, desc: 'leva um soco e volta', f: () => ({ escala: [[0, 1], [0.25, 0.86, 'entrada'], [1, 1, 'elastico']] }) },
85
+ 'sublinhar-luz': { d: 0.7, desc: 'fica vivo por um instante', f: () => ({ saturacao: [[0, 1], [0.4, 1.8, 'suave'], [1, 1, 'suave']], brilho: [[0, 1], [0.4, 1.3, 'suave'], [1, 1]] }) },
86
+ };
87
+
88
+ /* Os loops: repetem pela duração inteira (o padrão é a cena toda, 4 s). */
89
+ const LOOPS = {
90
+ flutuar: { d: 4, ciclo: 2.4, desc: 'sobe e desce devagar', f: () => ({ y: [[0, 0], [0.5, -16, 'suave'], [1, 0, 'suave']] }) },
91
+ respirar: { d: 4, ciclo: 2, desc: 'cresce e encolhe de leve', f: () => ({ escala: [[0, 1], [0.5, 1.05, 'suave'], [1, 1, 'suave']] }) },
92
+ 'girar-sem-parar': { d: 4, ciclo: 4, desc: 'gira sem parar', f: () => ({ rotacao: [[0, 0], [1, 360, 'linear']] }) },
93
+ 'oscilar': { d: 4, ciclo: 1.6, desc: 'balança de leve sem parar', f: () => ({ rotacao: [[0, -5], [0.5, 5, 'suave'], [1, -5, 'suave']] }) },
94
+ 'batimento': { d: 4, ciclo: 1, desc: 'pulsa como coração', f: () => ({ escala: [[0, 1], [0.15, 1.12], [0.3, 1], [0.45, 1.08], [0.6, 1], [1, 1]] }) },
95
+ 'piscar-devagar': { d: 4, ciclo: 1.6, desc: 'some e volta devagar', f: () => ({ opacidade: [[0, 1], [0.5, 0.35, 'suave'], [1, 1, 'suave']] }) },
96
+ 'orbitar': { d: 4, ciclo: 3, desc: 'anda em círculo pequeno', f: () => ({ x: [[0, 20], [0.25, 0, 'suave'], [0.5, -20, 'suave'], [0.75, 0, 'suave'], [1, 20, 'suave']], y: [[0, 0], [0.25, 20, 'suave'], [0.5, 0, 'suave'], [0.75, -20, 'suave'], [1, 0, 'suave']] }) },
97
+ 'deriva': { d: 4, ciclo: 4, desc: 'anda de lado bem devagar', f: () => ({ x: [[0, -12], [0.5, 12, 'suave'], [1, -12, 'suave']] }) },
98
+ 'girar-3d': { d: 4, ciclo: 5, desc: 'gira em Y sem parar, como vitrine', f: () => ({ rotacaoY: [[0, 0], [1, 360, 'linear']] }) },
99
+ 'brilho-vivo': { d: 4, ciclo: 2, desc: 'o brilho respira', f: () => ({ brilho: [[0, 1], [0.5, 1.25, 'suave'], [1, 1, 'suave']] }) },
100
+ 'balanco-3d': { d: 4, ciclo: 3, desc: 'inclina em 3D de um lado para o outro', f: () => ({ rotacaoY: [[0, -12], [0.5, 12, 'suave'], [1, -12, 'suave']] }) },
101
+ 'tremor-leve': { d: 4, ciclo: 0.3, desc: 'vibra o tempo todo (tensão, alerta)', f: () => ({ x: [[0, 0], [0.25, -2], [0.5, 2], [0.75, -1], [1, 0]] }) },
102
+ };
103
+
104
+ /* A SAÍDA é a entrada ao contrário: o mesmo desenho, tempo invertido. */
105
+ function inverter(gerado) {
106
+ const out = {};
107
+ for (const [prop, qs] of Object.entries(gerado)) {
108
+ const ordem = [...qs].reverse();
109
+ // A suavização de um quadro vale ATÉ ele; invertida, passa ao vizinho.
110
+ out[prop] = ordem.map(([f, v], i) => [1 - f, v, i > 0 ? inverterE(ordem[i - 1][2]) : undefined]);
111
+ }
112
+ return out;
113
+ }
114
+ const inverterE = (e) => ({ saida: 'entrada', entrada: 'saida', arrasto: 'acelera', acelera: 'arrasto', elastico: 'entrada', salto: 'entrada' }[e] || e);
115
+ const nomeDaSaida = (n) => n.replace(/^surgir/, 'sumir').replace(/^crescer/, 'encolher-sumindo').replace(/^chegar/, 'partir')
116
+ .replace(/^vir-do-fundo/, 'ir-pro-fundo').replace(/^cair-do-alto/, 'cair-pra-fora').replace(/^ganhar-cor/, 'perder-cor')
117
+ .replace(/^abrir-letras/, 'espalhar-letras').replace(/^estourar-de-luz/, 'apagar-estourando');
118
+
119
+ function montarCatalogo() {
120
+ const cat = {};
121
+ const pore = (nome, categoria, def, gerar) => { cat[nome] = { nome, categoria, descricao: def.desc, duracao: def.d, ciclo: def.ciclo, gerar }; };
122
+ for (const [estilo, def] of Object.entries(ESTILOS_DIRECAO)) {
123
+ for (const [dir, { eixo, sinal }] of Object.entries(DIRECOES)) {
124
+ const gerar = () => def.f(eixo, sinal);
125
+ pore(`${estilo}-${dir}`, 'entrada', { ...def, desc: `${def.desc}, vindo ${dir.replace('-', ' ')}` }, gerar);
126
+ pore(`${estilo}-${PARA[dir]}`.replace(/^deslizar-desfocado/, 'sair-desfocando').replace(/^deslizar/, 'sair-deslizando')
127
+ .replace(/^chegar-elastico/, 'sair-elastico').replace(/^quicar/, 'sair-quicando').replace(/^varrer/, 'sair-varrendo')
128
+ .replace(/^girar-entrando/, 'sair-girando'), 'saida', { ...def, desc: `sai: ${def.desc.split(',')[0]}, ${PARA[dir].replace(/-/g, ' ')}` },
129
+ () => inverter(def.f(eixo, sinal)));
130
+ }
131
+ }
132
+ for (const [nome, def] of Object.entries(ENTRADAS)) {
133
+ pore(nome, 'entrada', def, def.f);
134
+ pore(nomeDaSaida(nome) === nome ? `${nome}-saindo` : nomeDaSaida(nome), 'saida', { ...def, desc: `sai: ${def.desc}` }, () => inverter(def.f()));
135
+ }
136
+ for (const [nome, def] of Object.entries(ENFASES)) pore(nome, 'enfase', def, def.f);
137
+ for (const [nome, def] of Object.entries(LOOPS)) pore(nome, 'loop', def, def.f);
138
+ return cat;
139
+ }
140
+ const CATALOGO = montarCatalogo();
141
+
142
+ const r3 = (n) => Math.round(n * 1000) / 1000;
143
+ const semAcento = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
144
+
145
+ /** Os quadros-chave de um movimento, a partir de `em` segundos. */
146
+ const achar = (nome) => CATALOGO[semAcento(nome).trim().replace(/\s+/g, '-')] || null;
147
+
148
+ function quadros(nome, { em = 0, duracao } = {}) {
149
+ const m = achar(nome);
150
+ if (!m) return null;
151
+ const d = Number(duracao) > 0 ? Number(duracao) : m.duracao;
152
+ const gerado = m.gerar();
153
+ const anima = {};
154
+ if (m.categoria === 'loop') {
155
+ const ciclo = Math.min(m.ciclo || d, d);
156
+ const voltas = Math.max(1, Math.round(d / ciclo));
157
+ for (const [prop, qs] of Object.entries(gerado)) {
158
+ const lista = [];
159
+ for (let k = 0; k < voltas; k++) {
160
+ for (const [f, v, e] of qs) {
161
+ if (k > 0 && f === 0) continue; // o fim de uma volta é o começo da próxima
162
+ // o giro sem parar soma: a 2ª volta vai de 360 a 720
163
+ const soma = /rotacao/.test(prop) && qs[qs.length - 1][1] === 360 ? 360 * k : 0;
164
+ lista.push({ t: r3(em + (k + f) * ciclo), v: v + soma, ...(e ? { e } : {}) });
165
+ }
166
+ }
167
+ anima[prop] = lista;
168
+ }
169
+ return anima;
170
+ }
171
+ for (const [prop, qs] of Object.entries(gerado)) anima[prop] = qs.map(([f, v, e]) => ({ t: r3(em + f * d), v, ...(e ? { e } : {}) }));
172
+ return anima;
173
+ }
174
+
175
+ /** Procura por palavra (no nome ou na descrição) e por categoria. Sem termo, devolve um pouco de cada. */
176
+ function procurar({ termo = '', categoria = '', limite = 30 } = {}) {
177
+ const palavras = semAcento(termo).split(/[^a-z0-9]+/).filter(Boolean);
178
+ const cat = semAcento(categoria).replace('ê', 'e');
179
+ const lista = Object.values(CATALOGO).filter((m) => (!cat || m.categoria === cat.replace(/s$/, ''))
180
+ && palavras.every((p) => semAcento(m.nome + ' ' + m.descricao).includes(p)));
181
+ return {
182
+ ok: true, total: Object.keys(CATALOGO).length, achados: lista.length,
183
+ movimentos: lista.slice(0, limite).map((m) => ({ nome: m.nome, categoria: m.categoria, faz: m.descricao, duracao: m.duracao })),
184
+ comoUsar: 'no elemento do plano: movimento:"<nome>" ou {nome, em (s), duracao (s)} ou uma lista deles. O que você escrever em anima vence na mesma propriedade; pode inventar o seu em anima, quadro a quadro.',
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Troca `movimento` por `anima` em todos os elementos do plano. Muda o plano
190
+ * no lugar e devolve os nomes que não existem (o plano.js vira problema).
191
+ */
192
+ function expandir(plano) {
193
+ const desconhecidos = [];
194
+ for (const [i, cena] of ((plano && plano.cenas) || []).entries()) {
195
+ for (const [j, el] of ((cena && cena.elementos) || []).entries()) {
196
+ if (!el || el.movimento == null) continue;
197
+ const pedidos = (Array.isArray(el.movimento) ? el.movimento : [el.movimento]).map((m) => (typeof m === 'string' ? { nome: m } : m || {}));
198
+ const juntado = {};
199
+ for (const p of pedidos) {
200
+ const a = quadros(p.nome, { em: Number(p.em) || 0, duracao: p.duracao ?? ((achar(p.nome) || {}).categoria === 'loop' ? cena.duracao : undefined) });
201
+ if (!a) { desconhecidos.push({ onde: `cenas[${i}].elementos[${j}].movimento`, nome: String(p.nome) }); continue; }
202
+ // Dois movimentos na mesma propriedade: os quadros se somam na linha do tempo.
203
+ for (const [prop, qs] of Object.entries(a)) juntado[prop] = [...(juntado[prop] || []), ...qs].sort((x, y) => x.t - y.t);
204
+ }
205
+ el.anima = { ...juntado, ...(el.anima || {}) };
206
+ delete el.movimento;
207
+ }
208
+ }
209
+ return desconhecidos;
210
+ }
211
+
212
+ module.exports = { CATALOGO, quadros, procurar, expandir, inverter };
@@ -237,7 +237,11 @@ function posicionar(nomes, { semente = 1, agua = false, dentro = false, largura
237
237
  * pedido da pessoa: as coisas que ele cita entram na cena), semente, nElenco}
238
238
  */
239
239
  function mundoDe(pedido, { texto = '', pedidoTexto = '', semente = 1, nElenco = 1 } = {}) {
240
- const obj = pedido && typeof pedido === 'object' ? pedido : { lugar: String(pedido || '') };
240
+ let obj = pedido && typeof pedido === 'object' ? pedido : { lugar: String(pedido || '') };
241
+ /* Um cenário que a IA criou e a pessoa deixou salvar (cenarios.js): o
242
+ nome dele no lugar traz tudo de volta; o que a cena disser por cima vence. */
243
+ const salvo = require('./cenarios.js').ler(obj.cenario || obj.lugar);
244
+ if (salvo) obj = { ...salvo, ...obj, lugar: salvo.base, hora: obj.hora || salvo.hora, elementos: obj.elementos || salvo.elementos };
241
245
  const descr = [obj.lugar, obj.descricao, obj.hora].filter(Boolean).join(' ');
242
246
  let { lugar, hora } = lerLugar(descr);
243
247
  const nomeDireto = sem(obj.lugar).replace(/\s+/g, '_');
@@ -302,4 +306,23 @@ function aplicarIcones(partitura, achados) {
302
306
  return partitura;
303
307
  }
304
308
 
305
- module.exports = { mundoDe, lerLugar, lerCoisas, lerElenco, iconesPendentes, aplicarIcones, LUGARES, HORAS, PECAS, ESPECIES, sem };
309
+ /**
310
+ * As peças que a IA TROUXE para a cena: a logo, o botão ou o produto recortado
311
+ * do site (estudio_midia acao:elemento), um PNG sem fundo que vira blocos como
312
+ * os ícones 3D. "Imagens e parte de sites devem entrar no motion como ícone
313
+ * 3D" (Joel, 12/10). Ficam na frente, no centro, um pouco maiores que o resto
314
+ * do lugar: é o assunto. No máximo três, para não virar vitrine.
315
+ */
316
+ function adicionarObjetos(partitura, objetos) {
317
+ const lista = (Array.isArray(objetos) ? objetos : []).filter((o) => o && o.src).slice(0, 3);
318
+ if (!lista.length) return partitura;
319
+ for (const m of partitura.mundos || []) {
320
+ lista.forEach((o, i) => {
321
+ const x = (i - (lista.length - 1) / 2) * 2.4;
322
+ m.props.push({ icone: String(o.nome || 'objeto').slice(0, 40), src: o.src, x, z: -1.6, raio: 1, giro: 0, escala: 1.35, trazido: true });
323
+ });
324
+ }
325
+ return partitura;
326
+ }
327
+
328
+ module.exports = { mundoDe, lerLugar, lerCoisas, lerElenco, iconesPendentes, aplicarIcones, adicionarObjetos, LUGARES, HORAS, PECAS, ESPECIES, sem };
@@ -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,12 +146,14 @@ 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,
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 } : {}),
137
157
  // A cabeça facetada (low-poly) é o padrão; 'cubo' é o rosto de pixel da 10.5.
138
158
  ...(pedido.cabeca === 'cubo' ? { cabeca: 'cubo' } : {}),
139
159
  };
@@ -316,7 +336,13 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
316
336
  return bicho ? { ...dado, tipo: 'animal', especie: bicho[1] } : dado;
317
337
  };
318
338
  // 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 }));
339
+ const comGenero = (n) => {
340
+ const d = pedidoDe(n);
341
+ if (d.genero) return d;
342
+ const g = n === 'apresentador' ? generoDoApresentador(roteiro) : falantes.generoDe(n, d);
343
+ return g === 'f' || g === 'm' ? { ...d, genero: g } : d;
344
+ };
345
+ 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
346
  const quemDe = (pt) => {
321
347
  const f = typeof pt.falante === 'string' && pt.falante.trim() ? pt.falante.trim().toLowerCase() : 'apresentador';
322
348
  return nomes.indexOf(f) % Math.max(1, elenco.length);
@@ -453,4 +479,4 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
453
479
  };
454
480
  }
455
481
 
456
- module.exports = { calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
482
+ module.exports = { generoDoApresentador, calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
@@ -49,6 +49,7 @@ const fs = require('fs');
49
49
  const path = require('path');
50
50
  const morfar = require('./morfar.js');
51
51
  const kit = require('./kit.js');
52
+ const movimentos = require('./movimentos.js');
52
53
 
53
54
  // "kit": uma peça pronta do kit de motion (kit.js), pelo nome — ver kit/*.jsx.
54
55
  const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo', 'kit']);
@@ -283,6 +284,11 @@ function conferir(plano, { pasta = null } = {}) {
283
284
  if (!plano || typeof plano !== 'object') {
284
285
  return { ok: false, problemas: [erro('plano', 'o plano é um objeto JSON')], duracao: 0 };
285
286
  }
287
+ /* `movimento: "quicar-de-baixo"` vira quadros-chave (movimentos.js) antes
288
+ de tudo: o que segue confere o `anima` como se a IA o tivesse escrito. */
289
+ for (const d of movimentos.expandir(plano)) {
290
+ problemas.push(erro(d.onde, `o movimento "${d.nome}" não existe — procure com estudio_midia acao:"movimentos" termo:"…"`));
291
+ }
286
292
 
287
293
  const f = plano.formato || {};
288
294
  const largura = Number(f.largura) || 1920;
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
  }