primocode 10.4.4 → 10.5.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/lib/catalogo.js +7 -3
- package/lib/conta.js +9 -3
- package/lib/estudio/batida.js +95 -10
- package/lib/estudio/colecao.js +185 -0
- package/lib/estudio/dados/fluent-3d.json +1 -0
- package/lib/estudio/dados/noto-animado.json +1 -0
- package/lib/estudio/estilo.js +6 -2
- package/lib/estudio/gerativo.js +181 -29
- package/lib/estudio/index.js +97 -22
- package/lib/estudio/kit/animacao.jsx +41 -0
- package/lib/estudio/kit/gerativo.jsx +63 -5
- package/lib/estudio/kit/index.jsx +3 -1
- package/lib/estudio/kit/tres.jsx +156 -0
- package/lib/estudio/kit/voxel.jsx +609 -0
- package/lib/estudio/kit.js +5 -1
- package/lib/estudio/personagem.js +75 -4
- package/lib/estudio/remotion.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.4.
|
|
1
|
+
# PrimoCode v10.4.7
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.4.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.4.7
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/catalogo.js
CHANGED
|
@@ -93,8 +93,8 @@ const GRUPOS = [
|
|
|
93
93
|
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
94
94
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
|
|
95
95
|
+ 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
|
|
96
|
-
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa
|
|
97
|
-
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (o vídeo GRAVADO em 3D — cada parte é UMA fala {falante, texto, emocao?, destaque?}, sem tipo
|
|
96
|
+
+ 'estilo (SEM IMAGEM é o padrão: escolha o motion que serve a ESTE roteiro, ou deixe vazio e ele gira entre gerativo, fluido e impacto): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
97
|
+
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D, no visual de blocos/voxel de render de cinema: bonecos de blocos e o mascote laranja (quem é IA) num set de ladrilhos com uma parede de telas, luz roxa de recorte, o fundo desfocado de lente e a legenda branca no meio — cada parte é UMA fala {falante, texto, emocao?, destaque?}, sem tipo; a boca segue a voz, gestos, e a câmera corta entre aberto, ombro e close de baixo; é a escolha para animação 3D, personagem, boneco, avatar, mascote, podcast, entrevista ou alguém FALANDO em 3D — um narrador só também serve (sem falante). As telas do fundo mostram o ASSUNTO: paineis:["título curto", …] (até 12) — vazio, saem dos destaques e das palavras fortes. Com dois falantes (falante em cada parte) eles conversam; personagens:{"nome":{tipo:robo|pessoa, pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba, rotulo}} — nome de IA vira robô sozinho; emocao feliz|serio|surpreso|pensativo|animado|curioso e destaque:"texto curto" (o cartão na tela) em cada parte; cenario telas (o roxo da referência, padrão)|neon|oceano|terra; cor do mascote: personagens:{"claude":{cor:"#d97757"}}; visual:"liso" só se pedirem o boneco liso antigo). '
|
|
98
98
|
+ 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
99
99
|
+ 'voz (thalita|antonio|francisca). DUAS VOZES (o pedido fala em dois narradores, diálogo, conversa, entrevista): roteiro.vozes {"nome":"antonio","outro":"thalita"} e falante:"nome" em cada parte — cada um sai na sua voz. Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
100
100
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|
|
@@ -109,7 +109,11 @@ const GRUPOS = [
|
|
|
109
109
|
+ 'o render empurra para dentro, encolhe o texto largo e afasta o colado; enfeite de fundo marque decorativo: true.' },
|
|
110
110
|
|
|
111
111
|
{ tool: 'estudio_midia', escreve: true, faz: 'procura e baixa ícone, imagem, música e efeito sonoro da internet, e lista as fontes',
|
|
112
|
-
args: 'acao: imagem | icones | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
|
|
112
|
+
args: 'acao: biblioteca | imagem | icones | icones3d | animacoes | som | trilha | efeito | fontes | print | elementos | elemento | recortar. termo. id (para baixar). '
|
|
113
|
+
+ 'biblioteca: o cardápio INTEIRO do que existe (ícone 2D, ícone 3D, animação pronta, efeito sonoro, música, fonte, personagem 3D) — olhe antes de escolher. '
|
|
114
|
+
+ 'icones3d termo:"coffee": os 1.500+ ícones 3D renderizados do Fluent Emoji (MIT); com item:"fluent3d:<nome>" e o id do projeto, baixa e devolve o elemento {tipo:"imagem"} pronto. '
|
|
115
|
+
+ 'animacoes termo:"fire": as 881 animações prontas do Noto (Lottie, vetor; CC BY 4.0, ponha o crédito); com item:"noto:<código>", baixa e devolve o elemento {tipo:"kit", peca:"animacao", src, loop} pronto. '
|
|
116
|
+
+ 'O termo vai em inglês (as palavras comuns em português também casam). Escolha pelo que a fala DIZ: numa conversa sobre café, a xícara; nunca enfeite sem relação. '
|
|
113
117
|
+ 'print: url — tira o print REAL do site e devolve o src e um elemento {tipo:"kit", peca:"printsite"} pronto '
|
|
114
118
|
+ '(o site de verdade numa janela 3D que rola a página; celular: true para a tela do celular; inteira: false só a primeira dobra). '
|
|
115
119
|
+ 'Use SE o pedido trouxer um site e o vídeo for SOBRE ele (lançamento, tour, case); vídeo que só cita a marca não precisa. '
|
package/lib/conta.js
CHANGED
|
@@ -54,6 +54,12 @@ const FIREBASE_AUTH = process.env.PRIMOCODE_FIREBASE_AUTH_URL || 'https://identi
|
|
|
54
54
|
// (`PLANOS_COM_PRO`), e quem decide de verdade é ele: isto aqui só evita uma
|
|
55
55
|
// ida à rede para mostrar o selo na abertura.
|
|
56
56
|
const PLANOS_PRO = ['premium', 'super'];
|
|
57
|
+
/* ASSINANTE É QUEM NÃO ESTÁ NO GRÁTIS (11/10). A conta do Joel veio do Conecta
|
|
58
|
+
como "empresarial" — um plano pago que esta lista não conhecia — e ele caiu
|
|
59
|
+
no limite de quem não paga, com a mensagem "assine o Premium". O Conecta cria
|
|
60
|
+
plano novo sem avisar o PrimoCode; o que não muda é o nome do grátis. */
|
|
61
|
+
const PLANOS_GRATIS = ['free', 'gratis', 'grátis', 'gratuito', 'basico', 'básico', ''];
|
|
62
|
+
const ehPago = (plano) => !PLANOS_GRATIS.includes(String(plano == null ? '' : plano).trim().toLowerCase());
|
|
57
63
|
|
|
58
64
|
function pedir(rota, corpo, timeout = 30000, { token, base = CONECTA } = {}) {
|
|
59
65
|
return new Promise((resolve, reject) => {
|
|
@@ -116,7 +122,7 @@ function atual() {
|
|
|
116
122
|
const c = ler();
|
|
117
123
|
if (!c || !c.token) return null;
|
|
118
124
|
return { uid: c.uid, plano: c.plano || 'free', email: c.email || null,
|
|
119
|
-
pro:
|
|
125
|
+
pro: ehPago(c.plano), desde: c.desde };
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
// ── entrar ───────────────────────────────────────────────────────────────
|
|
@@ -189,7 +195,7 @@ async function entrar({ email, senha } = {}) {
|
|
|
189
195
|
token: r.token, uid: r.uid, plano: r.plano || 'free', email,
|
|
190
196
|
desde: new Date().toISOString(), servidor: CONECTA,
|
|
191
197
|
});
|
|
192
|
-
return { ok: true, uid: r.uid, plano: r.plano, pro:
|
|
198
|
+
return { ok: true, uid: r.uid, plano: r.plano, pro: ehPago(r.plano) };
|
|
193
199
|
} catch (e) {
|
|
194
200
|
return { ok: false, error: `não consegui concluir o login: ${e.message}` };
|
|
195
201
|
}
|
|
@@ -234,4 +240,4 @@ function versao() {
|
|
|
234
240
|
try { return require('../package.json').version; } catch { return '?'; }
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
module.exports = { entrar, sair, atual, sessao, dispositivos, erroDoFirebase, ARQUIVO, CONECTA, PLANOS_PRO };
|
|
243
|
+
module.exports = { entrar, sair, atual, sessao, dispositivos, erroDoFirebase, ARQUIVO, CONECTA, PLANOS_PRO, PLANOS_GRATIS, ehPago };
|
package/lib/estudio/batida.js
CHANGED
|
@@ -141,7 +141,16 @@ const CONFIANCA_MINIMA = 0.7; // abaixo disso a batida é chute: a faixa entra
|
|
|
141
141
|
|
|
142
142
|
const TRANSICOES_FORTES = new Set(['soco', 'flash', 'chicote', 'zoom', 'giro']);
|
|
143
143
|
|
|
144
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Os cortes do plano no tempo do vídeo: [{t, peso}]. Corte de impacto pesa 3; o maior impacto vira `impacto`.
|
|
146
|
+
*
|
|
147
|
+
* O GERATIVO É UMA CENA SÓ: as batidas dele moram dentro da partitura do kit
|
|
148
|
+
* (cena.elementos[].partitura.batidas). Contando só as cenas, o vídeo dele
|
|
149
|
+
* tinha 0 cortes, e o log dizia "0 de 0 cortes caem na batida" (10/10). Cada
|
|
150
|
+
* batida (menos a primeira) é um corte; a troca de campo (fundo de cor) e a
|
|
151
|
+
* tese gigante pesam 3. O corte leva a referência da batida para ser
|
|
152
|
+
* ENCOSTADO na batida da música depois (melhorInicio).
|
|
153
|
+
*/
|
|
145
154
|
function cortesDoPlano(plano) {
|
|
146
155
|
const cortes = [];
|
|
147
156
|
let t = 0;
|
|
@@ -150,35 +159,111 @@ function cortesDoPlano(plano) {
|
|
|
150
159
|
const tr = cena.transicao && (cena.transicao.tipo || cena.transicao);
|
|
151
160
|
cortes.push({ t: r3(t), peso: cena.impacto || TRANSICOES_FORTES.has(tr) ? 3 : 1 });
|
|
152
161
|
}
|
|
162
|
+
for (const el of cena.elementos || []) {
|
|
163
|
+
const bs = el && el.partitura && Array.isArray(el.partitura.batidas) ? el.partitura.batidas : null;
|
|
164
|
+
if (!bs) continue;
|
|
165
|
+
bs.forEach((b, j) => {
|
|
166
|
+
if (j === 0) return;
|
|
167
|
+
const forte = b.troca || b.comp === 'gigante' || b.campoCor;
|
|
168
|
+
// A palavra que a batida mostra: a entrada da primeira linha (sem linha, a batida mesmo).
|
|
169
|
+
const ts = (b.linhas || []).map((l) => l.t).filter((x) => typeof x === 'number');
|
|
170
|
+
const palavra = ts.length ? Math.min(...ts) : b.de;
|
|
171
|
+
cortes.push({ t: r3(t + b.de), peso: forte ? 3 : 1, ref: { bs, j, base: t, palavra } });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
153
174
|
t += Number(cena.duracao) || 0;
|
|
154
175
|
}
|
|
155
|
-
return cortes;
|
|
176
|
+
return cortes.sort((a, b) => a.t - b.t);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A batida mais perto de `t` numa lista ordenada: {bt, d}. */
|
|
180
|
+
function maisPerto(lista, t) {
|
|
181
|
+
let lo = 0, hi = lista.length - 1;
|
|
182
|
+
while (lo < hi) { const m = (lo + hi) >> 1; if (lista[m] < t) lo = m + 1; else hi = m; }
|
|
183
|
+
const a = lista[lo], b = lo > 0 ? lista[lo - 1] : Infinity;
|
|
184
|
+
return Math.abs(a - t) <= Math.abs(b - t) ? { bt: a, d: Math.abs(a - t) } : { bt: b, d: Math.abs(b - t) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Quanto a troca de tela pode se afastar da PALAVRA que ela mostra: a fala
|
|
188
|
+
continua onde estava, e a troca adianta ou atrasa a palavra em no máximo
|
|
189
|
+
150 ms — abaixo do que o olho percebe como fora de sincronia. A janela é
|
|
190
|
+
medida da palavra, não do corte de antes (que já vinha ~120 ms adiantado). */
|
|
191
|
+
const EMPURRAO = 0.15;
|
|
192
|
+
/** A janela [de, até] em que o corte pode cair, no tempo do vídeo. */
|
|
193
|
+
const janelaDo = (c) => (c.ref ? [c.ref.base + c.ref.palavra - EMPURRAO, c.ref.base + c.ref.palavra + EMPURRAO] : [c.t, c.t]);
|
|
194
|
+
/** A batida da música (no tempo do vídeo) dentro da janela, a mais perto do corte; ou null. */
|
|
195
|
+
function batidaNaJanela(bats, c, o) {
|
|
196
|
+
const [a, z] = janelaDo(c);
|
|
197
|
+
let lo = 0, hi = bats.length;
|
|
198
|
+
while (lo < hi) { const m = (lo + hi) >> 1; if (bats[m] - o < a) lo = m + 1; else hi = m; }
|
|
199
|
+
let melhor = null;
|
|
200
|
+
for (let i = lo; i < bats.length && bats[i] - o <= z; i++) {
|
|
201
|
+
const v = bats[i] - o;
|
|
202
|
+
if (!melhor || Math.abs(v - c.t) < Math.abs(melhor - c.t)) melhor = v;
|
|
203
|
+
}
|
|
204
|
+
return melhor;
|
|
156
205
|
}
|
|
206
|
+
const MIN_BATIDA = 0.6; // nenhuma batida fica mais curta que isto depois do empurrão
|
|
157
207
|
|
|
158
208
|
/**
|
|
159
209
|
* De onde a faixa começa (em s) para o máximo de cortes cair numa batida.
|
|
160
210
|
* Procura em passos de 10 ms, de 0 a `maximo`; empate fica com o início mais
|
|
161
211
|
* cedo. O corte mais pesado ganha bônus se cair num acento.
|
|
212
|
+
*
|
|
213
|
+
* Os cortes do gerativo (com `ref`) contam como "na batida" até EMPURRAO de
|
|
214
|
+
* distância, porque depois da escolha eles são ENCOSTADOS nela: a batida do
|
|
215
|
+
* vídeo anda (b.de e o ate da anterior), e o golpe da tese também. O plano é
|
|
216
|
+
* mudado no lugar — é ele que vai para o render.
|
|
162
217
|
*/
|
|
163
218
|
function melhorInicio(cortes, analise, { maximo = 0, tolerancia = 0.05 } = {}) {
|
|
164
219
|
const bats = (analise && analise.batidas) || [];
|
|
165
220
|
if (!cortes.length || bats.length < 4 || (analise.confianca != null && analise.confianca < CONFIANCA_MINIMA)) return { inicio: 0, alinhados: 0, total: cortes.length };
|
|
166
|
-
const perto = (lista, t) =>
|
|
167
|
-
let lo = 0, hi = lista.length - 1;
|
|
168
|
-
while (lo < hi) { const m = (lo + hi) >> 1; if (lista[m] < t) lo = m + 1; else hi = m; }
|
|
169
|
-
return Math.min(Math.abs(lista[lo] - t), lo > 0 ? Math.abs(lista[lo - 1] - t) : Infinity);
|
|
170
|
-
};
|
|
221
|
+
const perto = (lista, t) => maisPerto(lista, t).d;
|
|
171
222
|
const maior = cortes.reduce((a, b) => (b.peso > a.peso ? b : a), cortes[0]);
|
|
172
223
|
const acentos = (analise.acentos || []);
|
|
173
224
|
let melhor = { inicio: 0, pontos: -1 };
|
|
174
225
|
for (let o = 0; o <= Math.max(0, maximo) + 1e-9; o += 0.01) {
|
|
175
226
|
let pontos = 0;
|
|
176
|
-
for (const c of cortes)
|
|
227
|
+
for (const c of cortes) {
|
|
228
|
+
if (c.ref) {
|
|
229
|
+
const v = batidaNaJanela(bats, c, o);
|
|
230
|
+
if (v != null) pontos += c.peso * (1 - 0.3 * Math.min(1, Math.abs(v - c.t) / (2 * EMPURRAO)));
|
|
231
|
+
} else pontos += c.peso * Math.exp(-0.5 * Math.pow(perto(bats, c.t + o) / tolerancia, 2));
|
|
232
|
+
}
|
|
177
233
|
if (maior.peso > 1 && acentos.length && perto(acentos, maior.t + o) < 0.1) pontos += 2;
|
|
178
234
|
if (pontos > melhor.pontos + 1e-6) melhor = { inicio: r3(o), pontos };
|
|
179
235
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
236
|
+
const o = melhor.inicio;
|
|
237
|
+
// O empurrão: cada corte do gerativo vai para a batida mais perto, se couber.
|
|
238
|
+
let empurrados = 0;
|
|
239
|
+
for (const c of cortes) {
|
|
240
|
+
if (!c.ref) continue;
|
|
241
|
+
const { bs, j, base } = c.ref;
|
|
242
|
+
const v = batidaNaJanela(bats, c, o);
|
|
243
|
+
if (v == null || Math.abs(v - c.t) <= 1e-3) continue;
|
|
244
|
+
const delta = v - c.t;
|
|
245
|
+
const b = bs[j], a = bs[j - 1];
|
|
246
|
+
const novo = b.de + delta;
|
|
247
|
+
if (novo - a.de < MIN_BATIDA || b.ate - novo < MIN_BATIDA) continue;
|
|
248
|
+
const colado = Math.abs(a.ate - b.de) < 0.02;
|
|
249
|
+
b.de = r3(novo);
|
|
250
|
+
if (colado) a.ate = b.de;
|
|
251
|
+
c.t = r3(base + b.de);
|
|
252
|
+
empurrados++;
|
|
253
|
+
}
|
|
254
|
+
// O golpe (a tese que bate) também cai na batida, sem sair da batida dele.
|
|
255
|
+
const vistos = new Set();
|
|
256
|
+
for (const c of cortes) {
|
|
257
|
+
if (!c.ref || vistos.has(c.ref.bs)) continue;
|
|
258
|
+
vistos.add(c.ref.bs);
|
|
259
|
+
for (const b of c.ref.bs) for (const g of b.golpes || []) {
|
|
260
|
+
const { bt, d } = maisPerto(bats, c.ref.base + g.t + o);
|
|
261
|
+
const alvo = bt - o - c.ref.base;
|
|
262
|
+
if (d <= EMPURRAO && alvo > b.de + 0.05 && alvo < b.ate - 0.1) g.t = r3(alvo);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const alinhados = cortes.filter((c) => perto(bats, c.t + o) <= tolerancia).length;
|
|
266
|
+
return { inicio: o, alinhados, total: cortes.length, empurrados };
|
|
182
267
|
}
|
|
183
268
|
|
|
184
269
|
module.exports = { analisarPCM, analisar, cortesDoPlano, melhorInicio, TAXA, CONFIANCA_MINIMA };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* colecao.js — a biblioteca completa do Estúdio: o que existe para pôr num vídeo.
|
|
3
|
+
*
|
|
4
|
+
* "tem que ter uma biblioteca completa de ícones, pra ele poder pegar os
|
|
5
|
+
* ícones 3D, 2D, animações, também efeitos sonoros e músicas, também
|
|
6
|
+
* fontes de texto" (Joel, 11/10)
|
|
7
|
+
*
|
|
8
|
+
* O 2D (Iconify), a imagem e o som (Openverse), a trilha e as fontes já
|
|
9
|
+
* existiam (assets.js, biblioteca.js, sons-leves.js). Faltavam duas coisas, e
|
|
10
|
+
* as duas entram aqui:
|
|
11
|
+
*
|
|
12
|
+
* · ÍCONE 3D — o Fluent Emoji 3D da Microsoft (licença MIT): mais de 1.500 objetos
|
|
13
|
+
* renderizados em 3D, de "coffee" a "rocket". O índice mora no pacote
|
|
14
|
+
* (dados/fluent-3d.json), então PROCURAR não usa rede; só o PNG escolhido
|
|
15
|
+
* desce, do CDN do jsDelivr.
|
|
16
|
+
* · ANIMAÇÃO PRONTA — o Noto Emoji animado do Google (CC BY 4.0): 881
|
|
17
|
+
* animações em Lottie (vetor, nítido em qualquer tamanho), tocadas quadro
|
|
18
|
+
* a quadro pela peça kit/animacao.jsx. Índice também no pacote.
|
|
19
|
+
*
|
|
20
|
+
* E `cardapio()` junta TUDO numa resposta só, para a IA saber o que existe
|
|
21
|
+
* antes de escolher — a escolha é dela (nada entra no vídeo sozinho).
|
|
22
|
+
*
|
|
23
|
+
* A licença anda junto: cada download grava o .json ao lado (fonte, licença,
|
|
24
|
+
* crédito), como no acervo.js.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
const DADOS = path.join(__dirname, 'dados');
|
|
33
|
+
const CDN_FLUENT = 'https://cdn.jsdelivr.net/gh/microsoft/fluentui-emoji@main/assets/';
|
|
34
|
+
const LOTTIE_NOTO = (cp) => `https://fonts.gstatic.com/s/e/notoemoji/latest/${cp}/lottie.json`;
|
|
35
|
+
const GIF_NOTO = (cp) => `https://fonts.gstatic.com/s/e/notoemoji/latest/${cp}/512.gif`;
|
|
36
|
+
|
|
37
|
+
let _fluent = null, _noto = null;
|
|
38
|
+
const fluent = () => (_fluent = _fluent || JSON.parse(fs.readFileSync(path.join(DADOS, 'fluent-3d.json'), 'utf8')));
|
|
39
|
+
const noto = () => (_noto = _noto || JSON.parse(fs.readFileSync(path.join(DADOS, 'noto-animado.json'), 'utf8')));
|
|
40
|
+
|
|
41
|
+
/* As duas bibliotecas são em inglês. A IA costuma mandar o termo em inglês,
|
|
42
|
+
mas a pessoa fala português: as palavras mais comuns de vídeo têm par. */
|
|
43
|
+
const PT_EN = {
|
|
44
|
+
cafe: 'coffee', 'café': 'coffee', pao: 'bread', 'pão': 'bread', padaria: 'bread', dinheiro: 'money', grana: 'money', moeda: 'coin',
|
|
45
|
+
foguete: 'rocket', coracao: 'heart', 'coração': 'heart', fogo: 'fire', ideia: 'light bulb', lampada: 'light bulb', 'lâmpada': 'light bulb',
|
|
46
|
+
estrela: 'star', relogio: 'clock', 'relógio': 'clock', tempo: 'hourglass', celular: 'mobile phone', telefone: 'telephone',
|
|
47
|
+
computador: 'laptop', notebook: 'laptop', robo: 'robot', 'robô': 'robot', livro: 'books', escola: 'school', carro: 'car',
|
|
48
|
+
casa: 'house', comida: 'food', pizza: 'pizza', musica: 'musical note', 'música': 'musical note', foto: 'camera', camera: 'camera',
|
|
49
|
+
'câmera': 'camera', mundo: 'globe', terra: 'globe', chave: 'key', cadeado: 'locked', seguranca: 'shield', 'segurança': 'shield',
|
|
50
|
+
escudo: 'shield', grafico: 'chart', 'gráfico': 'chart', alvo: 'direct hit', trofeu: 'trophy', 'troféu': 'trophy', medalha: 'medal',
|
|
51
|
+
presente: 'gift', festa: 'party', sol: 'sun', chuva: 'rain', raio: 'high voltage', energia: 'high voltage', bateria: 'battery',
|
|
52
|
+
email: 'envelope', mensagem: 'speech balloon', conversa: 'speech balloon', pergunta: 'question', ok: 'check mark', certo: 'check mark',
|
|
53
|
+
errado: 'cross mark', aviso: 'warning', dinossauro: 'dinosaur', gato: 'cat', cachorro: 'dog', planta: 'seedling', arvore: 'tree',
|
|
54
|
+
'árvore': 'tree', sorriso: 'smile', feliz: 'smile', triste: 'sad', bravo: 'angry', amor: 'heart', rindo: 'joy', pensando: 'thinking',
|
|
55
|
+
surpreso: 'surprised', cerebro: 'brain', 'cérebro': 'brain', olho: 'eye', mao: 'hand', 'mão': 'hand', joinha: 'thumbs up',
|
|
56
|
+
palmas: 'clapping', aplauso: 'clapping', calendario: 'calendar', 'calendário': 'calendar', saco: 'money bag', banco: 'bank',
|
|
57
|
+
cartao: 'credit card', 'cartão': 'credit card', loja: 'shopping', compras: 'shopping cart', voto: 'ballot box', ferramenta: 'hammer',
|
|
58
|
+
engrenagem: 'gear', ciencia: 'test tube', 'ciência': 'test tube', saude: 'hospital', 'saúde': 'hospital', remedio: 'pill', 'remédio': 'pill',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const norm = (t) => String(t || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9 ]+/g, ' ').trim();
|
|
62
|
+
|
|
63
|
+
function termos(termo) {
|
|
64
|
+
const cru = String(termo || '').toLowerCase().trim();
|
|
65
|
+
const out = new Set([norm(cru)]);
|
|
66
|
+
for (const w of cru.split(/\s+/)) if (PT_EN[w]) out.add(PT_EN[w]);
|
|
67
|
+
if (PT_EN[cru]) out.add(PT_EN[cru]);
|
|
68
|
+
return [...out].filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A nota de um nome contra a busca: palavra inteira vale mais que pedaço.
|
|
72
|
+
function nota(nome, ts) {
|
|
73
|
+
const n = ' ' + norm(nome) + ' ';
|
|
74
|
+
let melhor = 0;
|
|
75
|
+
for (const t of ts) {
|
|
76
|
+
if (n.trim() === t) melhor = Math.max(melhor, 100);
|
|
77
|
+
else if (n.includes(' ' + t + ' ')) melhor = Math.max(melhor, 60 - n.length * 0.1);
|
|
78
|
+
else {
|
|
79
|
+
const ws = t.split(' ').filter((w) => w.length > 2);
|
|
80
|
+
const achou = ws.filter((w) => n.includes(' ' + w)).length;
|
|
81
|
+
if (achou) melhor = Math.max(melhor, (achou / ws.length) * 40 - n.length * 0.1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return melhor;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Os ícones 3D (Fluent) que casam com o termo. */
|
|
88
|
+
function procurarIcones3d(termo, { quantas = 10 } = {}) {
|
|
89
|
+
const ts = termos(termo);
|
|
90
|
+
if (!ts.length) return { ok: false, error: 'diga o que procurar (termo), de preferência em inglês: "coffee", "rocket".' };
|
|
91
|
+
// O nome manda; as palavras-chave do metadata (coffee em "hot beverage") e o grupo ajudam.
|
|
92
|
+
const achados = fluent().map(([nome, p, kw = '', grupo = '']) => ({ nome, p, grupo, n: Math.max(nota(nome, ts), nota(kw, ts) * 0.8, nota(grupo, ts) * 0.3) })).filter((x) => x.n > 0)
|
|
93
|
+
.sort((a, b) => b.n - a.n).slice(0, quantas);
|
|
94
|
+
return {
|
|
95
|
+
ok: true, termo, total: achados.length,
|
|
96
|
+
icones: achados.map((x) => ({ id: 'fluent3d:' + x.nome, nome: x.nome, grupo: x.grupo, url: CDN_FLUENT + encodeURI(x.p), licenca: 'MIT', fonte: 'Microsoft Fluent Emoji 3D' })),
|
|
97
|
+
comoUsar: 'baixe com acao:icones3d id:"fluent3d:<nome>" (precisa do id do projeto) — volta um elemento {tipo:"imagem"} pronto, PNG transparente de 256 px em 3D.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** As animações prontas (Noto animado, Lottie) que casam com o termo. */
|
|
102
|
+
function procurarAnimacoes(termo, { quantas = 10 } = {}) {
|
|
103
|
+
const ts = termos(termo);
|
|
104
|
+
if (!ts.length) return { ok: false, error: 'diga o que procurar (termo): "fire", "party", "thinking"…' };
|
|
105
|
+
const achados = noto().map(([cp, tags, cat]) => ({ cp, tags, cat, n: Math.max(nota(tags, ts), nota(cat, ts) * 0.5) }))
|
|
106
|
+
.filter((x) => x.n > 0).sort((a, b) => b.n - a.n).slice(0, quantas);
|
|
107
|
+
return {
|
|
108
|
+
ok: true, termo, total: achados.length,
|
|
109
|
+
animacoes: achados.map((x) => ({ id: 'noto:' + x.cp, nome: x.tags, categoria: x.cat, lottie: LOTTIE_NOTO(x.cp), gif: GIF_NOTO(x.cp),
|
|
110
|
+
licenca: 'CC BY 4.0', creditos: 'Noto Emoji Animation, Google (CC BY 4.0)' })),
|
|
111
|
+
comoUsar: 'baixe com acao:animacoes id:"noto:<código>" (precisa do id do projeto) — volta um elemento {tipo:"kit", peca:"animacao"} pronto, que toca quadro a quadro.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function baixarUrl(url, destino) {
|
|
116
|
+
const r = await fetch(url, { headers: { 'user-agent': 'primocode-estudio' } });
|
|
117
|
+
if (!r.ok) return { ok: false, error: `${r.status} ao baixar ${url}` };
|
|
118
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
119
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
120
|
+
fs.writeFileSync(destino, buf);
|
|
121
|
+
return { ok: true, bytes: buf.length };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Baixa um item da biblioteca para a pasta do projeto e devolve o elemento pronto.
|
|
126
|
+
* @param {string} id "fluent3d:<nome>" ou "noto:<código>"
|
|
127
|
+
*/
|
|
128
|
+
async function baixar(id, { pasta, largura = 1920, altura = 1080 } = {}) {
|
|
129
|
+
if (!pasta) return { ok: false, error: 'para baixar é preciso dizer em qual projeto (id).' };
|
|
130
|
+
const [tipo, chave0] = String(id || '').split(/:(.+)/);
|
|
131
|
+
const chave = String(chave0 || '').trim().toLowerCase();
|
|
132
|
+
const lado = Math.round(Math.min(largura, altura) * 0.32);
|
|
133
|
+
if (tipo === 'fluent3d') {
|
|
134
|
+
const item = fluent().find(([nome]) => nome === chave);
|
|
135
|
+
if (!item) return { ok: false, error: `não há o ícone 3D "${chave}" — procure com acao:icones3d termo:"…".` };
|
|
136
|
+
const arquivo = chave.replace(/[^a-z0-9]+/g, '-') + '.png';
|
|
137
|
+
const destino = path.join(pasta, 'midia', 'icones3d', arquivo);
|
|
138
|
+
if (!fs.existsSync(destino)) {
|
|
139
|
+
const r = await baixarUrl(CDN_FLUENT + encodeURI(item[1]), destino);
|
|
140
|
+
if (!r.ok) return r;
|
|
141
|
+
fs.writeFileSync(destino.replace(/\.png$/, '.json'), JSON.stringify({ fonte: 'Microsoft Fluent Emoji 3D', licenca: 'MIT', url: CDN_FLUENT + encodeURI(item[1]) }, null, 2));
|
|
142
|
+
}
|
|
143
|
+
const src = path.relative(pasta, destino).replace(/\\/g, '/');
|
|
144
|
+
return { ok: true, src, licenca: 'MIT',
|
|
145
|
+
elemento: { tipo: 'imagem', src, x: Math.round((largura - lado) / 2), y: Math.round((altura - lado) / 2), largura: lado, altura: lado, ajuste: 'contain' } };
|
|
146
|
+
}
|
|
147
|
+
if (tipo === 'noto') {
|
|
148
|
+
const item = noto().find(([cp]) => cp === chave);
|
|
149
|
+
if (!item) return { ok: false, error: `não há a animação "${chave}" — procure com acao:animacoes termo:"…".` };
|
|
150
|
+
const destino = path.join(pasta, 'midia', 'animacoes', chave + '.json');
|
|
151
|
+
if (!fs.existsSync(destino)) {
|
|
152
|
+
const r = await baixarUrl(LOTTIE_NOTO(chave), destino);
|
|
153
|
+
if (!r.ok) return r;
|
|
154
|
+
fs.writeFileSync(destino.replace(/\.json$/, '.licenca.json'), JSON.stringify({ fonte: 'Noto Emoji Animation (Google)', licenca: 'CC BY 4.0', url: LOTTIE_NOTO(chave) }, null, 2));
|
|
155
|
+
}
|
|
156
|
+
const src = path.relative(pasta, destino).replace(/\\/g, '/');
|
|
157
|
+
return { ok: true, src, licenca: 'CC BY 4.0', creditos: 'Noto Emoji Animation, Google (CC BY 4.0)',
|
|
158
|
+
elemento: { tipo: 'kit', peca: 'animacao', src, loop: true, x: Math.round((largura - lado) / 2), y: Math.round((altura - lado) / 2), largura: lado, altura: lado } };
|
|
159
|
+
}
|
|
160
|
+
return { ok: false, error: 'id da biblioteca é "fluent3d:<nome>" ou "noto:<código>".' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* O CARDÁPIO: tudo o que a biblioteca tem, numa resposta. A IA lê isto e
|
|
165
|
+
* escolhe pelo assunto do vídeo — a regra da casa é que todo elemento tenha a
|
|
166
|
+
* ver com o que a fala diz.
|
|
167
|
+
*/
|
|
168
|
+
function cardapio() {
|
|
169
|
+
let fontes = 0, sons = [];
|
|
170
|
+
try { fontes = Object.keys(require('./biblioteca.js').FONTES || {}).length; } catch { /* sem a lista */ }
|
|
171
|
+
try { sons = Object.keys(require('./sons-leves.js').EFEITOS || {}); } catch { /* sem a lista */ }
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
icones2d: { quantos: '200 mil+ (Iconify: Material, Lucide, Phosphor, marcas…)', acao: 'icones', exemplo: 'termo:"bread"' },
|
|
175
|
+
icones3d: { quantos: fluent().length, fonte: 'Microsoft Fluent Emoji 3D (MIT)', acao: 'icones3d', exemplo: 'termo:"coffee" → id:"fluent3d:hot beverage"' },
|
|
176
|
+
animacoes: { quantas: noto().length, fonte: 'Noto Emoji animado do Google, Lottie (CC BY 4.0)', acao: 'animacoes', exemplo: 'termo:"fire" → id:"noto:1f525"' },
|
|
177
|
+
personagens3d: 'o estilo personagem (voxel): bonecos de blocos e o mascote, com a câmera de cinema — estudio_roteiro estilo:"personagem"',
|
|
178
|
+
efeitosSonoros: { sintetizados: sons, internet: 'acao:efeito termo:"whoosh" (Openverse, licença aberta, até 6 s)' },
|
|
179
|
+
musicas: { internet: 'acao:trilha termo:"lofi" (Openverse, 30 s ou mais)', automatica: 'sem trilha pedida, o PrimoCode escolhe uma sem repetir as últimas' },
|
|
180
|
+
fontes: { quantas: fontes || 128, acao: 'fontes', nota: 'Google Fonts, por tipo (condensada, display, serifada, manuscrita…)' },
|
|
181
|
+
imagens: 'acao:imagem termo:"…" (Openverse e Wikimedia; Google com chave) — só quando a pessoa pediu imagem',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { procurarIcones3d, procurarAnimacoes, baixar, cardapio, termos, PT_EN };
|