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.
- package/README.md +2 -2
- package/bin/primocode.js +58 -37
- package/lib/boas-vindas.js +12 -13
- package/lib/config.js +5 -2
- package/lib/conta.js +194 -169
- package/lib/estudio/conferencia.js +98 -0
- package/lib/estudio/gerativo.js +25 -3
- package/lib/estudio/index.js +10 -2
- package/lib/estudio/kit/fluido.jsx +5 -0
- package/lib/estudio/kit/mundo.jsx +11 -2
- package/lib/estudio/kit/voxel.jsx +58 -15
- package/lib/estudio/personagem.js +35 -7
- package/lib/nuvem.js +11 -22
- package/lib/planos.js +71 -0
- package/package.json +2 -2
package/lib/estudio/index.js
CHANGED
|
@@ -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(`
|
|
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('
|
|
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: '
|
|
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
|
-
|
|
293
|
-
|
|
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,
|
|
302
|
+
out.push({ p: [Math.sin(az) * Math.cos(el) * 0.33, y, z - 0.02], r, k: i });
|
|
296
303
|
}
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
616
|
-
const
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
28
|
-
*
|
|
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
|
|
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
|
-
/*
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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:
|
|
154
|
-
expira: Date.now() +
|
|
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.
|
|
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"
|