primocode 9.9.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.
- package/README.md +30 -6
- package/bin/primocode.js +100 -341
- package/lib/act.js +245 -23
- package/lib/browser.js +17 -0
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +4 -2
- package/lib/estudio/diretor.js +9 -1
- package/lib/estudio/ende.js +2 -0
- package/lib/estudio/estilo.js +51 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/index.js +63 -11
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/index.jsx +11 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +72 -0
- package/lib/estudio/lancamento.js +413 -0
- package/lib/estudio/morfar.js +191 -0
- package/lib/estudio/motion.js +333 -0
- package/lib/estudio/plano.js +54 -7
- package/lib/estudio/remotion.js +167 -52
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +62 -0
- package/lib/estudio/vox.js +385 -0
- package/lib/fala.js +1 -94
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +8 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +79 -20
- package/lib/ui.js +13 -6
- package/lib/video/analise.js +89 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +247 -0
- package/lib/video/ops.js +322 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/render.js +221 -0
- package/lib/video/texto.js +142 -0
- package/lib/video/timeline.js +310 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +21 -1
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
package/lib/cursor.js
CHANGED
|
@@ -95,7 +95,16 @@ function subir() {
|
|
|
95
95
|
};
|
|
96
96
|
proc.on('exit', morreu);
|
|
97
97
|
proc.on('error', morreu);
|
|
98
|
-
|
|
98
|
+
// Demorou demais: desiste E mata — senão o Python ficava órfão, e o
|
|
99
|
+
// `pronto` guardado como false deixava o cursor fora a sessão toda.
|
|
100
|
+
const teto = setTimeout(() => {
|
|
101
|
+
if (subiu) return;
|
|
102
|
+
semCursor = 'o cursor demorou demais para abrir';
|
|
103
|
+
try { proc && proc.kill(); } catch { /* já morreu */ }
|
|
104
|
+
pronto = null;
|
|
105
|
+
resolve(false);
|
|
106
|
+
}, 20000);
|
|
107
|
+
if (teto.unref) teto.unref();
|
|
99
108
|
// O cursor não pode segurar o PrimoCode aberto na saída.
|
|
100
109
|
proc.unref();
|
|
101
110
|
proc.stdout.unref && proc.stdout.unref();
|
package/lib/entrada.js
CHANGED
|
@@ -143,7 +143,8 @@ function montar(e) {
|
|
|
143
143
|
if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
|
|
144
144
|
conteudo += c.dim(e.fantasma);
|
|
145
145
|
}
|
|
146
|
-
if (i === 0 && vazio && e.
|
|
146
|
+
if (i === 0 && vazio && e.ocupado) conteudo = c.dim('mande um recado — ele entra na tarefa depois do passo atual');
|
|
147
|
+
else if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
|
|
147
148
|
// Sem `encher`: sem barra à direita não há nada para alinhar, e o
|
|
148
149
|
// espaço sobrando só apagaria o que estivesse embaixo.
|
|
149
150
|
out.push(' ' + marca + conteudo);
|
|
@@ -315,7 +316,8 @@ class Entrada extends EventEmitter {
|
|
|
315
316
|
suspender() { this.suspenso = true; this._apagar(); }
|
|
316
317
|
retomar() { this.suspenso = false; if (this.ativo && !this.fechado) this._desenhar(); }
|
|
317
318
|
|
|
318
|
-
/** O agente está trabalhando: a caixa esmaece e o Enter
|
|
319
|
+
/** O agente está trabalhando: a caixa esmaece, e o Enter manda um RECADO
|
|
320
|
+
* que entra na tarefa em curso (a caixa de entrada de lib/act.js). */
|
|
319
321
|
trabalhando(sim) {
|
|
320
322
|
if (this.ocupado === !!sim) return;
|
|
321
323
|
this.ocupado = !!sim;
|
package/lib/estudio/diretor.js
CHANGED
|
@@ -62,6 +62,14 @@ function precisaDeProducao(texto) {
|
|
|
62
62
|
return { producao: false };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/* Editar a GRAVAÇÃO da pessoa não é produzir vídeo do zero: aí o caminho é o
|
|
66
|
+
editor por linha do tempo (`primocode video`, lib/video/), não o roteiro. */
|
|
67
|
+
const PEDE_EDICAO = /\b(edit[ae]r?|edi[cç][aã]o|cort[ae]r?|legend[ae]r?|tir[ae]r?\s+(o|os)\s+sil[eê]ncios?)\b[\s\S]{0,80}\b(grava[cç][aã]o|meu\s+v[ií]deo|esse\s+v[ií]deo|este\s+v[ií]deo|[\w-]+\.(mp4|mov|mkv|webm|m4v))\b|\b[\w-]+\.(mp4|mov|mkv|webm|m4v)\b[\s\S]{0,80}\b(edit|cort|legend|sil[eê]nci)/i;
|
|
68
|
+
|
|
69
|
+
function precisaDeEdicao(texto) {
|
|
70
|
+
return PEDE_EDICAO.test(String(texto || ''));
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
// ── 2. as decisões que não são do modelo ─────────────────────────────────
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -293,7 +301,7 @@ function pauta(plano) {
|
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
module.exports = {
|
|
296
|
-
precisaDeProducao, vozPara, ritmoPara,
|
|
304
|
+
precisaDeProducao, precisaDeEdicao, vozPara, ritmoPara,
|
|
297
305
|
normalizarPlano, extrairJSON, planejar, pauta,
|
|
298
306
|
DIRETOR, REVISAO,
|
|
299
307
|
};
|
package/lib/estudio/ende.js
CHANGED
|
@@ -38,6 +38,8 @@ const CLIMAS = {
|
|
|
38
38
|
viral: ['upbeat', 'beat', 'energetic', 'pop', 'electronic'],
|
|
39
39
|
impacto: ['epic', 'drums', 'trailer', 'energetic', 'beat'],
|
|
40
40
|
neutro: ['background', 'corporate', 'chilled', 'calm', 'ambient'],
|
|
41
|
+
// Produto e tecnologia: pulso de sintetizador, limpo, com impulso.
|
|
42
|
+
tecnologia: ['arpeggiator', 'analog synths', 'clockwork', 'rhythm', 'commercial', 'clean mix', 'crisp'],
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
const limpar = (s) => String(s || '').replace(/<[^>]*>/g, '').replace(/&/g, '&').replace(/�?39;/g, "'").replace(/\s+/g, ' ').trim();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* estilo.js — qual estilo de vídeo um roteiro vira.
|
|
3
|
+
*
|
|
4
|
+
* motion o padrão: um plano contínuo em 3D, o protagonista que se deforma (motion.js)
|
|
5
|
+
* vox explainer editorial: tinta sobre papel, os oito dispositivos VOX (vox.js)
|
|
6
|
+
* marca brand reveal: o brilho, o vetor, a marca cromada, o padrão (lancamento.js)
|
|
7
|
+
* lancamento filme de lançamento de produto: cartões, painel, calendário, mural (lancamento.js)
|
|
8
|
+
* impacto anúncio: corte a cada batida (impacto.js)
|
|
9
|
+
* cinema filme de marca premium com fotos reais (cinema.js)
|
|
10
|
+
* composicao a composição antiga, uma cena por parte (composicao.js)
|
|
11
|
+
*
|
|
12
|
+
* Quem manda, em ordem: o estilo que o AGENTE pediu; o do roteiro (a menos
|
|
13
|
+
* que o pedido diga motion); o que o pedido descreve; e o motion.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const ESTILOS = ['motion', 'vox', 'marca', 'lancamento', 'impacto', 'cinema', 'composicao'];
|
|
19
|
+
// Nomes que o modelo usa para os mesmos estilos.
|
|
20
|
+
const APELIDOS = { limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
|
|
21
|
+
|
|
22
|
+
const PEDE_MOTION = /\bmotion\b|anima[çc][ãa]o|animad[oa]/i;
|
|
23
|
+
const PEDE_VOX = /\bvox\b|explica|explainer|document[aá]rio|editorial|hist[oó]ria d[aoe]s?\b/i;
|
|
24
|
+
const PEDE_MARCA = /\bbrand|identidade visual|revela[çc][ãa]o d[ae] marca|\blogo(tipo|marca)?\b|vinheta de marca/i;
|
|
25
|
+
const PEDE_LANCAMENTO = /lan[çc]amento|\blaunch|\bsaas\b|produto novo|apresenta(ndo|r|ção d[oe]) (o |a )?(app|produto|plataforma|ferramenta)/i;
|
|
26
|
+
const PEDE_IMPACTO = /an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i;
|
|
27
|
+
|
|
28
|
+
const normal = (e) => {
|
|
29
|
+
const x = String(e || '').trim().toLowerCase();
|
|
30
|
+
if (!x) return null;
|
|
31
|
+
const n = APELIDOS[x] || x;
|
|
32
|
+
return ESTILOS.includes(n) ? n : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function escolherEstilo({ pedido = '', agente = null, roteiro = null } = {}) {
|
|
36
|
+
const pedidoAgente = normal(agente);
|
|
37
|
+
if (pedidoAgente) return pedidoAgente;
|
|
38
|
+
const doRoteiro = normal(roteiro);
|
|
39
|
+
if (doRoteiro && !(doRoteiro !== 'motion' && PEDE_MOTION.test(pedido) && !PEDE_VOX.test(pedido))) return doRoteiro;
|
|
40
|
+
if (PEDE_VOX.test(pedido)) return 'vox';
|
|
41
|
+
if (PEDE_MARCA.test(pedido)) return 'marca';
|
|
42
|
+
if (PEDE_LANCAMENTO.test(pedido)) return 'lancamento';
|
|
43
|
+
if (PEDE_MOTION.test(pedido)) return 'motion';
|
|
44
|
+
if (PEDE_IMPACTO.test(pedido)) return 'impacto';
|
|
45
|
+
return 'motion';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
|
|
49
|
+
const LEVES = new Set(['motion', 'vox', 'marca', 'lancamento']);
|
|
50
|
+
|
|
51
|
+
module.exports = { escolherEstilo, ESTILOS, LEVES, PEDE_MOTION };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glifos.js — as letras de uma palavra como VETOR de verdade: o contorno, as
|
|
3
|
+
* âncoras e as alças, lidos da própria fonte TrueType.
|
|
4
|
+
*
|
|
5
|
+
* A cena do brand reveal que mais vende "feito por designer" é a palavra
|
|
6
|
+
* enorme em contorno, com os quadradinhos das âncoras, as alças de bezier e o
|
|
7
|
+
* cursor passeando. Isso só convence com os pontos REAIS da letra — pontos
|
|
8
|
+
* inventados em cima de um <text> denunciam na hora. Então o PrimoCode lê a
|
|
9
|
+
* fonte: a tabela glyf de um .ttf guarda cada contorno como pontos na curva
|
|
10
|
+
* (âncoras) e fora dela (controles de quadrática, as alças).
|
|
11
|
+
*
|
|
12
|
+
* Zero dependência: um leitor de TrueType de ~150 linhas (cmap formatos 4 e
|
|
13
|
+
* 12, loca, glyf simples e composto, hmtx). A fonte vem do Google Fonts
|
|
14
|
+
* (a API antiga devolve .ttf) e fica em cache; sem rede, `palavra()` devolve
|
|
15
|
+
* null e a peça "vetor" desenha o contorno do próprio texto, sem alças.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// ── o leitor ─────────────────────────────────────────────────────────────
|
|
25
|
+
function tabelas(buf) {
|
|
26
|
+
const n = buf.readUInt16BE(4);
|
|
27
|
+
const t = {};
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
const o = 12 + i * 16;
|
|
30
|
+
t[buf.toString('latin1', o, o + 4)] = { off: buf.readUInt32BE(o + 8), len: buf.readUInt32BE(o + 12) };
|
|
31
|
+
}
|
|
32
|
+
return t;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Caractere → índice do glifo (cmap 3/10 formato 12, ou 3/1 e 0/x formato 4). */
|
|
36
|
+
function mapaDeCaracteres(buf, cmap) {
|
|
37
|
+
const n = buf.readUInt16BE(cmap.off + 2);
|
|
38
|
+
const subs = [];
|
|
39
|
+
for (let i = 0; i < n; i++) {
|
|
40
|
+
const o = cmap.off + 4 + i * 8;
|
|
41
|
+
subs.push({ plat: buf.readUInt16BE(o), enc: buf.readUInt16BE(o + 2), off: cmap.off + buf.readUInt32BE(o + 4) });
|
|
42
|
+
}
|
|
43
|
+
subs.sort((a, b) => buf.readUInt16BE(b.off) - buf.readUInt16BE(a.off)); // o formato 12 primeiro
|
|
44
|
+
for (const s of subs) {
|
|
45
|
+
const fmt = buf.readUInt16BE(s.off);
|
|
46
|
+
if (fmt === 12) {
|
|
47
|
+
const grupos = buf.readUInt32BE(s.off + 12);
|
|
48
|
+
return (cp) => {
|
|
49
|
+
for (let g = 0; g < grupos; g++) {
|
|
50
|
+
const o = s.off + 16 + g * 12;
|
|
51
|
+
const ini = buf.readUInt32BE(o), fim = buf.readUInt32BE(o + 4);
|
|
52
|
+
if (cp >= ini && cp <= fim) return buf.readUInt32BE(o + 8) + (cp - ini);
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (fmt === 4) {
|
|
58
|
+
const segs = buf.readUInt16BE(s.off + 6) / 2;
|
|
59
|
+
const fins = s.off + 14, inis = fins + segs * 2 + 2, deltas = inis + segs * 2, ranges = deltas + segs * 2;
|
|
60
|
+
return (cp) => {
|
|
61
|
+
for (let i = 0; i < segs; i++) {
|
|
62
|
+
const fim = buf.readUInt16BE(fins + i * 2);
|
|
63
|
+
if (cp > fim) continue;
|
|
64
|
+
const ini = buf.readUInt16BE(inis + i * 2);
|
|
65
|
+
if (cp < ini) return 0;
|
|
66
|
+
const delta = buf.readInt16BE(deltas + i * 2), ro = buf.readUInt16BE(ranges + i * 2);
|
|
67
|
+
if (!ro) return (cp + delta) & 0xffff;
|
|
68
|
+
const g = buf.readUInt16BE(ranges + i * 2 + ro + (cp - ini) * 2);
|
|
69
|
+
return g ? (g + delta) & 0xffff : 0;
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return () => 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Os contornos de um glifo: [[{x, y, on}]], já com os componentes do composto. */
|
|
79
|
+
function contornos(f, gid, profundidade = 0) {
|
|
80
|
+
const a = f.loca(gid), b = f.loca(gid + 1);
|
|
81
|
+
if (b <= a || profundidade > 4) return [];
|
|
82
|
+
const buf = f.buf, o = f.glyf + a;
|
|
83
|
+
const nc = buf.readInt16BE(o);
|
|
84
|
+
if (nc < 0) {
|
|
85
|
+
// Composto: cada componente é outro glifo, deslocado (e às vezes escalado).
|
|
86
|
+
const saida = [];
|
|
87
|
+
let p = o + 10, flags;
|
|
88
|
+
do {
|
|
89
|
+
flags = buf.readUInt16BE(p); const g = buf.readUInt16BE(p + 2); p += 4;
|
|
90
|
+
let dx, dy;
|
|
91
|
+
if (flags & 1) { dx = buf.readInt16BE(p); dy = buf.readInt16BE(p + 2); p += 4; } else { dx = buf.readInt8(p); dy = buf.readInt8(p + 1); p += 2; }
|
|
92
|
+
let sx = 1, sy = 1;
|
|
93
|
+
if (flags & 8) { sx = sy = buf.readInt16BE(p) / 16384; p += 2; }
|
|
94
|
+
else if (flags & 0x40) { sx = buf.readInt16BE(p) / 16384; sy = buf.readInt16BE(p + 2) / 16384; p += 4; }
|
|
95
|
+
else if (flags & 0x80) { sx = buf.readInt16BE(p) / 16384; sy = buf.readInt16BE(p + 6) / 16384; p += 8; }
|
|
96
|
+
if (!(flags & 2)) { dx = 0; dy = 0; } // pontos casados em vez de deslocamento: raro em fonte do Google
|
|
97
|
+
for (const c of contornos(f, g, profundidade + 1)) saida.push(c.map((q) => ({ x: q.x * sx + dx, y: q.y * sy + dy, on: q.on })));
|
|
98
|
+
} while (flags & 0x20);
|
|
99
|
+
return saida;
|
|
100
|
+
}
|
|
101
|
+
const fins = [];
|
|
102
|
+
for (let i = 0; i < nc; i++) fins.push(buf.readUInt16BE(o + 10 + i * 2));
|
|
103
|
+
const n = nc ? fins[nc - 1] + 1 : 0;
|
|
104
|
+
let p = o + 10 + nc * 2;
|
|
105
|
+
p += 2 + buf.readUInt16BE(p); // pula as instruções
|
|
106
|
+
const fl = [];
|
|
107
|
+
while (fl.length < n) {
|
|
108
|
+
const f0 = buf.readUInt8(p++); fl.push(f0);
|
|
109
|
+
if (f0 & 8) { let r = buf.readUInt8(p++); while (r-- > 0) fl.push(f0); }
|
|
110
|
+
}
|
|
111
|
+
const coord = (curto, igual) => {
|
|
112
|
+
const v = []; let atual = 0;
|
|
113
|
+
for (let i = 0; i < n; i++) {
|
|
114
|
+
const q = fl[i];
|
|
115
|
+
if (q & curto) { const d = buf.readUInt8(p++); atual += q & igual ? d : -d; }
|
|
116
|
+
else if (!(q & igual)) { atual += buf.readInt16BE(p); p += 2; }
|
|
117
|
+
v.push(atual);
|
|
118
|
+
}
|
|
119
|
+
return v;
|
|
120
|
+
};
|
|
121
|
+
const xs = coord(2, 16), ys = coord(4, 32);
|
|
122
|
+
const saida = [];
|
|
123
|
+
let ini = 0;
|
|
124
|
+
for (const fim of fins) {
|
|
125
|
+
const c = [];
|
|
126
|
+
for (let i = ini; i <= fim; i++) c.push({ x: xs[i], y: ys[i], on: Boolean(fl[i] & 1) });
|
|
127
|
+
saida.push(c);
|
|
128
|
+
ini = fim + 1;
|
|
129
|
+
}
|
|
130
|
+
return saida;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function abrirFonte(buf) {
|
|
134
|
+
const t = tabelas(buf);
|
|
135
|
+
if (!t.glyf || !t.loca || !t.head || !t.cmap || !t.hmtx || !t.hhea) throw new Error('não é uma fonte TrueType com contornos (glyf)');
|
|
136
|
+
const longo = buf.readInt16BE(t.head.off + 50) === 1;
|
|
137
|
+
const nMetr = buf.readUInt16BE(t.hhea.off + 34);
|
|
138
|
+
return {
|
|
139
|
+
buf, glyf: t.glyf.off,
|
|
140
|
+
porEm: buf.readUInt16BE(t.head.off + 18),
|
|
141
|
+
loca: (i) => (longo ? buf.readUInt32BE(t.loca.off + i * 4) : buf.readUInt16BE(t.loca.off + i * 2) * 2),
|
|
142
|
+
avanco: (g) => buf.readUInt16BE(t.hmtx.off + Math.min(g, nMetr - 1) * 4),
|
|
143
|
+
glifo: mapaDeCaracteres(buf, t.cmap),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A palavra desenhada: {d, pontos: [[x, y, naCurva, contorno]], caixa: [x, y, w, h]}.
|
|
151
|
+
* Coordenadas em unidades da fonte, com y para baixo (como no SVG).
|
|
152
|
+
*/
|
|
153
|
+
function desenhar(buf, texto, { espacamento = 0 } = {}) {
|
|
154
|
+
const f = abrirFonte(buf);
|
|
155
|
+
let x = 0, nC = 0;
|
|
156
|
+
const d = [], pontos = [];
|
|
157
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
158
|
+
for (const ch of String(texto)) {
|
|
159
|
+
const g = f.glifo(ch.codePointAt(0));
|
|
160
|
+
for (const c of contornos(f, g)) {
|
|
161
|
+
if (c.length < 2) continue;
|
|
162
|
+
const P = c.map((q) => ({ x: r1(q.x + x), y: r1(-q.y), on: q.on }));
|
|
163
|
+
for (const q of P) {
|
|
164
|
+
pontos.push([q.x, q.y, q.on ? 1 : 0, nC]);
|
|
165
|
+
if (q.x < minX) minX = q.x; if (q.x > maxX) maxX = q.x; if (q.y < minY) minY = q.y; if (q.y > maxY) maxY = q.y;
|
|
166
|
+
}
|
|
167
|
+
// Quadrática com pontos implícitos: entre dois controles seguidos mora uma âncora no meio.
|
|
168
|
+
let k0 = P.findIndex((q) => q.on);
|
|
169
|
+
let inicio = k0 >= 0 ? P[k0] : { x: r1((P[0].x + P[1].x) / 2), y: r1((P[0].y + P[1].y) / 2), on: true };
|
|
170
|
+
if (k0 < 0) k0 = 0;
|
|
171
|
+
const seg = [`M ${inicio.x} ${inicio.y}`];
|
|
172
|
+
let ctrl = null;
|
|
173
|
+
for (let j = 1; j <= P.length; j++) {
|
|
174
|
+
const q = P[(k0 + j) % P.length];
|
|
175
|
+
if (q.on) { seg.push(ctrl ? `Q ${ctrl.x} ${ctrl.y} ${q.x} ${q.y}` : `L ${q.x} ${q.y}`); ctrl = null; }
|
|
176
|
+
else if (ctrl) { const m = { x: r1((ctrl.x + q.x) / 2), y: r1((ctrl.y + q.y) / 2) }; seg.push(`Q ${ctrl.x} ${ctrl.y} ${m.x} ${m.y}`); ctrl = q; }
|
|
177
|
+
else ctrl = q;
|
|
178
|
+
}
|
|
179
|
+
if (ctrl) seg.push(`Q ${ctrl.x} ${ctrl.y} ${inicio.x} ${inicio.y}`);
|
|
180
|
+
d.push(seg.join(' ') + ' Z');
|
|
181
|
+
nC++;
|
|
182
|
+
}
|
|
183
|
+
x += f.avanco(g) + espacamento * f.porEm;
|
|
184
|
+
}
|
|
185
|
+
if (!pontos.length) return null;
|
|
186
|
+
return { d: d.join(' '), pontos, caixa: [r1(minX), r1(minY), r1(maxX - minX), r1(maxY - minY)], porEm: f.porEm };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── a fonte, do Google Fonts para o cache ────────────────────────────────
|
|
190
|
+
const CACHE = path.join(os.homedir(), '.primocode', 'studio', 'cache-fontes');
|
|
191
|
+
|
|
192
|
+
async function baixarFonte(familia, peso = 700) {
|
|
193
|
+
const nome = String(familia).replace(/[^a-z0-9]+/gi, '-').toLowerCase() + '-' + peso + '.ttf';
|
|
194
|
+
const arq = path.join(CACHE, nome);
|
|
195
|
+
if (fs.existsSync(arq)) return arq;
|
|
196
|
+
// A API antiga (css, não css2) devolve .ttf; a nova devolve woff2, que não tem a glyf crua.
|
|
197
|
+
const css = await (await fetch('https://fonts.googleapis.com/css?family=' + encodeURIComponent(familia) + ':' + peso, { signal: AbortSignal.timeout(10000) })).text();
|
|
198
|
+
const url = (css.match(/url\((https:[^)]+\.ttf)\)/) || [])[1];
|
|
199
|
+
if (!url) return null;
|
|
200
|
+
const bin = Buffer.from(await (await fetch(url, { signal: AbortSignal.timeout(15000) })).arrayBuffer());
|
|
201
|
+
fs.mkdirSync(CACHE, { recursive: true });
|
|
202
|
+
fs.writeFileSync(arq, bin);
|
|
203
|
+
return arq;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A palavra em vetor na fonte da marca, ou null (sem rede, fonte sem glyf, palavra vazia). */
|
|
207
|
+
async function palavra(texto, { fonte = 'Plus Jakarta Sans', peso = 700 } = {}) {
|
|
208
|
+
if (!String(texto || '').trim()) return null;
|
|
209
|
+
try {
|
|
210
|
+
const arq = await baixarFonte(fonte, peso);
|
|
211
|
+
return arq ? desenhar(fs.readFileSync(arq), texto) : null;
|
|
212
|
+
} catch { return null; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { palavra, desenhar, abrirFonte, baixarFonte, CACHE };
|
package/lib/estudio/index.js
CHANGED
|
@@ -45,6 +45,13 @@ const composicao = require('./composicao.js');
|
|
|
45
45
|
const acervoSite = require('./acervo-site.js');
|
|
46
46
|
const impacto = require('./impacto.js');
|
|
47
47
|
const cinema = require('./cinema.js');
|
|
48
|
+
const motion = require('./motion.js');
|
|
49
|
+
const vox = require('./vox.js');
|
|
50
|
+
const lancamento = require('./lancamento.js');
|
|
51
|
+
const glifos = require('./glifos.js');
|
|
52
|
+
const { escolherEstilo, LEVES } = require('./estilo.js');
|
|
53
|
+
const sonsLeves = require('./sons-leves.js');
|
|
54
|
+
const ende = require('./ende.js');
|
|
48
55
|
|
|
49
56
|
const PY = path.join(__dirname, '..', '..', 'studio');
|
|
50
57
|
|
|
@@ -219,7 +226,8 @@ function roteiroValido(r) {
|
|
|
219
226
|
const partes = Array.isArray(x) ? x : x && x.partes;
|
|
220
227
|
if (!Array.isArray(partes) || partes.length < 2) return null;
|
|
221
228
|
const CINEMA = ['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'];
|
|
222
|
-
|
|
229
|
+
const NOVOS = ['logo', 'cores', 'produto']; // vox.js e lancamento.js
|
|
230
|
+
if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo) || NOVOS.includes(pt.tipo)))) return null;
|
|
223
231
|
const obj = Array.isArray(x) ? { partes } : x;
|
|
224
232
|
return partes.some((pt) => CINEMA.includes(pt.tipo)) && !obj.estilo ? { ...obj, estilo: 'cinema' } : obj;
|
|
225
233
|
}
|
|
@@ -242,6 +250,7 @@ async function garantirAcervo(p) {
|
|
|
242
250
|
/* O ROTEIRISTA do servidor: quando o modelo não manda um roteiro que preste
|
|
243
251
|
(nenhum, ou texto corrido), o pedido e os fatos da marca viram o roteiro
|
|
244
252
|
numa chamada só, com uma tarefa só. */
|
|
253
|
+
|
|
245
254
|
async function roteiroDoServidor(p, args) {
|
|
246
255
|
const cfg = require('../config').loadConfig();
|
|
247
256
|
const api = require('../api');
|
|
@@ -254,7 +263,8 @@ async function roteiroDoServidor(p, args) {
|
|
|
254
263
|
fotos: (acervo.fotos || []).length ? `${acervo.fotos.length} fotos reais` : '',
|
|
255
264
|
site: acervo.site ? new URL(acervo.site).hostname.replace(/^www\./, '') : '',
|
|
256
265
|
rascunho: typeof args.roteiro === 'string' ? args.roteiro : '',
|
|
257
|
-
|
|
266
|
+
// Pedido de motion nunca vira impacto, nem sendo anúncio.
|
|
267
|
+
estilo: args.estilo || (escolherEstilo({ pedido }) === 'impacto' ? 'impacto' : undefined),
|
|
258
268
|
}, cfg.token);
|
|
259
269
|
if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'o roteirista não respondeu.' };
|
|
260
270
|
return { ok: true, roteiro: { ...r.roteiro, marca: (args.roteiro && args.roteiro.marca) || p.marca || undefined } };
|
|
@@ -916,11 +926,15 @@ async function prepararRoteiro(p, args) {
|
|
|
916
926
|
if (roteiro.marca.logo && !existe(roteiro.marca.logo)) roteiro.marca.logo = (p.marca && existe(p.marca.logo)) ? p.marca.logo : undefined;
|
|
917
927
|
} else if (p.marca) roteiro.marca = p.marca;
|
|
918
928
|
const partes = roteiro.partes || [];
|
|
919
|
-
/* O ESTILO
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
929
|
+
/* O ESTILO (estilo.js). O padrão é MOTION (motion.js): um plano contínuo
|
|
930
|
+
em 3D — "isso sim é motion" (Joel, 28/09). Explicação vira VOX
|
|
931
|
+
(editorial, tinta sobre papel), marca vira MARCA (brand reveal),
|
|
932
|
+
lançamento de produto vira LANCAMENTO, anúncio e reels vão para o
|
|
933
|
+
IMPACTO. O que o agente pediu vale mais que tudo. */
|
|
934
|
+
const pedidoTxt = `${p.pedido || ''} ${args.pedido || ''}`;
|
|
935
|
+
const estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo });
|
|
936
|
+
// Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
|
|
937
|
+
const ehMotion = LEVES.has(estilo);
|
|
924
938
|
const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
|
|
925
939
|
const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'slides' ? PROPORCOES['16:9'] : PROPORCOES['16:9']);
|
|
926
940
|
|
|
@@ -1131,8 +1145,17 @@ async function prepararRoteiro(p, args) {
|
|
|
1131
1145
|
tempos = v.partes;
|
|
1132
1146
|
narracao = path.relative(p.pasta, mp3).replace(/\\/g, '/');
|
|
1133
1147
|
projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${partes.length} partes, voz ${v.voz}`);
|
|
1134
|
-
|
|
1135
|
-
|
|
1148
|
+
// O motion usa os efeitos leves; os estilos de corte, os cheios.
|
|
1149
|
+
sons = ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
|
|
1150
|
+
// A trilha do motion: ende.app (CC BY 4.0, sem Content ID), clima tecnologia.
|
|
1151
|
+
if (ehMotion) {
|
|
1152
|
+
try {
|
|
1153
|
+
const segundos = tempos.length ? tempos[tempos.length - 1].ate + 2 : 20;
|
|
1154
|
+
const m = await ende.provedor({ pasta: path.join(p.pasta, 'midia') }).gerar({ clima: roteiro.clima || 'tecnologia', segundos });
|
|
1155
|
+
if (m) { trilha = path.relative(p.pasta, m.src).replace(/\\/g, '/'); projeto.anotar(p.id, 'trilha', m.credito); }
|
|
1156
|
+
} catch { /* cai na busca de sempre */ }
|
|
1157
|
+
}
|
|
1158
|
+
if (!trilha) try {
|
|
1136
1159
|
const b = await assets.procurarMusica(roteiro.musica || args.trilha || (estilo === 'impacto' ? 'epic energetic' : 'inspiring corporate'), { quantas: 6, duracaoMin: 45 });
|
|
1137
1160
|
for (const item of (b.ok ? b.sons : [])) {
|
|
1138
1161
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
|
|
@@ -1156,7 +1179,36 @@ async function prepararRoteiro(p, args) {
|
|
|
1156
1179
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo impacto: ${r.resumo.cenas} batidas`);
|
|
1157
1180
|
return r;
|
|
1158
1181
|
}
|
|
1159
|
-
|
|
1182
|
+
if (estilo === 'vox' && modo === 'video') {
|
|
1183
|
+
const r = vox.montarVox({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
|
|
1184
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo vox: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
|
|
1185
|
+
return r;
|
|
1186
|
+
}
|
|
1187
|
+
if ((estilo === 'marca' || estilo === 'lancamento') && modo === 'video') {
|
|
1188
|
+
/* As letras em vetor (a peça "vetor") pedem os pontos de verdade da
|
|
1189
|
+
fonte: glifos.js lê o .ttf. Sem rede, o vetor desenha o contorno do
|
|
1190
|
+
próprio texto, sem as alças. */
|
|
1191
|
+
const m = roteiro.marca || {};
|
|
1192
|
+
const nome = m.nome || (partes.find((pt) => pt.tipo === 'logo' || pt.tipo === 'abertura') || {}).titulo || '';
|
|
1193
|
+
const precisa = partes.some((pt) => pt.tipo === 'logo' || pt.visual === 'vetor');
|
|
1194
|
+
const g = precisa ? await glifos.palavra((partes.find((pt) => pt.tipo === 'logo') || {}).titulo || nome,
|
|
1195
|
+
{ fonte: (m.fontes && m.fontes.titulo) || (estilo === 'marca' ? 'Plus Jakarta Sans' : 'Inter Tight') }) : null;
|
|
1196
|
+
const r = lancamento.montarLancamento({ roteiro, variante: estilo, largura, altura, tempos, narracao, sons, trilha, imagens, glifos: g });
|
|
1197
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${estilo}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s${precisa ? (g ? ', vetor com os pontos da fonte' : ', vetor sem os pontos (sem a fonte)') : ''}`);
|
|
1198
|
+
return r;
|
|
1199
|
+
}
|
|
1200
|
+
if (estilo === 'motion' && modo === 'video') {
|
|
1201
|
+
const r = motion.montarMotion({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
|
|
1202
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo motion: ${r.resumo.segundos}s contínuos, ${r.resumo.partes} partes, ${r.resumo.fps} fps`);
|
|
1203
|
+
return r;
|
|
1204
|
+
}
|
|
1205
|
+
/* A composição (e os slides) só conhece os tipos antigos: as partes novas
|
|
1206
|
+
viram a mais parecida delas, e o roteiro do vídeo serve à página. */
|
|
1207
|
+
const PARECIDA = { logo: 'abertura', cores: 'lista', produto: 'titulo' };
|
|
1208
|
+
const paraComposicao = { ...roteiro, partes: partes.map((pt) => (PARECIDA[pt.tipo]
|
|
1209
|
+
? { ...pt, tipo: PARECIDA[pt.tipo], itens: pt.itens || (pt.cores || []).map((c) => (typeof c === 'string' ? c : c.nome)) }
|
|
1210
|
+
: pt)) };
|
|
1211
|
+
return composicao.comporRoteiro({ roteiro: paraComposicao, modo, largura, altura, tempos, narracao, sons, trilha, imagens, iconesOk, fundos });
|
|
1160
1212
|
}
|
|
1161
1213
|
|
|
1162
1214
|
/* Sem o tempo de cada palavra, cada trecho é repartido pelas palavras dele,
|
|
@@ -1435,5 +1487,5 @@ async function ferramentaSite(args) {
|
|
|
1435
1487
|
module.exports = {
|
|
1436
1488
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
1437
1489
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
1438
|
-
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes,
|
|
1490
|
+
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro,
|
|
1439
1491
|
};
|