primocode 9.10.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/estilo.js +51 -0
  12. package/lib/estudio/glifos.js +215 -0
  13. package/lib/estudio/index.js +41 -18
  14. package/lib/estudio/kit/base.jsx +285 -0
  15. package/lib/estudio/kit/index.jsx +11 -0
  16. package/lib/estudio/kit/marca.jsx +741 -0
  17. package/lib/estudio/kit/vox.jsx +357 -0
  18. package/lib/estudio/kit.js +72 -0
  19. package/lib/estudio/lancamento.js +413 -0
  20. package/lib/estudio/motion.js +2 -5
  21. package/lib/estudio/plano.js +28 -3
  22. package/lib/estudio/remotion.js +87 -43
  23. package/lib/estudio/ritmo.js +77 -0
  24. package/lib/estudio/vox.js +385 -0
  25. package/lib/fala.js +1 -94
  26. package/lib/pipeline.js +55 -56
  27. package/lib/repeticao.js +3 -1
  28. package/lib/skills.js +8 -2
  29. package/lib/snapshot.js +111 -0
  30. package/lib/tools.js +79 -20
  31. package/lib/ui.js +13 -6
  32. package/lib/video/analise.js +89 -0
  33. package/lib/video/cli.js +74 -0
  34. package/lib/video/delta.js +90 -0
  35. package/lib/video/edits.js +379 -0
  36. package/lib/video/filters.js +247 -0
  37. package/lib/video/ops.js +322 -0
  38. package/lib/video/projeto.js +133 -0
  39. package/lib/video/render.js +221 -0
  40. package/lib/video/texto.js +142 -0
  41. package/lib/video/timeline.js +310 -0
  42. package/lib/video/transcript.js +124 -0
  43. package/lib/voz.js +75 -223
  44. package/package.json +2 -3
  45. package/skills/motion-de-primeira/SKILL.md +3 -4
  46. package/studio/texto.py +208 -0
  47. package/voz/primo_voz/__main__.py +96 -236
  48. package/voz/primo_voz/transcrever.py +31 -1
  49. package/app/ansi.js +0 -55
  50. package/app/app.css +0 -500
  51. package/app/app.js +0 -1017
  52. package/app/icone.svg +0 -6
  53. package/app/index.html +0 -220
  54. package/app/primo-marca.js +0 -192
  55. package/lib/app.js +0 -615
  56. package/lib/icone.js +0 -263
  57. package/lib/instalar-app.js +0 -231
  58. package/lib/painel.js +0 -291
  59. package/voz/primo_voz/desperta.py +0 -195
  60. package/voz/primo_voz/orbe.py +0 -406
@@ -98,6 +98,7 @@ import {
98
98
  useVideoConfig, interpolate, staticFile, continueRender, delayRender,
99
99
  } from 'remotion';
100
100
  import { morfar, ehCor, misturarCor } from './morfar';
101
+ import { KIT, CURVAS_KIT, Filtros } from './kit';
101
102
 
102
103
  /* ── SUAVIZAÇÃO ──────────────────────────────────────────────────────────
103
104
  * Cada curva é uma função de 0..1 em 0..1. "suave" é o padrão porque
@@ -117,6 +118,8 @@ const CURVAS = {
117
118
  if (t < 2.5 / d) return n * (t -= 2.25 / d) * t + 0.9375;
118
119
  return n * (t -= 2.625 / d) * t + 0.984375;
119
120
  },
121
+ // editorial, camera, arrasto, acelera (kit/base.jsx): a curva certa do guia VOX.
122
+ ...CURVAS_KIT,
120
123
  };
121
124
 
122
125
  /* O valor de uma propriedade animada no segundo "t".
@@ -217,7 +220,12 @@ function Elemento({ el, t, dados }) {
217
220
 
218
221
  const tLocal = t - (el.de || 0);
219
222
  const a = animado(el.anima, tLocal);
220
- const base = posicao(el);
223
+ /* A PEÇA DO KIT desenha nas coordenadas da cena (x, y dela são o lugar do
224
+ assunto, não o canto de uma caixa): a caixa dela é o quadro inteiro, e o
225
+ "anima" do elemento move o conjunto. */
226
+ const base = el.tipo === 'kit'
227
+ ? { position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' }
228
+ : posicao(el);
221
229
  const move = transformDe(a, {});
222
230
  const estilo = {
223
231
  ...base,
@@ -355,6 +363,16 @@ function Elemento({ el, t, dados }) {
355
363
  /* O GRUPO: uma tela dentro da tela. Os filhos vivem no tempo e no espaço
356
364
  dele; com "recorte" o que passa da borda some (a tela do celular). O
357
365
  grupo anima como qualquer elemento: cresce, sai do celular, cobre tudo. */
366
+ case 'kit': {
367
+ const Peca = KIT[el.peca];
368
+ if (!Peca) return null;
369
+ return (
370
+ <div style={{ ...estilo, transformOrigin: el.origem || ((el.x || 0) + 'px ' + (el.y || 0) + 'px') }}>
371
+ <Peca el={el} t={tLocal} fps={dados.fps} />
372
+ </div>
373
+ );
374
+ }
375
+
358
376
  case 'grupo':
359
377
  return (
360
378
  <div style={{
@@ -516,11 +534,36 @@ export function Cena({ cena, dados, tFixo }) {
516
534
  cam.rotacaoY != null ? 'rotateY(' + cam.rotacaoY + 'deg)' : '',
517
535
  ].filter(Boolean).join(' ');
518
536
 
537
+ /* A CÂMERA EM CAMADAS (o parallax do guia VOX): "camadas" são planos de
538
+ profundidade, e o que está longe anda menos que o que está perto. O
539
+ parallax é CONSEQUÊNCIA da câmera, não um efeito à parte: o plano do
540
+ assunto ("sujeito") recebe a câmera exata, os outros na proporção da
541
+ profundidade deles — e cada um com o desfoque da lente naquela distância.
542
+ Cada elemento diz a camada dele ("camada": índice); sem, é a do sujeito. */
543
+ const camadas = Array.isArray(cena.camadas) && cena.camadas.length ? cena.camadas : null;
544
+ const iSujeito = camadas ? Math.max(0, camadas.findIndex((c) => c.sujeito)) : 0;
545
+ const mundo = (els, f, desfoque) => (
546
+ <AbsoluteFill style={{
547
+ transform: f == null ? (camTransform || undefined) : [
548
+ 'translate(' + (-(cam.x || 0) * f + tx) + 'px,' + (-(cam.y || 0) * f + ty) + 'px)',
549
+ 'scale(' + ((1 + ((cam.zoom ?? 1) - 1) * f) * soco) + ')',
550
+ 'rotate(' + ((cam.rotacao || 0) * f + tr) + 'deg)',
551
+ ].join(' '),
552
+ transformOrigin: cena.cameraOrigem || 'center center',
553
+ // O mundo guarda o 3D dos filhos; a perspectiva vem do pai.
554
+ transformStyle: cena.perspectiva ? 'preserve-3d' : undefined,
555
+ filter: desfoque ? 'blur(' + desfoque + 'px)' : undefined,
556
+ }}>
557
+ {els.map((el, i) => <Elemento key={i} el={el} t={t} dados={dados} />)}
558
+ </AbsoluteFill>
559
+ );
560
+
519
561
  return (
520
562
  /* A perspectiva mora AQUI, um nível acima do mundo: se ficasse no mesmo
521
563
  elemento da câmera, girar a câmera sairia achatado. */
522
564
  <AbsoluteFill style={{ ...fundoCSS(cena.fundo), overflow: 'hidden',
523
565
  perspective: cena.perspectiva ? cena.perspectiva + 'px' : undefined }}>
566
+ <Filtros />
524
567
  {cena.fundo && cena.fundo.tipo === 'imagem' && (
525
568
  <Img src={staticFile(cena.fundo.src)} style={{
526
569
  position: 'absolute', inset: 0, width: '100%', height: '100%',
@@ -533,16 +576,13 @@ export function Cena({ cena, dados, tFixo }) {
533
576
  objectFit: cena.fundo.ajuste || 'cover',
534
577
  }} />
535
578
  )}
536
- <AbsoluteFill style={{
537
- transform: camTransform || undefined,
538
- transformOrigin: cena.cameraOrigem || 'center center',
539
- // O mundo guarda o 3D dos filhos; a perspectiva vem do pai.
540
- transformStyle: cena.perspectiva ? 'preserve-3d' : undefined,
541
- }}>
542
- {(cena.elementos || []).map((el, i) => (
543
- <Elemento key={i} el={el} t={t} dados={dados} />
544
- ))}
545
- </AbsoluteFill>
579
+ {camadas
580
+ ? camadas.map((c, ci) => (
581
+ <React.Fragment key={ci}>
582
+ {mundo((cena.elementos || []).filter((el) => (el.camada ?? iSujeito) === ci), (c.p || 1) / (camadas[iSujeito].p || 1), c.desfoque)}
583
+ </React.Fragment>
584
+ ))
585
+ : mundo(cena.elementos || [], null, 0)}
546
586
  {flash > 0.01 && <AbsoluteFill style={{ backgroundColor: cena.corFlash || '#fff', opacity: flash * 0.85 }} />}
547
587
  </AbsoluteFill>
548
588
  );
@@ -552,6 +592,7 @@ export function Cena({ cena, dados, tFixo }) {
552
592
  const RAIZ_JSX = String.raw`import React from 'react';
553
593
  import { AbsoluteFill, Audio, Sequence, staticFile, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
554
594
  import { Cena } from './Cena';
595
+ import { Acabamento, Hud, bolhaCaminho } from './kit';
555
596
  import plano from './plano.json';
556
597
  import icones from './icones.json';
557
598
 
@@ -560,6 +601,7 @@ import icones from './icones.json';
560
601
  * o "piscar" de fundo que denuncia corte mal feito. */
561
602
  function Transicao({ tipo, progresso, children }) {
562
603
  const p = progresso;
604
+ const { width: W, height: H } = useVideoConfig();
563
605
  let estilo = {};
564
606
  if (tipo === 'esmaecer') estilo = { opacity: p };
565
607
  else if (tipo === 'deslizar') estilo = { transform: 'translateX(' + (1 - p) * 100 + '%)' };
@@ -572,6 +614,12 @@ function Transicao({ tipo, progresso, children }) {
572
614
  // O SOCO: a cena entra grande e bate no lugar.
573
615
  else if (tipo === 'soco') estilo = { transform: 'scale(' + (1.35 - 0.35 * p) + ')', opacity: Math.min(1, p * 3) };
574
616
  else if (tipo === 'flash') estilo = { opacity: Math.min(1, p * 2), filter: 'brightness(' + (1 + (1 - p) * 3) + ')' };
617
+ /* O LÍQUIDO: a cena nova entra como uma mancha que cresce de um canto e
618
+ engole a anterior, com a borda viva (os lóbulos respiram enquanto crescem). */
619
+ else if (tipo === 'liquido' && p < 1) {
620
+ const k = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
621
+ estilo = { clipPath: "path('" + bolhaCaminho(W * 0.78, H * 0.62, Math.hypot(W, H) * 0.95 * k, p * 3, 2) + "')" };
622
+ }
575
623
  return <AbsoluteFill style={estilo}>{children}</AbsoluteFill>;
576
624
  }
577
625
 
@@ -591,7 +639,7 @@ export const Paginas = () => {
591
639
  };
592
640
 
593
641
  export const Video = () => {
594
- const { fps } = useVideoConfig();
642
+ const { fps, width: largura, height: altura } = useVideoConfig();
595
643
  const frame = useCurrentFrame();
596
644
  const dados = { fps, icones };
597
645
 
@@ -632,31 +680,17 @@ export const Video = () => {
632
680
  );
633
681
  })}
634
682
 
635
- {/* O ACABAMENTO: granulação de filme, vinheta e a luz da marca passando.
636
- É o que tira a cara de apresentação e dá a cara de peça de 2026. */}
637
- {plano.acabamento && plano.acabamento.luz && (
638
- <AbsoluteFill style={{ pointerEvents: 'none', mixBlendMode: 'screen', opacity: plano.acabamento.luzForca ?? 0.22,
639
- background: 'radial-gradient(circle at ' + (50 + 40 * Math.sin(frame / 38)) + '% ' + (30 + 25 * Math.cos(frame / 51)) + '%, '
640
- + plano.acabamento.luz[0] + ' 0%, transparent 45%), radial-gradient(circle at ' + (50 - 40 * Math.sin(frame / 44)) + '% '
641
- + (75 - 20 * Math.sin(frame / 33)) + '%, ' + (plano.acabamento.luz[1] || plano.acabamento.luz[0]) + ' 0%, transparent 40%)' }} />
642
- )}
643
- {plano.acabamento && plano.acabamento.grade && (
644
- <AbsoluteFill style={{ pointerEvents: 'none', opacity: plano.acabamento.grade,
645
- backgroundImage: 'linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)',
646
- backgroundSize: (plano.acabamento.passoGrade || 120) + 'px ' + (plano.acabamento.passoGrade || 120) + 'px',
647
- backgroundPosition: (frame * 0.2) + 'px ' + (frame * 0.1) + 'px' }} />
648
- )}
649
- {plano.acabamento && plano.acabamento.vinheta && (
650
- <AbsoluteFill style={{ pointerEvents: 'none', background: 'radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,' + plano.acabamento.vinheta + ') 100%)' }} />
651
- )}
652
- {plano.acabamento && plano.acabamento.grao && (
653
- <AbsoluteFill style={{ pointerEvents: 'none', opacity: plano.acabamento.grao, mixBlendMode: 'overlay' }}>
654
- <svg width="100%" height="100%">
655
- <filter id="grao"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed={frame % 60} /></filter>
656
- <rect width="100%" height="100%" filter="url(#grao)" />
657
- </svg>
658
- </AbsoluteFill>
659
- )}
683
+ {/* O ACABAMENTO: granulação de filme, vinheta e a luz da marca passando
684
+ (kit/base.jsx). É o que tira a cara de apresentação. */}
685
+ <Acabamento ac={plano.acabamento} frame={frame} />
686
+ {/* O HUD do motion designer: cantos, timecode, fps e a seção da cena. */}
687
+ {plano.hud && (() => {
688
+ const atual = pedacos.filter((q) => frame >= q.de).pop() || pedacos[0];
689
+ const total = pedacos.length ? pedacos[pedacos.length - 1].de + pedacos[pedacos.length - 1].dur : 1;
690
+ // A cena diz se é escura (o HUD claro) ou clara (o HUD escuro).
691
+ const escuro = !(atual && atual.cena.escuro === false);
692
+ return <Hud hud={plano.hud} frame={frame} fps={fps} total={total} W={largura} H={altura} secao={atual && atual.cena.secao} escuro={escuro} />;
693
+ })()}
660
694
 
661
695
  {plano.audio && plano.audio.narracao && (
662
696
  <Audio src={staticFile(plano.audio.narracao)} volume={plano.audio.volumeNarracao ?? 1} />
@@ -800,6 +834,8 @@ function montar({ pasta, titulo, plano, icones = {} }) {
800
834
  escrever(path.join(src, 'Video.jsx'), RAIZ_JSX);
801
835
  escrever(path.join(src, 'Cena.jsx'), CENA_JSX);
802
836
  escrever(path.join(src, 'morfar.js'), require('./morfar.js').fonteEsm());
837
+ // O kit de motion (kit.js): as peças que o elemento {"tipo":"kit"} monta.
838
+ for (const f of require('./kit.js').arquivos()) escrever(path.join(src, f.nome), f.conteudo);
803
839
  escrever(path.join(src, 'plano.json'), JSON.stringify(plano, null, 2));
804
840
  escrever(path.join(src, 'icones.json'), JSON.stringify(icones, null, 2));
805
841
  escrever(path.join(src, 'fontes.css'), fontesCSS(plano));
@@ -826,16 +862,24 @@ function montar({ pasta, titulo, plano, icones = {} }) {
826
862
  do usuário e roda com rede disponível no `npm install`; o Chromium do
827
863
  Remotion baixa a fonte no primeiro quadro e o Remotion espera por ela. */
828
864
  function fontesCSS(plano) {
865
+ /* Toda chave que começa com "fonte" (fonte, fonteMono, fonteDef…), em
866
+ qualquer profundidade: dentro de grupo, na legenda, nas peças do kit e
867
+ no HUD. Fonte que o plano usa e o CSS não declara sai em Times. */
829
868
  const usadas = new Set();
830
- for (const cena of plano.cenas || []) {
831
- for (const el of cena.elementos || []) {
832
- if (el.fonte) usadas.add(el.fonte);
833
- if (el.estilo && el.estilo.fonte) usadas.add(el.estilo.fonte);
869
+ const andar = (o) => {
870
+ if (!o || typeof o !== 'object') return;
871
+ if (Array.isArray(o)) return o.forEach(andar);
872
+ for (const [k, v] of Object.entries(o)) {
873
+ if (/^fonte/.test(k) && typeof v === 'string' && v.trim()) usadas.add(v.trim());
874
+ else if (v && typeof v === 'object') andar(v);
834
875
  }
835
- }
876
+ };
877
+ for (const cena of plano.cenas || []) andar(cena.elementos || []);
878
+ andar(plano.hud);
836
879
  if (!usadas.size) return '/* nenhuma fonte externa neste vídeo */\n';
837
880
  const familias = [...usadas]
838
- .map((f) => 'family=' + encodeURIComponent(f).replace(/%20/g, '+') + ':wght@300;400;500;600;700;800;900')
881
+ // O Google devolve o que a família tem e ignora o resto: pedir todos os pesos e o itálico não quebra.
882
+ .map((f) => 'family=' + encodeURIComponent(f).replace(/%20/g, '+') + ':ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,600')
839
883
  .join('&');
840
884
  return `@import url('https://fonts.googleapis.com/css2?${familias}&display=block');\n`;
841
885
  }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ritmo.js — o tempo de um vídeo feito de partes: onde cada uma corta, quando
3
+ * cada coisa entra, e a música que abaixa quando a voz fala.
4
+ *
5
+ * "O áudio comanda o quadro." — guia MOTION VOX STYLE
6
+ *
7
+ * Os estilos de cena por parte (vox.js, lancamento.js) dividem isto: o corte
8
+ * cai no beat (um pouco antes da primeira palavra da parte), cada elemento
9
+ * entra no instante em que a palavra dele é dita, e o volume da trilha segue
10
+ * a narração. Puro: tempos entram, números saem.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const { quando } = require('./cinema.js');
16
+
17
+ const r2 = (n) => Math.round(n * 100) / 100;
18
+
19
+ /**
20
+ * Onde cada parte começa e quanto dura.
21
+ *
22
+ * Com voz, a parte começa `antes` segundos antes da primeira palavra (o corte
23
+ * seco no beat) e vai até a próxima; a última ganha `cauda` de respiro. Sem
24
+ * voz, cada parte dura o que se leva para ler o que ela mostra.
25
+ *
26
+ * @returns {{ini:number[], dur:number[], total:number, temVoz:boolean}}
27
+ */
28
+ function tempoDasPartes(partes, tempos = [], { semVoz = () => 2.6, antes = 0.12, cauda = 1.1, minimo = 1.1 } = {}) {
29
+ const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
30
+ const ini = [];
31
+ if (temVoz) partes.forEach((_, i) => ini.push(i === 0 ? 0 : r2(Math.max(ini[i - 1] + minimo, tempos[i].de - antes))));
32
+ else partes.forEach((p, i) => ini.push(i === 0 ? 0 : r2(ini[i - 1] + Math.max(minimo, semVoz(partes[i - 1])))));
33
+ const total = r2(temVoz ? Math.max(ini[ini.length - 1] + minimo, tempos[tempos.length - 1].ate + cauda) : ini[ini.length - 1] + Math.max(minimo, semVoz(partes[partes.length - 1])) + 0.4);
34
+ const dur = ini.map((t, i) => r2((i < ini.length - 1 ? ini[i + 1] : total) - t));
35
+ return { ini, dur, total, temVoz };
36
+ }
37
+
38
+ /**
39
+ * Quando cada texto entra, no tempo DA CENA: o instante em que a primeira
40
+ * palavra forte dele é dita. Sem voz (ou sem achar), espalha os textos pelo
41
+ * começo da cena, respeitando a ordem.
42
+ */
43
+ function gatilhos(textos, tempo, inicio, dur, { t0 = 0.3, ate = 0.62 } = {}) {
44
+ const palavras = (tempo && tempo.palavras) || [];
45
+ const fim = Math.max(t0 + 0.2, dur * ate);
46
+ if (!palavras.length) return textos.map((_, i) => r2(t0 + (fim - t0) * (i / Math.max(1, textos.length))));
47
+ const abs = quando(textos, palavras, inicio + fim, inicio + t0);
48
+ // Nunca antes do corte, nunca tarde demais para ser lido, sempre em ordem.
49
+ let antes = 0;
50
+ return abs.map((t) => {
51
+ const rel = Math.min(Math.max(0.08, t - inicio), Math.max(0.1, dur - 0.9));
52
+ antes = Math.max(antes + 0.1, rel);
53
+ return r2(antes);
54
+ });
55
+ }
56
+
57
+ /** As palavras faladas numa parte, no tempo da cena: [{texto, t}]. */
58
+ function palavrasNaCena(tempo, inicio) {
59
+ return ((tempo && tempo.palavras) || []).map((w) => ({ texto: w.texto, t: r2(Math.max(0.05, w.de - inicio)) }));
60
+ }
61
+
62
+ /** O volume da trilha no tempo: cheia no respiro, baixa quando a voz fala, some no fim. */
63
+ function volumesDaTrilha(tempos, total, { alto = 0.22, baixo = 0.11 } = {}) {
64
+ const k = (t, v) => ({ t: r2(t), v });
65
+ const vs = [k(0, 0.05), k(0.4, alto)];
66
+ // Um quadro-chave de cada vez, sempre depois do anterior: fala colada na outra não volta no tempo.
67
+ const poe = (t, v) => vs.push(k(Math.max(vs[vs.length - 1].t + 0.01, t), v));
68
+ for (const tp of tempos || []) {
69
+ if (!tp || !(tp.ate > tp.de)) continue;
70
+ poe(tp.de - 0.15, alto); poe(tp.de, baixo); poe(tp.ate, baixo); poe(tp.ate + 0.35, alto);
71
+ }
72
+ poe(total - 0.6, alto * 0.9);
73
+ poe(total, 0);
74
+ return vs;
75
+ }
76
+
77
+ module.exports = { tempoDasPartes, gatilhos, palavrasNaCena, volumesDaTrilha, r2 };