primocode 10.1.0 → 10.3.0-beta.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 +6 -26
- package/lib/act.js +5 -0
- package/lib/browser.js +1 -1
- package/lib/catalogo.js +17 -8
- package/lib/estudio/assets.js +75 -1
- package/lib/estudio/assinatura.js +108 -0
- package/lib/estudio/assinaturas/fluido.json +15 -0
- package/lib/estudio/assinaturas/gerativo.json +15 -0
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +52 -12
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/diretor.js +70 -4
- package/lib/estudio/edicao.js +34 -16
- package/lib/estudio/efeitos.js +95 -0
- package/lib/estudio/estilo.js +17 -6
- package/lib/estudio/falantes.js +97 -0
- package/lib/estudio/fluido.js +110 -16
- package/lib/estudio/gerativo.js +309 -85
- package/lib/estudio/hibrido.js +1 -1
- package/lib/estudio/index.js +252 -45
- package/lib/estudio/kit/fluido.jsx +130 -9
- package/lib/estudio/kit/gerativo.jsx +90 -21
- package/lib/estudio/kit/marca.jsx +58 -1
- package/lib/estudio/kit/tres.jsx +4 -1
- package/lib/estudio/kit.js +2 -0
- package/lib/estudio/plano.js +5 -1
- package/lib/estudio/print-site.js +152 -0
- package/lib/estudio/referencias.js +47 -25
- package/lib/estudio/remotion.js +74 -3
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variedade.js +83 -21
- package/lib/video/filters.js +6 -1
- package/lib/video/ops.js +4 -3
- package/lib/video/timeline.js +1 -1
- package/lib/video/zoom.js +275 -0
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +3 -5
|
@@ -113,12 +113,47 @@ const norm = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toL
|
|
|
113
113
|
function acharFonte(nome) { const n = norm(nome); return FONTES.find((f) => norm(f.nome) === n) || null; }
|
|
114
114
|
function acharPaleta(nome) { const n = norm(nome).replace(/\s+/g, '-'); return PALETAS.find((p) => p.nome === n) || null; }
|
|
115
115
|
|
|
116
|
-
/* O
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
/* O CASAMENTO DA FONTE COM O ROTEIRO (07/10): "a IA deve escolher fontes e
|
|
117
|
+
combinações de acordo com assunto, tom, público e ritmo" (Joel). Antes eram
|
|
118
|
+
dois sorteios independentes. Agora:
|
|
119
|
+
- o CLIMA do roteiro (o mesmo que escolhe a trilha, variedade.climaDe) diz
|
|
120
|
+
quanto cada tipo de título serve (0 a 1);
|
|
121
|
+
- o tipo do título diz que tipo de texto faz par com ele (contraste, não
|
|
122
|
+
duas fontes que brigam);
|
|
123
|
+
- a memória penaliza o recente pela distância, não corta: o que serve ao
|
|
124
|
+
tom ganha do que só é novo (os pontos de variedade.js). */
|
|
125
|
+
const TOM_DO_TITULO = {
|
|
126
|
+
tecnologia: { display: 1, larga: 0.9, condensada: 0.6, texto: 0.7, serifa: 0.2 },
|
|
127
|
+
energetico: { condensada: 1, display: 0.9, larga: 0.6, texto: 0.5, serifa: 0.2 },
|
|
128
|
+
epico: { condensada: 1, serifa: 0.8, display: 0.6, larga: 0.5, texto: 0.3 },
|
|
129
|
+
calmo: { serifa: 1, texto: 0.9, display: 0.5, condensada: 0.3, larga: 0.2 },
|
|
130
|
+
alegre: { display: 1, larga: 0.8, texto: 0.6, condensada: 0.4, serifa: 0.4 },
|
|
131
|
+
emocional: { serifa: 1, texto: 0.7, display: 0.5, condensada: 0.4, larga: 0.2 },
|
|
132
|
+
misterio: { condensada: 0.9, serifa: 0.9, larga: 0.6, display: 0.5, texto: 0.4 },
|
|
133
|
+
};
|
|
134
|
+
// O par do texto, pelo tipo do título: o contraste que funciona.
|
|
135
|
+
const PAR_DO_TEXTO = {
|
|
136
|
+
condensada: { texto: 1, mono: 0.8, serifa: 0.6, display: 0.4 },
|
|
137
|
+
display: { texto: 1, serifa: 0.7, mono: 0.6, display: 0.2 },
|
|
138
|
+
larga: { mono: 1, texto: 0.9, serifa: 0.4, display: 0.2 },
|
|
139
|
+
serifa: { texto: 1, mono: 0.7, display: 0.5, serifa: 0.3 },
|
|
140
|
+
texto: { serifa: 1, mono: 0.9, texto: 0.3, display: 0.5 },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** O tom do roteiro para a fonte: o clima (variedade.climaDe). Neutro serve tudo igual. */
|
|
144
|
+
function tomDe(roteiro, estilo = '') { return require('./variedade.js').climaDe(roteiro || {}, estilo); }
|
|
145
|
+
|
|
146
|
+
/* A escolha por pontos: 2 × adequação − recência (1 para o último usado,
|
|
147
|
+
metade a cada quarto da memória). Entre os quase empatados, sorteia. */
|
|
148
|
+
function escolher(lista, recentes, chave, sorte, adequa = () => 1, lembra = 10) {
|
|
149
|
+
const meia = Math.max(1, lembra / 4);
|
|
150
|
+
const pts = lista.map((x) => {
|
|
151
|
+
const pos = recentes.indexOf(chave(x));
|
|
152
|
+
return { x, p: 2 * adequa(x) - (pos < 0 ? 0 : Math.pow(0.5, pos / meia)) };
|
|
153
|
+
});
|
|
154
|
+
const melhor = Math.max(...pts.map((q) => q.p));
|
|
155
|
+
const quase = pts.filter((q) => q.p >= melhor - 0.05).map((q) => q.x);
|
|
156
|
+
return quase[Math.floor(sorte() * quase.length) % quase.length];
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
/**
|
|
@@ -126,7 +161,7 @@ function sortear(lista, recentes, chave, sorte) {
|
|
|
126
161
|
* trocar nada que a marca já tem. Devolve o que escolheu e de onde veio.
|
|
127
162
|
* memoria: null não lê nem grava o disco (os testes); sorte: () => [0,1).
|
|
128
163
|
*/
|
|
129
|
-
function vestir(roteiro, { memoria = ARQ, sorte = Math.random } = {}) {
|
|
164
|
+
function vestir(roteiro, { memoria = ARQ, sorte = Math.random, estilo = '' } = {}) {
|
|
130
165
|
if (!roteiro || typeof roteiro !== 'object') return { mudou: false };
|
|
131
166
|
const marca = roteiro.marca || {};
|
|
132
167
|
const cores = marca.cores || {};
|
|
@@ -136,17 +171,22 @@ function vestir(roteiro, { memoria = ARQ, sorte = Math.random } = {}) {
|
|
|
136
171
|
|
|
137
172
|
if (!(marca.fontes && marca.fontes.titulo)) {
|
|
138
173
|
// A IA escolheu pelo nome: vale qualquer família do Google Fonts, mesmo fora do cardápio.
|
|
139
|
-
const
|
|
174
|
+
const tom = TOM_DO_TITULO[tomDe(roteiro, estilo)] || null;
|
|
175
|
+
const titulo = visual.titulo ? String(visual.titulo)
|
|
176
|
+
: escolher(TITULO, mem.titulo, (f) => f.nome, sorte, (f) => (tom ? tom[f.tipo] ?? 0.3 : 1), LEMBRA.titulo).nome;
|
|
177
|
+
const tipoTitulo = (acharFonte(titulo) || {}).tipo;
|
|
178
|
+
const par = PAR_DO_TEXTO[tipoTitulo] || null;
|
|
140
179
|
const textoLista = TEXTO.filter((f) => f.nome !== titulo);
|
|
141
|
-
const texto = visual.texto ? String(visual.texto)
|
|
180
|
+
const texto = visual.texto ? String(visual.texto)
|
|
181
|
+
: escolher(textoLista, mem.texto, (f) => f.nome, sorte, (f) => (par ? par[f.tipo] ?? 0.2 : 1), LEMBRA.texto).nome;
|
|
142
182
|
marca.fontes = { ...(marca.fontes || {}), titulo, texto };
|
|
143
183
|
mem.titulo = [titulo, ...mem.titulo.filter((x) => x !== titulo)].slice(0, LEMBRA.titulo);
|
|
144
184
|
mem.texto = [texto, ...mem.texto.filter((x) => x !== texto)].slice(0, LEMBRA.texto);
|
|
145
|
-
Object.assign(escolha, { mudou: true, titulo, texto, fonteDaIA: !!visual.titulo });
|
|
185
|
+
Object.assign(escolha, { mudou: true, titulo, texto, fonteDaIA: !!visual.titulo, tom: tomDe(roteiro, estilo) });
|
|
146
186
|
}
|
|
147
187
|
// A cor da marca manda: com destaque ou gradiente declarado, a paleta não entra.
|
|
148
188
|
if (!(marca.gradiente && marca.gradiente.length >= 2) && !cores.destaque) {
|
|
149
|
-
const p = acharPaleta(visual.paleta) ||
|
|
189
|
+
const p = acharPaleta(visual.paleta) || escolher(PALETAS, mem.paleta, (x) => x.base, sorte, () => 1, LEMBRA.paleta);
|
|
150
190
|
marca.gradiente = [...p.grad];
|
|
151
191
|
marca.cores = { ...cores, fundo: cores.fundo || p.fundo, destaque: p.grad[1], apoio: p.grad[0] };
|
|
152
192
|
mem.paleta = [p.base, ...mem.paleta.filter((x) => x !== p.base)].slice(0, LEMBRA.paleta);
|
|
@@ -171,4 +211,4 @@ function cardapio() {
|
|
|
171
211
|
};
|
|
172
212
|
}
|
|
173
213
|
|
|
174
|
-
module.exports = { vestir, cardapio, larguraDe, acharFonte, acharPaleta, FONTES, PALETAS, BASES, TITULO, TEXTO, luz, ARQ };
|
|
214
|
+
module.exports = { vestir, cardapio, tomDe, TOM_DO_TITULO, PAR_DO_TEXTO, larguraDe, acharFonte, acharPaleta, FONTES, PALETAS, BASES, TITULO, TEXTO, luz, ARQ };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decisao.js — o que cada frase da edição pede: movimento, recorte, som — ou
|
|
3
|
+
* nada, e a cena respira.
|
|
4
|
+
*
|
|
5
|
+
* "Não quero simplesmente adicionar efeitos. Quero melhorar a decisão de
|
|
6
|
+
* edição. Nem todo momento precisa de movimento, transição ou efeito. Às
|
|
7
|
+
* vezes a melhor decisão é deixar a cena respirar." (Joel, 07/10)
|
|
8
|
+
*
|
|
9
|
+
* Antes a edição era um rodízio: zoom por `i % 3`, a pessoa recortada em
|
|
10
|
+
* `i % 4 === 2`, whoosh em `i % 3 === 1`. A frase mais forte do vídeo podia
|
|
11
|
+
* cair num "parado", e uma frase de ligação ganhava o soco. Aqui:
|
|
12
|
+
* 1. INTENSIDADE (0 a 1) de cada frase: a palavra mais forte, número,
|
|
13
|
+
* exclamação e pergunta, frase curta de efeito, a pausa antes dela (o
|
|
14
|
+
* silêncio prepara o golpe), o gancho do começo e o fecho;
|
|
15
|
+
* 2. MOVIMENTO pela intensidade, com três regras de editor: o golpe é raro
|
|
16
|
+
* (no máximo ~1 em 3 frases), depois do golpe vem um respiro, e o mesmo
|
|
17
|
+
* movimento não se repete em seguida;
|
|
18
|
+
* 3. o RECORTE (a pessoa na frente da palavra gigante) vai para as frases
|
|
19
|
+
* mais fortes, longe uma da outra; o WHOOSH só anuncia o soco.
|
|
20
|
+
* Puro: frases entram, decisões saem. `aleatorio` decide os empates.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
26
|
+
const limpa = (t) => String(t || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9%]/g, '');
|
|
27
|
+
|
|
28
|
+
/* Os movimentos de câmera (zoom em quadros-chave, no tempo da cena):
|
|
29
|
+
parado nada — a cena respira
|
|
30
|
+
empurra aproxima devagar (tensão que cresce)
|
|
31
|
+
soco entra já fechado e segura (o golpe)
|
|
32
|
+
recua abre devagar (alívio, conclusão)
|
|
33
|
+
reenquadra fecha num ponto fora do centro (mudança de assunto) */
|
|
34
|
+
const MOVIMENTOS = {
|
|
35
|
+
parado: () => ({ zoom: [{ t: 0, v: 1.0 }, { t: 1, v: 1.0 }] }),
|
|
36
|
+
empurra: (d, k) => ({ zoom: [{ t: 0, v: 1.0 }, { t: d, v: r2(1.04 + 0.06 * k), e: 'linear' }] }),
|
|
37
|
+
soco: (d, k) => ({ zoom: [{ t: 0, v: r2(1.14 + 0.08 * k) }, { t: d, v: r2(1.17 + 0.08 * k), e: 'linear' }] }),
|
|
38
|
+
recua: (d, k) => ({ zoom: [{ t: 0, v: r2(1.1 + 0.05 * k) }, { t: d, v: 1.02, e: 'suave' }] }),
|
|
39
|
+
// O x é em px: 3,5% do lado menor cabe na sobra de um zoom de 1,12.
|
|
40
|
+
reenquadra: (d, k, lado, base = 1080) => ({ zoom: [{ t: 0, v: r2(1.12 + 0.04 * k) }, { t: d, v: r2(1.15 + 0.04 * k), e: 'suave' }],
|
|
41
|
+
x: [{ t: 0, v: Math.round((lado ? 1 : -1) * base * 0.03) }, { t: d, v: Math.round((lado ? 1 : -1) * base * 0.035), e: 'suave' }] }),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A intensidade de cada frase (lista de palavras {de, ate, texto}), de 0 a 1. */
|
|
45
|
+
function intensidades(grupos, peso = (w) => limpa(w).length) {
|
|
46
|
+
const n = grupos.length;
|
|
47
|
+
const brutos = grupos.map((g, i) => {
|
|
48
|
+
const texto = g.map((p) => p.texto).join(' ');
|
|
49
|
+
const forte = Math.max(0, ...g.map((p) => peso(p.texto)));
|
|
50
|
+
let s = Math.min(1, forte / 14) * 0.45;
|
|
51
|
+
if (/\d/.test(texto)) s += 0.2;
|
|
52
|
+
if (/!/.test(texto)) s += 0.2;
|
|
53
|
+
if (/\?/.test(texto)) s += 0.1;
|
|
54
|
+
if (g.length <= 4) s += 0.15; // frase curta é frase de efeito
|
|
55
|
+
const pausa = i > 0 ? g[0].de - grupos[i - 1][grupos[i - 1].length - 1].ate : 0;
|
|
56
|
+
if (pausa > 0.45) s += Math.min(0.2, pausa * 0.25); // o silêncio prepara o golpe
|
|
57
|
+
if (i === 0) s += 0.15; // o gancho
|
|
58
|
+
if (i === n - 1) s += 0.1; // o fecho
|
|
59
|
+
return s;
|
|
60
|
+
});
|
|
61
|
+
const max = Math.max(...brutos, 1e-9), min = Math.min(...brutos);
|
|
62
|
+
return brutos.map((s) => r2(max - min < 1e-6 ? 0.5 : (s - min) / (max - min)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* As decisões da edição: uma por frase.
|
|
67
|
+
* @returns {Array<{intensidade:number, movimento:string, recorte:boolean, whoosh:boolean}>}
|
|
68
|
+
*/
|
|
69
|
+
function decidir(grupos, { peso, aleatorio = Math.random, maxRecortes = 3 } = {}) {
|
|
70
|
+
const ints = intensidades(grupos, peso);
|
|
71
|
+
const n = ints.length;
|
|
72
|
+
// O golpe é raro: só as frases do terço de cima (pelo menos 1, no máximo n/3).
|
|
73
|
+
const ordem = [...ints.keys()].sort((a, b) => ints[b] - ints[a]);
|
|
74
|
+
const golpes = new Set(ordem.slice(0, Math.max(1, Math.floor(n / 3))).filter((i) => ints[i] >= 0.6 || i === ordem[0]));
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i < n; i++) {
|
|
77
|
+
const ant = out[i - 1];
|
|
78
|
+
let mov;
|
|
79
|
+
if (golpes.has(i) && !(ant && ant.movimento === 'soco')) mov = 'soco';
|
|
80
|
+
else if (ant && ant.movimento === 'soco') mov = 'parado'; // depois do golpe, respira
|
|
81
|
+
else if (i === n - 1) mov = ints[i] > 0.4 ? 'recua' : 'parado';
|
|
82
|
+
else {
|
|
83
|
+
// O resto pelo que a frase pede, sem repetir o anterior.
|
|
84
|
+
const pede = ints[i] > 0.45 ? ['reenquadra', 'empurra'] : ints[i] > 0.2 ? ['empurra', 'parado', 'reenquadra'] : ['parado', 'empurra'];
|
|
85
|
+
const opcoes = pede.filter((m) => !ant || m !== ant.movimento);
|
|
86
|
+
mov = opcoes[Math.floor(aleatorio() * Math.min(2, opcoes.length)) % opcoes.length];
|
|
87
|
+
}
|
|
88
|
+
out.push({ intensidade: ints[i], movimento: mov, recorte: false, whoosh: mov === 'soco' && i > 0 });
|
|
89
|
+
}
|
|
90
|
+
// O recorte: as frases mais fortes, com pelo menos 3 frases entre um e outro.
|
|
91
|
+
const escolhidos = [];
|
|
92
|
+
for (const i of ordem) {
|
|
93
|
+
if (escolhidos.length >= maxRecortes || ints[i] < 0.5) break;
|
|
94
|
+
if (i === 0 || escolhidos.some((j) => Math.abs(j - i) < 3)) continue;
|
|
95
|
+
escolhidos.push(i);
|
|
96
|
+
out[i].recorte = true;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A câmera da cena para o movimento decidido. `lado` alterna o reenquadramento. */
|
|
102
|
+
function camera(movimento, dur, intensidade = 0.5, lado = 0, base = 1080) {
|
|
103
|
+
return (MOVIMENTOS[movimento] || MOVIMENTOS.parado)(dur, Math.max(0, Math.min(1, intensidade)), lado, base);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { intensidades, decidir, camera, MOVIMENTOS };
|
package/lib/estudio/diretor.js
CHANGED
|
@@ -115,9 +115,62 @@ function ritmoPara(segundos) {
|
|
|
115
115
|
palavrasPorCena: Math.round(media * 2.6) }; // ~155 palavras/minuto de narração
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// ── 2b. a estrutura: um cardápio, e a memória do que já saiu ─────────────
|
|
119
|
+
|
|
120
|
+
/* "o roteiro vicia: todo vídeo começa igual, não escolhe outra estrutura"
|
|
121
|
+
(Joel, 09/10). A regra 3 do diretor tinha UM arco só — prende em 2
|
|
122
|
+
segundos, entrega, pede ação — e o produtor nem isso via: todo roteiro
|
|
123
|
+
saía com a mesma abertura de impacto e o mesmo miolo de lista. Aqui não
|
|
124
|
+
tem regex escolhendo pelo pedido: o modelo recebe o CARDÁPIO (cada
|
|
125
|
+
estrutura com o "quando serve" e o jeito de abrir) e o que os últimos
|
|
126
|
+
vídeos já usaram. Quem escolhe é ele; o código só lembra. */
|
|
127
|
+
const ESTRUTURAS = [
|
|
128
|
+
{ id: 'historia', nome: 'história', quando: 'tem uma pessoa, um caso real ou um antes e depois', abre: 'uma cena concreta: alguém, um lugar, um momento ("Às 6h, a Ana abriu o caixa e…")' },
|
|
129
|
+
{ id: 'problema-solucao', nome: 'problema → solução', quando: 'o assunto resolve uma dor clara', abre: 'a dor dita como quem sente ("Você perde duas horas por semana com…")' },
|
|
130
|
+
{ id: 'lista', nome: 'lista', quando: 'são dicas, razões ou passos que se sustentam sozinhos', abre: 'a promessa com o número ("3 erros que…")' },
|
|
131
|
+
{ id: 'pergunta-resposta', nome: 'pergunta → resposta', quando: 'o público já tem uma dúvida', abre: 'a pergunta, direta, sem a resposta ainda' },
|
|
132
|
+
{ id: 'dialogo', nome: 'diálogo', quando: 'o pedido fala em duas pessoas, duas vozes, conversa ou entrevista', abre: 'a fala de um, e o outro responde' },
|
|
133
|
+
{ id: 'demonstracao', nome: 'demonstração', quando: 'o valor está em VER funcionando', abre: 'o resultado pronto primeiro; depois, como se chega lá' },
|
|
134
|
+
{ id: 'mito-verdade', nome: 'contraste (mito × verdade)', quando: 'existe uma crença comum que está errada', abre: 'o mito, dito como se fosse verdade' },
|
|
135
|
+
{ id: 'dado-surpresa', nome: 'dado que surpreende', quando: 'há um número que muda o jeito de ver o assunto', abre: 'o número sozinho, e a frase que diz por que ele importa' },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const vari = () => require('./variedade.js'); // tardio: a memória mora em ~/.primocode/studio
|
|
139
|
+
const idEstrutura = (x) => String(x || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
140
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
|
|
141
|
+
|
|
142
|
+
/** As últimas estruturas e aberturas que saíram em vídeo, mais nova primeiro. */
|
|
143
|
+
function recentes(n = 3) {
|
|
144
|
+
return { estruturas: vari().usadas('estrutura').slice(0, n), aberturas: vari().usadas('abertura').slice(0, n) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** O cardápio em texto, para o prompt — com o que foi usado por último. */
|
|
148
|
+
function cardapioEstruturas({ usadas = recentes() } = {}) {
|
|
149
|
+
const linhas = ESTRUTURAS.map((e) => `- ${e.id} (${e.nome}): se ${e.quando}. Abre com ${e.abre}.`);
|
|
150
|
+
if (usadas.estruturas.length) linhas.push(`Usadas recentemente: ${usadas.estruturas.join(', ')} — prefira outra, a menos que o pedido peça essa.`);
|
|
151
|
+
if (usadas.aberturas.length) linhas.push(`Os últimos vídeos começaram assim: ${usadas.aberturas.map((a) => `"${a}"`).join(' · ')} — não comece do mesmo jeito.`);
|
|
152
|
+
linhas.push('Nenhuma serve? Invente a estrutura e dê um nome a ela.');
|
|
153
|
+
return 'CARDÁPIO DE ESTRUTURAS (você escolhe pelo pedido):\n' + linhas.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Depois que o roteiro virou plano: guarda a estrutura que o modelo escolheu
|
|
158
|
+
* e como o vídeo abriu. Aceita o roteiro (partes/fala) ou o plano do diretor
|
|
159
|
+
* (cenas/narracao). Não julga a escolha — só lembra dela.
|
|
160
|
+
*/
|
|
161
|
+
function registrarEstrutura(r) {
|
|
162
|
+
const x = r && typeof r === 'object' ? r : {};
|
|
163
|
+
const estrutura = idEstrutura(x.estrutura) || null;
|
|
164
|
+
const p0 = (x.partes || x.cenas || [])[0] || {};
|
|
165
|
+
const abertura = String(p0.fala || p0.narracao || p0.titulo || '').split(/\s+/).filter(Boolean).slice(0, 8).join(' ') || null;
|
|
166
|
+
if (estrutura) vari().marcar('estrutura', estrutura);
|
|
167
|
+
if (abertura) vari().marcar('abertura', abertura);
|
|
168
|
+
return { estrutura, abertura };
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
// ── 3. o plano ───────────────────────────────────────────────────────────
|
|
119
172
|
|
|
120
|
-
const CAMPOS_CENA = ['titulo', 'narracao', 'duracao', 'aTela', 'fundo', 'transicao',
|
|
173
|
+
const CAMPOS_CENA = ['titulo', 'falante', 'narracao', 'duracao', 'aTela', 'fundo', 'transicao',
|
|
121
174
|
'elementos', 'midia', 'camera', 'chaves', 'notas'];
|
|
122
175
|
|
|
123
176
|
/**
|
|
@@ -170,6 +223,7 @@ function normalizarPlano(bruto, { segundos } = {}) {
|
|
|
170
223
|
tom: p.tom || 'neutro',
|
|
171
224
|
publico: p.publico || null,
|
|
172
225
|
promessa: p.promessa || null, // o que a pessoa leva do vídeo
|
|
226
|
+
estrutura: idEstrutura(p.estrutura) || null,
|
|
173
227
|
segundos: total,
|
|
174
228
|
voz: vozPara({ tom: p.tom, idioma: p.idioma || 'pt-BR',
|
|
175
229
|
genero: p.voz && p.voz.genero }),
|
|
@@ -181,6 +235,9 @@ function normalizarPlano(bruto, { segundos } = {}) {
|
|
|
181
235
|
midia: limpas.flatMap((c, i) => c.midia.map((m) => ({ cena: i + 1, ...m }))),
|
|
182
236
|
creditos: !!p.creditos,
|
|
183
237
|
};
|
|
238
|
+
// Duas vozes (falantes.js): o mapa e quem fala cada cena, quando o diretor escreveu diálogo.
|
|
239
|
+
const { vozes } = require('./falantes.js').normalizar({ vozes: p.vozes, partes: limpas });
|
|
240
|
+
if (vozes) plano.vozes = vozes;
|
|
184
241
|
return { plano, defeitos, ok: defeitos.length === 0 };
|
|
185
242
|
}
|
|
186
243
|
|
|
@@ -197,6 +254,7 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
|
|
|
197
254
|
|
|
198
255
|
{
|
|
199
256
|
"titulo": "…",
|
|
257
|
+
"estrutura": "o id que você escolheu no cardápio abaixo (ou o nome da que inventou)",
|
|
200
258
|
"formato": "${formato}",
|
|
201
259
|
"proporcao": "16:9 | 9:16 | 1:1 | 4:5",
|
|
202
260
|
"visual": "a direção de arte em uma ou duas frases: paleta (diga as cores em hex), que ar tem (limpo, denso, luminoso, sujo), que tipografia pede. NÃO existe lista de temas — descreva o que este vídeo tem de próprio.",
|
|
@@ -204,9 +262,11 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
|
|
|
204
262
|
"publico": "para quem é",
|
|
205
263
|
"promessa": "o que a pessoa leva ao terminar de ver",
|
|
206
264
|
"trilha": { "busca": "termo de busca da música", "humor": "…", "volume": 0.18 },
|
|
265
|
+
"vozes": { "nome de quem fala": "antonio | thalita | francisca" },
|
|
207
266
|
"cenas": [
|
|
208
267
|
{
|
|
209
268
|
"titulo": "o que aparece grande na tela",
|
|
269
|
+
"falante": "quem fala esta cena (um nome de vozes) — só quando houver mais de uma voz",
|
|
210
270
|
"narracao": "a frase FALADA, escrita para ser lida em voz alta",
|
|
211
271
|
"duracao": 3.5,
|
|
212
272
|
"aTela": "o que se vê nesta cena, descrito como você descreveria para um motion designer: onde está cada coisa, que tamanho tem, o que domina o quadro",
|
|
@@ -223,13 +283,16 @@ Devolva SÓ um bloco \`\`\`json com este formato, sem texto antes nem depois:
|
|
|
223
283
|
REGRAS QUE SEPARAM UM VÍDEO BOM DE UM GENÉRICO:
|
|
224
284
|
1. NARRAÇÃO É FALA, NÃO LEGENDA. "Vantagens" não é narração. Escreva a frase que a pessoa ouve.
|
|
225
285
|
2. O QUE SE OUVE E O QUE SE VÊ NÃO SE REPETEM. A tela mostra a prova (número, imagem, logo); a voz conta o que ela significa.
|
|
226
|
-
3.
|
|
286
|
+
3. ESTRUTURA. Escolha no cardápio do fim a que serve a ESTE pedido; a primeira cena abre do jeito dela e prende em 2 segundos, a última pede uma ação. Sem "Introdução / Desenvolvimento / Conclusão".
|
|
227
287
|
4. NÚMERO É CONCRETO. Nada de "muitos clientes": ou tem o número, ou a frase muda.
|
|
228
288
|
5. RITMO VARIADO. Cenas de durações diferentes e transições diferentes. Tudo em fade parece slide.
|
|
229
289
|
5b. NADA DE GAVETA. Não existe tema pronto, layout pronto nem fundo pronto: é você que decide a cor, a tipografia e a composição DESTE vídeo. Dois vídeos seus sobre assuntos diferentes não podem sair parecidos.
|
|
230
290
|
6. MÍDIA COM PROPÓSITO. Só peça imagem ou logo quando ela PROVA algo. Diga o termo de busca de verdade ("painel solar em telhado residencial", não "imagem legal").
|
|
231
291
|
7. MOVIMENTO. Em cada cena diga em "chaves" o movimento principal — é ele que tira a cara de apresentação parada.
|
|
232
|
-
8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar"
|
|
292
|
+
8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar".
|
|
293
|
+
9. UMA VOZ OU DUAS. Se o pedido fala em dois narradores, duas vozes, diálogo, conversa ou entrevista, escreva como DIÁLOGO: dê nome a cada um em "vozes" (com vozes diferentes) e diga em cada cena quem fala ("falante"). Com uma voz só, deixe "vozes" e "falante" de fora.
|
|
294
|
+
|
|
295
|
+
${cardapioEstruturas()}`;
|
|
233
296
|
};
|
|
234
297
|
|
|
235
298
|
const REVISAO = (defeitos) => `O plano voltou com defeitos de FORMA. Corrija SÓ o que está listado e devolva o JSON inteiro de novo, no mesmo formato:
|
|
@@ -289,9 +352,11 @@ function pauta(plano) {
|
|
|
289
352
|
linhas.push(`PLANO DE PRODUÇÃO — ${p.titulo}`);
|
|
290
353
|
linhas.push(`formato ${p.formato} · ${p.proporcao} · ${p.segundos}s · tom ${p.tom}`);
|
|
291
354
|
if (p.visual) linhas.push(`visual: ${p.visual}`);
|
|
355
|
+
if (p.estrutura) linhas.push(`estrutura: ${p.estrutura} (mande em roteiro.estrutura)`);
|
|
292
356
|
if (p.promessa) linhas.push(`promessa: ${p.promessa}`);
|
|
293
357
|
if (p.publico) linhas.push(`público: ${p.publico}`);
|
|
294
358
|
linhas.push(`voz: ${p.voz && p.voz.perfil} · ${p.voz && p.voz.instrucao} · ${p.voz && p.voz.idioma}`);
|
|
359
|
+
if (p.vozes) linhas.push(`vozes: ${Object.entries(p.vozes).map(([k, v]) => `${k} = ${v}`).join(', ')} (mande em roteiro.vozes, e falante em cada parte)`);
|
|
295
360
|
if (p.trilha) linhas.push(`trilha: ${p.trilha.busca || p.trilha.arquivo} (volume ${p.trilha.volume})`);
|
|
296
361
|
linhas.push('');
|
|
297
362
|
(p.cenas || []).forEach((c, i) => {
|
|
@@ -299,7 +364,7 @@ function pauta(plano) {
|
|
|
299
364
|
if (c.titulo) linhas.push(` na tela: ${c.titulo}`);
|
|
300
365
|
if (c.aTela) linhas.push(` composição: ${c.aTela}`);
|
|
301
366
|
if (c.fundo) linhas.push(` fundo: ${typeof c.fundo === 'string' ? c.fundo : JSON.stringify(c.fundo)}`);
|
|
302
|
-
linhas.push(` narração: ${c.narracao}`);
|
|
367
|
+
linhas.push(` narração${c.falante ? ` (${c.falante})` : ''}: ${c.narracao}`);
|
|
303
368
|
if (c.camera && c.camera !== 'nenhuma') linhas.push(` câmera: ${c.camera}`);
|
|
304
369
|
if (c.chaves) linhas.push(` movimento: ${c.chaves}`);
|
|
305
370
|
(c.midia || []).forEach((m) => linhas.push(
|
|
@@ -314,6 +379,7 @@ function pauta(plano) {
|
|
|
314
379
|
|
|
315
380
|
module.exports = {
|
|
316
381
|
precisaDeProducao, precisaDeEdicao, vozPara, ritmoPara,
|
|
382
|
+
ESTRUTURAS, recentes, cardapioEstruturas, registrarEstrutura,
|
|
317
383
|
normalizarPlano, extrairJSON, planejar, pauta,
|
|
318
384
|
DIRETOR, REVISAO,
|
|
319
385
|
};
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
'use strict';
|
|
28
28
|
|
|
29
|
+
const decisao = require('./decisao.js');
|
|
30
|
+
|
|
29
31
|
// ── as palavras que não carregam ideia ───────────────────────────────────
|
|
30
32
|
const VAZIAS = new Set((
|
|
31
33
|
'a o as os um uma uns umas de do da dos das no na nos nas em por para pra pro com sem que se e '
|
|
@@ -88,6 +90,25 @@ const ICONES = [
|
|
|
88
90
|
[['surpre', 'chocad', 'absurd'], 'exploding-head'],
|
|
89
91
|
];
|
|
90
92
|
|
|
93
|
+
/* AS OUTRAS FORMAS DE DIZER O MESMO (07/10): "não quero que o sistema fique
|
|
94
|
+
repetindo os mesmos ícones". Cada conceito tinha UM emoji — todo vídeo de
|
|
95
|
+
dinheiro saía com o mesmo saco de dinheiro. O ícone automático continua
|
|
96
|
+
saindo daqui pelo primeiro nome (o de ICONES); a passada de variedade do
|
|
97
|
+
vídeo (index.js, variarIcones) troca pelo que os últimos vídeos menos
|
|
98
|
+
usaram, o mesmo conceito com o mesmo ícone dentro do vídeo. Nomes
|
|
99
|
+
conferidos no fluent-emoji-flat do Iconify em 07/10. */
|
|
100
|
+
const ALTERNATIVAS = {
|
|
101
|
+
robot: ['mechanical-arm', 'desktop-computer'], brain: ['thought-balloon', 'light-bulb'], 'light-bulb': ['sparkles', 'glowing-star', 'gem-stone'],
|
|
102
|
+
'money-bag': ['dollar-banknote', 'coin', 'money-with-wings', 'credit-card'], stopwatch: ['hourglass-not-done', 'alarm-clock', 'mantelpiece-clock', 'watch'],
|
|
103
|
+
'chart-increasing': ['seedling', 'mountain', 'chequered-flag'], 'bar-chart': ['clipboard', 'hundred-points'], rocket: ['high-voltage', 'airplane'],
|
|
104
|
+
fire: ['collision', 'high-voltage', 'dizzy'], 'check-mark-button': ['check-mark', 'thumbs-up', 'hundred-points'], 'cross-mark': ['stop-sign', 'no-entry'],
|
|
105
|
+
laptop: ['desktop-computer', 'keyboard'], 'globe-showing-americas': ['globe-with-meridians', 'satellite-antenna'], 'speaker-high-volume': ['microphone', 'headphone', 'megaphone'],
|
|
106
|
+
memo: ['pencil', 'open-book', 'clipboard'], books: ['open-book', 'pencil'], 'speech-balloon': ['speaking-head', 'thought-balloon', 'megaphone'],
|
|
107
|
+
gear: ['hammer-and-wrench', 'toolbox', 'link'], calendar: ['spiral-calendar'], trophy: ['sports-medal', 'crown', 'glowing-star'],
|
|
108
|
+
'flexed-biceps': ['high-voltage'], handshake: ['people-hugging', 'link'], locked: ['shield', 'key'], briefcase: ['bank', 'classical-building'],
|
|
109
|
+
'party-popper': ['confetti-ball', 'sparkles'], 'red-heart': ['sparkling-heart'], 'shopping-cart': ['receipt', 'credit-card'], 'clapper-board': ['video-camera', 'camera'],
|
|
110
|
+
};
|
|
111
|
+
|
|
91
112
|
/* O termo de busca da IMAGEM DE APOIO, pelo emoji que a palavra ganhou. Em
|
|
92
113
|
inglês de propósito: os acervos abertos respondem muito melhor assim. */
|
|
93
114
|
const FOTO = {
|
|
@@ -225,14 +246,6 @@ function blocos(palavras, maximo) {
|
|
|
225
246
|
return out;
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
// O zoom de cada cena, em rodízio: parado, punch-in, empurrando devagar.
|
|
229
|
-
function camera(i, dur) {
|
|
230
|
-
const tipo = i % 3;
|
|
231
|
-
if (tipo === 0) return { zoom: [{ t: 0, v: 1 }, { t: dur, v: 1.04, e: 'linear' }] };
|
|
232
|
-
if (tipo === 1) return { zoom: [{ t: 0, v: 1.18 }, { t: dur, v: 1.21, e: 'linear' }] };
|
|
233
|
-
return { zoom: [{ t: 0, v: 1.07 }, { t: dur, v: 1.13, e: 'suave' }] };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
249
|
const ESTILOS = {
|
|
237
250
|
viral: { icones: true, letreiro: true, sons: true, barra: true, apoio: true, recorte: true, fundoRecorte: ['#1B1F3B', '#3A1C71'], destaque: '#FFD400', forte: '#00E0A4' },
|
|
238
251
|
limpo: { icones: false, letreiro: false, sons: false, barra: false, apoio: true, recorte: false, fundoRecorte: ['#111111', '#333333'], destaque: '#FFFFFF', forte: '#FFD400' },
|
|
@@ -251,8 +264,6 @@ const ESTILOS = {
|
|
|
251
264
|
* @param {object} op.sons { pop, whoosh } — caminhos na pasta do projeto, opcionais
|
|
252
265
|
* @param {Set} op.iconesOk os nomes de ícone que existem de verdade (sem isso, todos valem)
|
|
253
266
|
*/
|
|
254
|
-
// Em quais cenas a pessoa é recortada, com a palavra gigante atrás dela.
|
|
255
|
-
const temRecorte = (i) => i % 4 === 2;
|
|
256
267
|
|
|
257
268
|
function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {}, marca = null }) {
|
|
258
269
|
const e = ESTILOS[estilo] || ESTILOS.viral;
|
|
@@ -263,6 +274,10 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
263
274
|
const base = Math.min(largura, altura);
|
|
264
275
|
const grupos = frases(lista);
|
|
265
276
|
const total = duracao || lista[lista.length - 1].ate + 0.3;
|
|
277
|
+
/* O QUE CADA FRASE PEDE (decisao.js): movimento, recorte e whoosh pela
|
|
278
|
+
intensidade dela, não pelo rodízio i % 3 — e depois do golpe, respiro. */
|
|
279
|
+
const decisoes = decisao.decidir(grupos, { peso });
|
|
280
|
+
let ladoQuadro = 0;
|
|
266
281
|
const cenas = [];
|
|
267
282
|
const efeitos = [];
|
|
268
283
|
const infos = []; // onde cada cena começa e a palavra forte dela — para o recorte
|
|
@@ -283,12 +298,13 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
283
298
|
deOrigem: r2(ini), ateOrigem: r2(ini + dur), volume: 1,
|
|
284
299
|
}];
|
|
285
300
|
|
|
286
|
-
infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null
|
|
301
|
+
infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null,
|
|
302
|
+
recorte: decisoes[i].recorte, intensidade: decisoes[i].intensidade });
|
|
287
303
|
|
|
288
304
|
/* A PALAVRA ATRÁS DA PESSOA: o fundo do quarto some, entra a cor da
|
|
289
305
|
marca, a palavra forte aparece enorme e a pessoa, recortada, fica
|
|
290
306
|
na frente dela. É o efeito que mais denuncia edição de videomaker. */
|
|
291
|
-
const rec = e.recorte &&
|
|
307
|
+
const rec = e.recorte && decisoes[i].recorte ? recortes[i] : null;
|
|
292
308
|
if (rec && forte) {
|
|
293
309
|
const vida = r2(rec.ate - rec.de);
|
|
294
310
|
const some = [{ t: 0, v: 0 }, { t: 0.1, v: 1 }, { t: vida - 0.12, v: 1 }, { t: vida, v: 0 }];
|
|
@@ -403,10 +419,12 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
403
419
|
});
|
|
404
420
|
}
|
|
405
421
|
|
|
406
|
-
// O whoosh entra no
|
|
407
|
-
if (e.sons && sons.whoosh && i
|
|
422
|
+
// O whoosh entra no soco: é o corte que ele anuncia (e só nele).
|
|
423
|
+
if (e.sons && sons.whoosh && decisoes[i].whoosh) efeitos.push({ t: r2(Math.max(0, ini - 0.12)), src: sons.whoosh, volume: 0.6 });
|
|
408
424
|
|
|
409
|
-
|
|
425
|
+
const d = decisoes[i];
|
|
426
|
+
if (d.movimento === 'reenquadra') ladoQuadro = 1 - ladoQuadro;
|
|
427
|
+
cenas.push({ duracao: dur, camera: decisao.camera(d.movimento, dur, d.intensidade, ladoQuadro, base), elementos, transicao: { tipo: 'corte' }, impacto: d.movimento === 'soco' || undefined });
|
|
410
428
|
});
|
|
411
429
|
|
|
412
430
|
return {
|
|
@@ -473,4 +491,4 @@ async function prepararSons(pasta, video) {
|
|
|
473
491
|
return sons;
|
|
474
492
|
}
|
|
475
493
|
|
|
476
|
-
module.exports = { montarEdicao,
|
|
494
|
+
module.exports = { montarEdicao, ALTERNATIVAS, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, rotaDeApoio, DEPOIS_DA_PALAVRA, ESTILOS, ICONES, FOTO };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* efeitos.js — o efeito que a IA escreveu num vídeo fica para os próximos.
|
|
3
|
+
*
|
|
4
|
+
* "Quero mais animações, mais efeitos, e que possa criar tudo isso
|
|
5
|
+
* também." (Joel, 09/10/2026)
|
|
6
|
+
*
|
|
7
|
+
* O `componente` (plano.js) é a válvula: quando nenhuma primitiva alcança, a
|
|
8
|
+
* IA escreve um arquivo React na pasta do projeto. Só que ele morria com o
|
|
9
|
+
* projeto — o efeito bonito do vídeo de terça não existia para o de quarta,
|
|
10
|
+
* e a IA reescrevia do zero (ou pior, nem lembrava que tinha feito).
|
|
11
|
+
*
|
|
12
|
+
* Aqui o componente que ficou bom é GUARDADO em ~/.primocode/studio/efeitos/
|
|
13
|
+
* com nome e descrição, e volta como cardápio: os menos usados primeiro,
|
|
14
|
+
* pela mesma memória de variedade das trilhas e dos ícones (variedade.js,
|
|
15
|
+
* tipo 'efeito'). Guardar e reusar são escolhas do modelo; nada entra no
|
|
16
|
+
* vídeo sozinho.
|
|
17
|
+
*
|
|
18
|
+
* Antes de guardar, a fonte é conferida: o efeito vai rodar em OUTROS
|
|
19
|
+
* projetos, então import relativo (o arquivo vizinho não vai junto),
|
|
20
|
+
* Math.random/Date.now (cada render sai diferente) e timer (quadro
|
|
21
|
+
* repetido) são recusados com o motivo.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const variedade = require('./variedade.js');
|
|
30
|
+
|
|
31
|
+
const pasta = () => path.join(os.homedir(), '.primocode', 'studio', 'efeitos');
|
|
32
|
+
const catalogoArq = () => path.join(pasta(), 'catalogo.json');
|
|
33
|
+
const PACOTES = /^(react|react\/jsx-runtime|remotion|@remotion\/[a-z-]+|three|three\/examples\/jsm\/.+|@react-three\/fiber)$/;
|
|
34
|
+
const TETO = 150 * 1024;
|
|
35
|
+
|
|
36
|
+
function ler() {
|
|
37
|
+
try { const c = JSON.parse(fs.readFileSync(catalogoArq(), 'utf8')); return Array.isArray(c) ? c : []; } catch { return []; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** O que impede este código de rodar em outro projeto. [] = pode guardar. */
|
|
41
|
+
function conferirFonte(codigo) {
|
|
42
|
+
const falta = [];
|
|
43
|
+
const sem = String(codigo || '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
44
|
+
if (!/export\s+(default|function|const)/.test(sem)) falta.push('o arquivo não exporta o componente (export default function …)');
|
|
45
|
+
for (const m of sem.matchAll(/(?:from\s+|import\s*\(\s*|require\s*\(\s*)['"]([^'"]+)['"]/g)) {
|
|
46
|
+
if (!PACOTES.test(m[1])) falta.push(`import "${m[1]}": só react, remotion, @remotion/*, three e @react-three/fiber — o arquivo vai sozinho para outro projeto`);
|
|
47
|
+
}
|
|
48
|
+
if (/Math\.random|Date\.now/.test(sem)) falta.push('Math.random/Date.now: cada render sairia diferente — use random("semente") do Remotion');
|
|
49
|
+
if (/setTimeout|setInterval|requestAnimationFrame/.test(sem)) falta.push('timer não existe no render quadro a quadro — anime pelo quadro (useCurrentFrame ou a prop t)');
|
|
50
|
+
return falta;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Guarda o componente do projeto no catálogo, com nome e descrição. */
|
|
54
|
+
function guardar({ pasta: proj, arquivo, nome, descricao, tags = [] } = {}) {
|
|
55
|
+
const slug = String(nome || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
56
|
+
if (slug.length < 2) return { ok: false, error: 'dê um nome ao efeito (nome), como "vidro-quebrando".' };
|
|
57
|
+
if (!String(descricao || '').trim()) return { ok: false, error: 'diga o que o efeito faz (descricao) — é o que você vai ler no cardápio depois.' };
|
|
58
|
+
if (!proj || !arquivo) return { ok: false, error: 'diga o projeto (id) e o arquivo do componente (arquivo).' };
|
|
59
|
+
const cheio = path.resolve(proj, arquivo);
|
|
60
|
+
if (!cheio.startsWith(path.resolve(proj) + path.sep) || !/\.(jsx|js|tsx)$/.test(cheio)) return { ok: false, error: 'o arquivo é um .jsx/.js dentro da pasta do projeto.' };
|
|
61
|
+
if (!fs.existsSync(cheio)) return { ok: false, error: `o arquivo "${arquivo}" não existe no projeto.` };
|
|
62
|
+
const codigo = fs.readFileSync(cheio, 'utf8');
|
|
63
|
+
if (codigo.length > TETO) return { ok: false, error: 'o componente passa de 150 KB — guarde a parte que é o efeito.' };
|
|
64
|
+
const falta = conferirFonte(codigo);
|
|
65
|
+
if (falta.length) return { ok: false, error: 'não guardei: ' + falta.join('; ') };
|
|
66
|
+
fs.mkdirSync(pasta(), { recursive: true });
|
|
67
|
+
fs.writeFileSync(path.join(pasta(), slug + '.jsx'), codigo);
|
|
68
|
+
const item = { nome: slug, descricao: String(descricao).trim().slice(0, 300), tags: [].concat(tags || []).map(String).slice(0, 8), guardado: new Date().toISOString().slice(0, 10) };
|
|
69
|
+
fs.writeFileSync(catalogoArq(), JSON.stringify([item, ...ler().filter((x) => x.nome !== slug)], null, 2));
|
|
70
|
+
return { ok: true, ...item };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** O cardápio: os guardados, os menos usados primeiro (memória da variedade). */
|
|
74
|
+
function cardapio({ aleatorio = Math.random } = {}) {
|
|
75
|
+
const itens = ler().filter((x) => fs.existsSync(path.join(pasta(), x.nome + '.jsx')));
|
|
76
|
+
return variedade.pontuar(itens, 'efeito', { chave: (x) => x.nome })
|
|
77
|
+
.map((p) => ({ ...p, pontos: p.pontos + aleatorio() * 0.05 }))
|
|
78
|
+
.sort((a, b) => b.pontos - a.pontos)
|
|
79
|
+
.map((p) => ({ nome: p.opcao.nome, descricao: p.opcao.descricao, tags: p.opcao.tags }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Copia o efeito para o projeto e devolve o elemento pronto. Marca o uso. */
|
|
83
|
+
function reusar({ pasta: proj, nome } = {}) {
|
|
84
|
+
const item = ler().find((x) => x.nome === nome);
|
|
85
|
+
const de = item && path.join(pasta(), item.nome + '.jsx');
|
|
86
|
+
if (!item || !fs.existsSync(de)) return { ok: false, error: `não há efeito "${nome}" guardado — veja o cardápio (acao: efeitos).` };
|
|
87
|
+
if (!proj) return { ok: false, error: 'diga o projeto (id).' };
|
|
88
|
+
const src = `efeitos/${item.nome}.jsx`;
|
|
89
|
+
fs.mkdirSync(path.join(proj, 'efeitos'), { recursive: true });
|
|
90
|
+
fs.copyFileSync(de, path.join(proj, src));
|
|
91
|
+
variedade.marcar('efeito', item.nome);
|
|
92
|
+
return { ok: true, src, descricao: item.descricao, elemento: { tipo: 'componente', src, x: 0, y: 0 } };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { guardar, cardapio, reusar, conferirFonte, pasta };
|
package/lib/estudio/estilo.js
CHANGED
|
@@ -51,13 +51,23 @@ const normal = (e) => {
|
|
|
51
51
|
/* O RODÍZIO: quando ninguém escolheu ("vários estilos... que não sejam padrão
|
|
52
52
|
da IA", Joel 05/10), o padrão não é sempre o híbrido — gira entre os que
|
|
53
53
|
servem a qualquer roteiro, sem repetir os últimos (memória 'estilo'). */
|
|
54
|
-
|
|
54
|
+
/* Com imagem pedida (08/10): o padrão é o FLUIDO MISTO — motion com a
|
|
55
|
+
imagem em poucos momentos, cada uma entrando de um jeito. O híbrido e o
|
|
56
|
+
cinema ("10 mil imagens de banco e a mesma animação") ficam pelo nome. */
|
|
57
|
+
const RODIZIO = ['fluido'];
|
|
58
|
+
/* Sem imagem (o padrão desde 07/10), o rodízio fica nos dois motions que
|
|
59
|
+
nasceram das referências boas (motions-exemplos/motion-god): o fluido e o
|
|
60
|
+
gerativo. O impacto (emoji 3D em cada batida, cara de anúncio) saiu do
|
|
61
|
+
rodízio em 08/10 — "horríveis, nada a ver com as referências" — e fica
|
|
62
|
+
para quem o pede pelo nome (o agente ou o roteiro). */
|
|
63
|
+
const RODIZIO_SEM_IMAGEM = ['fluido', 'gerativo'];
|
|
55
64
|
|
|
56
|
-
function estiloDaVez(aleatorio = Math.random) {
|
|
57
|
-
|
|
65
|
+
function estiloDaVez(aleatorio = Math.random, semImagem = false) {
|
|
66
|
+
const lista = semImagem ? RODIZIO_SEM_IMAGEM : RODIZIO;
|
|
67
|
+
return variedade.sortear(lista, 'estilo', { aleatorio }) || lista[0];
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, aleatorio } = {}) {
|
|
70
|
+
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, semImagem = false, aleatorio } = {}) {
|
|
61
71
|
const pedidoAgente = normal(agente);
|
|
62
72
|
if (pedidoAgente) return pedidoAgente;
|
|
63
73
|
const doRoteiro = normal(roteiro);
|
|
@@ -73,10 +83,11 @@ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = fa
|
|
|
73
83
|
/* Anúncio, reels e o resto: o HÍBRIDO. "Um mix da versão antiga (o reels
|
|
74
84
|
da imersão, no cinema) e da última (o kit) — absurdo, que nem parece
|
|
75
85
|
que foi IA" (29/09). Com `girar`, o padrão roda (RODIZIO). */
|
|
76
|
-
|
|
86
|
+
if (girar) return estiloDaVez(aleatorio, semImagem);
|
|
87
|
+
return semImagem ? 'gerativo' : 'hibrido';
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
|
|
80
91
|
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido']);
|
|
81
92
|
|
|
82
|
-
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO };
|
|
93
|
+
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM };
|