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
package/lib/estudio/variedade.js
CHANGED
|
@@ -20,24 +20,84 @@ const fs = require('fs');
|
|
|
20
20
|
const os = require('os');
|
|
21
21
|
const path = require('path');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/* A pasta é lida a cada uso (não no require): o teste troca o HOME depois de
|
|
24
|
+
carregar o módulo e a memória de verdade fica intocada. */
|
|
25
|
+
const PASTA = () => path.join(os.homedir(), '.primocode', 'studio');
|
|
26
|
+
/* Os tipos do gerativo e do fluido (09/10): "vício em cores, músicas, fontes"
|
|
27
|
+
— a paleta, as fontes e o gênero saíam só da semente, sem lembrar do vídeo
|
|
28
|
+
anterior. Agora passam por aqui como o resto. */
|
|
29
|
+
const MEMORIA = { trilha: 16, sfx: 40, broll: 80, icones: 4, estilo: 8, '3d': 24, 'icone-nome': 30, 'paleta-fluido': 6,
|
|
30
|
+
'fonte-fluido': 4, 'humor-gerativo': 4, 'paleta-gerativo': 10, 'fonte-gerativo': 18, 'genero-gerativo': 8,
|
|
31
|
+
'arranjo-icone': 10, 'material-icone': 4, 'heroi-gerativo': 8, 'composicao-gerativo': 12,
|
|
32
|
+
'moldura-gerativo': 4, 'ambiente-gerativo': 4, 'acento-gerativo': 3 };
|
|
33
|
+
|
|
34
|
+
/* A DIVERSIDADE POR PONTOS (07/10): "o sistema não deve simplesmente proibir
|
|
35
|
+
repetição; escolhas usadas recentemente recebem penalidade, as muito usadas
|
|
36
|
+
recebem penalidade maior, as antigas podem voltar, e o que é adequado ao
|
|
37
|
+
conteúdo continua tendo prioridade" (Joel). Antes era uma janela fixa: o
|
|
38
|
+
que saía da janela voltava como se nunca tivesse sido usado.
|
|
39
|
+
pontos = PESO_ADEQUACAO × adequação − recência − frequência
|
|
40
|
+
- recência: 1 para a última usada, cai pela metade a cada MEIA_VIDA_USOS
|
|
41
|
+
escolhas do mesmo tipo (exp), e some quando sai da lista;
|
|
42
|
+
- frequência: log2(1 + usos), com os usos perdendo metade a cada
|
|
43
|
+
MEIA_VIDA_DIAS — o muito usado pesa mais, e o tempo perdoa;
|
|
44
|
+
- adequação: 0 a 1, de quem chama (o clima, o assunto). Com peso 2, o que
|
|
45
|
+
serve ao roteiro ganha do que só é novo: a variedade não quebra a coerência. */
|
|
46
|
+
const PESO_ADEQUACAO = 2;
|
|
47
|
+
const PESO_FREQUENCIA = 0.25;
|
|
48
|
+
const MEIA_VIDA_DIAS = 21;
|
|
49
|
+
const EMPATE = 0.05; // dentro disso, é sorteio
|
|
50
|
+
|
|
51
|
+
function arquivo(tipo) { return path.join(PASTA(), `${tipo}-usadas.json`); }
|
|
52
|
+
function arquivoHistorico(tipo) { return path.join(PASTA(), `${tipo}-historico.json`); }
|
|
27
53
|
|
|
28
54
|
function usadas(tipo) {
|
|
29
55
|
try { const l = JSON.parse(fs.readFileSync(arquivo(tipo), 'utf8')); return Array.isArray(l) ? l : []; } catch { return []; }
|
|
30
56
|
}
|
|
31
57
|
|
|
32
|
-
|
|
58
|
+
/** { id: { n: usos, t: ms da última } } — a frequência, que a lista de recentes não guarda. */
|
|
59
|
+
function historico(tipo) {
|
|
60
|
+
try { const h = JSON.parse(fs.readFileSync(arquivoHistorico(tipo), 'utf8')); return h && typeof h === 'object' && !Array.isArray(h) ? h : {}; } catch { return {}; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const usosHoje = (reg, agora) => (reg ? reg.n * Math.pow(0.5, Math.max(0, agora - reg.t) / (MEIA_VIDA_DIAS * 864e5)) : 0);
|
|
64
|
+
|
|
65
|
+
function marcar(tipo, id, agora = Date.now()) {
|
|
33
66
|
if (!id) return;
|
|
34
67
|
try {
|
|
35
|
-
fs.mkdirSync(PASTA, { recursive: true });
|
|
36
|
-
|
|
68
|
+
fs.mkdirSync(PASTA(), { recursive: true });
|
|
69
|
+
// A lista de recentes é maior que a janela de antes: a recência decai, não corta.
|
|
70
|
+
const l = [id, ...usadas(tipo).filter((x) => x !== id)].slice(0, (MEMORIA[tipo] || 20) * 2);
|
|
37
71
|
fs.writeFileSync(arquivo(tipo), JSON.stringify(l));
|
|
72
|
+
const h = historico(tipo);
|
|
73
|
+
h[id] = { n: usosHoje(h[id], agora) + 1, t: agora };
|
|
74
|
+
// Guarda os 400 mais recentes, para o arquivo não crescer sem fim.
|
|
75
|
+
const ids = Object.keys(h).sort((x, y) => h[y].t - h[x].t).slice(0, 400);
|
|
76
|
+
fs.writeFileSync(arquivoHistorico(tipo), JSON.stringify(Object.fromEntries(ids.map((k) => [k, h[k]]))));
|
|
38
77
|
} catch { /* sem memória: só perde a garantia de não repetir */ }
|
|
39
78
|
}
|
|
40
79
|
|
|
80
|
+
/** Quanto pesa ter usado `id`: recência + frequência. 0 = nunca usado. */
|
|
81
|
+
function penalidade(id, tipo, { lista = usadas(tipo), hist = historico(tipo), agora = Date.now() } = {}) {
|
|
82
|
+
const pos = lista.indexOf(id);
|
|
83
|
+
const meiaVida = Math.max(1, (MEMORIA[tipo] || 20) / 4);
|
|
84
|
+
const recencia = pos < 0 ? 0 : Math.pow(0.5, pos / meiaVida);
|
|
85
|
+
return recencia + PESO_FREQUENCIA * Math.log2(1 + usosHoje(hist[id], agora));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Os pontos de cada opção: adequação menos penalidade. `adequacao` é uma
|
|
90
|
+
* função (opção → 0..1) ou um objeto {id: 0..1}; sem ela, tudo serve igual (1).
|
|
91
|
+
*/
|
|
92
|
+
function pontuar(opcoes, tipo, { prefixo = '', adequacao = null, chave = (o) => o, agora = Date.now() } = {}) {
|
|
93
|
+
const lista = usadas(tipo), hist = historico(tipo);
|
|
94
|
+
const fit = (o) => {
|
|
95
|
+
const v = typeof adequacao === 'function' ? adequacao(o) : adequacao && typeof adequacao === 'object' ? adequacao[chave(o)] : 1;
|
|
96
|
+
return Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0;
|
|
97
|
+
};
|
|
98
|
+
return opcoes.map((o) => ({ opcao: o, pontos: PESO_ADEQUACAO * fit(o) - penalidade(prefixo + chave(o), tipo, { lista, hist, agora }) }));
|
|
99
|
+
}
|
|
100
|
+
|
|
41
101
|
function embaralhar(lista, aleatorio = Math.random) {
|
|
42
102
|
const a = [...lista];
|
|
43
103
|
for (let i = a.length - 1; i > 0; i--) {
|
|
@@ -47,25 +107,27 @@ function embaralhar(lista, aleatorio = Math.random) {
|
|
|
47
107
|
return a;
|
|
48
108
|
}
|
|
49
109
|
|
|
50
|
-
/** Os itens da busca na ordem de tentar:
|
|
51
|
-
function ordenar(itens, tipo, aleatorio = Math.random) {
|
|
52
|
-
const ja = new Set(usadas(tipo));
|
|
110
|
+
/** Os itens da busca na ordem de tentar: mais pontos primeiro; quase empatados, sorteados. */
|
|
111
|
+
function ordenar(itens, tipo, aleatorio = Math.random, adequacao = null) {
|
|
53
112
|
const validos = (itens || []).filter((x) => x && x.url);
|
|
54
|
-
return
|
|
113
|
+
return pontuar(validos, tipo, { adequacao, chave: (x) => x.url })
|
|
114
|
+
.map((p) => ({ ...p, pontos: p.pontos + aleatorio() * EMPATE }))
|
|
115
|
+
.sort((a, b) => b.pontos - a.pontos)
|
|
116
|
+
.map((p) => p.opcao);
|
|
55
117
|
}
|
|
56
118
|
|
|
57
119
|
/**
|
|
58
|
-
* Uma das opções,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
120
|
+
* Uma das opções, pelos pontos: as nunca usadas primeiro, a recente e a muito
|
|
121
|
+
* usada por último, e a adequada (opcional) acima de todas. Entre as quase
|
|
122
|
+
* empatadas, sorteia. Marca a escolhida na memória do tipo. Com `prefixo`,
|
|
123
|
+
* várias listas dividem o mesmo arquivo ('acabamento:fosco').
|
|
61
124
|
*/
|
|
62
|
-
function sortear(opcoes, tipo, { prefixo = '', aleatorio = Math.random } = {}) {
|
|
125
|
+
function sortear(opcoes, tipo, { prefixo = '', aleatorio = Math.random, adequacao = null } = {}) {
|
|
63
126
|
if (!opcoes || !opcoes.length) return null;
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const escolha = [...novas, ...velhas][0];
|
|
127
|
+
const pts = pontuar(opcoes, tipo, { prefixo, adequacao });
|
|
128
|
+
const melhor = Math.max(...pts.map((p) => p.pontos));
|
|
129
|
+
const quase = pts.filter((p) => p.pontos >= melhor - EMPATE).map((p) => p.opcao);
|
|
130
|
+
const escolha = quase[Math.floor(aleatorio() * quase.length) % quase.length];
|
|
69
131
|
marcar(tipo, prefixo + escolha);
|
|
70
132
|
return escolha;
|
|
71
133
|
}
|
|
@@ -179,5 +241,5 @@ function trocarIcones(plano, troca = {}) {
|
|
|
179
241
|
return n;
|
|
180
242
|
}
|
|
181
243
|
|
|
182
|
-
module.exports = { usadas, marcar, embaralhar, ordenar, sortear, climaDe, termosTrilha, termosSfx, termosBroll, buscarSemRepetir,
|
|
244
|
+
module.exports = { usadas, historico, marcar, penalidade, pontuar, embaralhar, ordenar, sortear, climaDe, termosTrilha, termosSfx, termosBroll, buscarSemRepetir,
|
|
183
245
|
colecaoIcones, iconesTrocaveis, trocarIcones, CLIMAS, SFX, JEITOS, FAMILIAS_ICONE };
|
package/lib/video/filters.js
CHANGED
|
@@ -224,7 +224,12 @@ function montarGrafo(tl, { from = 0, to, camadasDeTexto = {}, escala = 1, audio
|
|
|
224
224
|
else f.push(`scale=${bw}:${bh}:force_original_aspect_ratio=increase`, `crop=${bw}:${bh}`);
|
|
225
225
|
f.push('setsar=1');
|
|
226
226
|
const zoom = expressao(c.keyframes.zoom, X, fps);
|
|
227
|
-
|
|
227
|
+
// O foco do zoom (fx,fy) é o ponto do clipe que fica no meio; a vista nunca sai do quadro.
|
|
228
|
+
const fx = expressao(c.keyframes.fx, X, fps);
|
|
229
|
+
const fy = expressao(c.keyframes.fy, X, fps);
|
|
230
|
+
const zx = fx ? `max(0,min(iw-iw/zoom,(${fx})*iw-iw/zoom/2))` : 'iw/2-(iw/zoom/2)';
|
|
231
|
+
const zy = fy ? `max(0,min(ih-ih/zoom,(${fy})*ih-ih/zoom/2))` : 'ih/2-(ih/zoom/2)';
|
|
232
|
+
if (zoom) f.push(`zoompan=z='max(1,${zoom})':x='${zx}':y='${zy}':d=1:s=${bw}x${bh}:fps=${fps}`);
|
|
228
233
|
for (const e of c.effects) f.push(...filtrosDoEfeito(e));
|
|
229
234
|
f.push('format=yuva420p');
|
|
230
235
|
const op = expressao(c.keyframes.opacity, Xt, fps);
|
package/lib/video/ops.js
CHANGED
|
@@ -184,7 +184,7 @@ const OPS = {
|
|
|
184
184
|
},
|
|
185
185
|
},
|
|
186
186
|
set_keyframes: {
|
|
187
|
-
uso: 'set_keyframes {id,prop(zoom|cx|cy|rotation|opacity|volume),keys:[{frame(timeline),value,interp?:ease|linear|hold}]} — keys:[] limpa',
|
|
187
|
+
uso: 'set_keyframes {id,prop(zoom|fx|fy|cx|cy|rotation|opacity|volume),keys:[{frame(timeline),value,interp?:ease|linear|hold}]} — keys:[] limpa',
|
|
188
188
|
async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'set_keyframes', (tl) => { E.keyframes(tl, a.id, a.prop, a.keys); }); },
|
|
189
189
|
},
|
|
190
190
|
apply_layout: {
|
|
@@ -273,7 +273,8 @@ const OPS = {
|
|
|
273
273
|
const FXO = require('./ops-fx.js');
|
|
274
274
|
const PED = require('./pedido.js');
|
|
275
275
|
const AUTO = require('./auto.js');
|
|
276
|
-
|
|
276
|
+
const ZOOM = require('./zoom.js'); // o zoom que segue o cursor (gravação de tela): fica na ajuda principal
|
|
277
|
+
Object.assign(OPS, FXO.OPS, PED.OPS, AUTO.OPS, ZOOM.OPS);
|
|
277
278
|
|
|
278
279
|
const ALIAS = { timeline: 'get_timeline', transcript: 'get_transcript', captions: 'add_captions', texts: 'add_texts', render: 'export', pedido: 'do', analyze: 'track' };
|
|
279
280
|
|
|
@@ -310,7 +311,7 @@ const EFEITOS = new Set([...Object.keys(FXO.OPS), ...Object.keys(PED.OPS), ...Ob
|
|
|
310
311
|
/* A ajuda principal fica curta (o modelo a lê inteira a cada tarefa): os
|
|
311
312
|
efeitos entram num resumo de três linhas, e o detalhe em `help efeitos`. */
|
|
312
313
|
function ajuda(secao) {
|
|
313
|
-
Object.assign(EX, FXO.EX, PED.EX, AUTO.EX);
|
|
314
|
+
Object.assign(EX, FXO.EX, PED.EX, AUTO.EX, ZOOM.EX);
|
|
314
315
|
const linha = (nome, o, linhas) => {
|
|
315
316
|
linhas.push(' ' + o.uso);
|
|
316
317
|
if (EX[nome]) linhas.push(` ex: primocode video ${nome} --json '${EX[nome]}'`);
|
package/lib/video/timeline.js
CHANGED
|
@@ -29,7 +29,7 @@ const CABE = { video: ['video', 'image'], audio: ['audio'], text: ['text'] };
|
|
|
29
29
|
|
|
30
30
|
const TRANSFORM_PADRAO = { cx: 0.5, cy: 0.5, w: 1, h: 1, rotation: 0 };
|
|
31
31
|
const CROP_PADRAO = { l: 0, t: 0, r: 0, b: 0 };
|
|
32
|
-
const PROPS_ANIMAVEIS = ['zoom', 'cx', 'cy', 'rotation', 'opacity', 'volume'];
|
|
32
|
+
const PROPS_ANIMAVEIS = ['zoom', 'fx', 'fy', 'cx', 'cy', 'rotation', 'opacity', 'volume']; // fx,fy = foco do zoom (0–1 do clipe)
|
|
33
33
|
const INTERP = ['linear', 'ease', 'hold'];
|
|
34
34
|
|
|
35
35
|
function novo({ nome = 'video', width = 1080, height = 1920, fps = 30 } = {}) {
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zoom.js — o zoom que segue o cursor numa gravação de tela.
|
|
3
|
+
*
|
|
4
|
+
* Três peças, como conta pura e testável sem vídeo nenhum:
|
|
5
|
+
*
|
|
6
|
+
* · os FOCOS: o clique, quando há registro dele; sem clique, o lugar onde
|
|
7
|
+
* o movimento PARA (o cursor anda, assenta e alguém vai clicar ali);
|
|
8
|
+
* · os TRECHOS: focos próximos no tempo viram um zoom só; uma pausa longa
|
|
9
|
+
* ou um salto para longe (com tempo de sobra) fecham o zoom e abrem outro;
|
|
10
|
+
* · a CÂMERA: dentro do trecho ela fica parada enquanto o cursor anda numa
|
|
11
|
+
* zona morta no meio da vista, e só anda quando ele sai dali — puxada por
|
|
12
|
+
* uma mola criticamente amortecida, sem tremer e sem passar do ponto.
|
|
13
|
+
*
|
|
14
|
+
* Reimplementação independente da ideia (curva, regra de clique e mola):
|
|
15
|
+
* nenhuma linha vem de outro projeto.
|
|
16
|
+
*
|
|
17
|
+
* Saída: keyframes de zoom e do FOCO do zoom (fx, fy: 0–1 do clipe), que o
|
|
18
|
+
* filters.js já sabe desenhar no zoompan.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { spawn } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const lim = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
26
|
+
|
|
27
|
+
// ── medir: onde a tela mexeu (quando ninguém gravou o cursor) ────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Quadros cinza (Uint8Array w*h, um por amostra) → [{t, x, y, move}].
|
|
31
|
+
* x,y = centro do que mudou desde o quadro anterior (o cursor, numa tela
|
|
32
|
+
* parada). Mudança grande demais (rolagem, troca de janela) não é cursor.
|
|
33
|
+
*/
|
|
34
|
+
function movimento(quadros, w, h, fps, { limiar = 24, maxFracao = 0.2 } = {}) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (let i = 1; i < quadros.length; i++) {
|
|
37
|
+
const a = quadros[i - 1];
|
|
38
|
+
const b = quadros[i];
|
|
39
|
+
let n = 0;
|
|
40
|
+
let sx = 0;
|
|
41
|
+
let sy = 0;
|
|
42
|
+
for (let k = 0; k < w * h; k++) {
|
|
43
|
+
if (Math.abs(a[k] - b[k]) > limiar) { n++; sx += k % w; sy += Math.floor(k / w); }
|
|
44
|
+
}
|
|
45
|
+
const t = i / fps;
|
|
46
|
+
if (n < 2 || n > maxFracao * w * h) out.push({ t, x: null, y: null, move: false });
|
|
47
|
+
else out.push({ t, x: (sx / n + 0.5) / w, y: (sy / n + 0.5) / h, move: true });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Trilha de cursor [{t,x,y,click?}] → amostras com `move` (andou desde a anterior). */
|
|
53
|
+
function doCursor(cursor, { parado = 0.004 } = {}) {
|
|
54
|
+
const c = cursor.filter((p) => Number.isFinite(p.t) && Number.isFinite(p.x) && Number.isFinite(p.y)).sort((a, b) => a.t - b.t);
|
|
55
|
+
return c.map((p, i) => ({ ...p, move: i > 0 && Math.hypot(p.x - c[i - 1].x, p.y - c[i - 1].y) > parado }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Os focos: os cliques, se houver; senão, onde o movimento para — andou por
|
|
60
|
+
* `minAndou` s e depois ficou parado por `minParado` s. [{t, x, y}].
|
|
61
|
+
*/
|
|
62
|
+
function focos(amostras, { minAndou = 0.15, minParado = 0.3 } = {}) {
|
|
63
|
+
const cliques = amostras.filter((a) => a.click);
|
|
64
|
+
if (cliques.length) return cliques.map(({ t, x, y }) => ({ t, x, y }));
|
|
65
|
+
const out = [];
|
|
66
|
+
let ini = null;
|
|
67
|
+
let ult = null;
|
|
68
|
+
for (const a of amostras) {
|
|
69
|
+
if (a.move) { if (ini === null) ini = a.t; ult = a; continue; }
|
|
70
|
+
if (ult && ini !== null && ult.t - ini >= minAndou - 1e-9 && a.t - ult.t >= minParado - 1e-9) {
|
|
71
|
+
out.push({ t: ult.t, x: ult.x, y: ult.y });
|
|
72
|
+
ini = null;
|
|
73
|
+
ult = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── os trechos e a câmera ────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Focos (t em QUADROS do clipe) → trechos [{de, ate, focos}]. Fecha o trecho
|
|
83
|
+
* na pausa maior que `juntar` ou no salto maior que `longe` quando há tempo
|
|
84
|
+
* de sair e voltar (2 rampas); salto rápido fica no mesmo trecho e vira pan.
|
|
85
|
+
*/
|
|
86
|
+
function trechos(fs, { fps, juntar = 2.5, longe = 0.35, antes = 0.4, segura = 1.2, rampa = 0.5 } = {}) {
|
|
87
|
+
const out = [];
|
|
88
|
+
const R = rampa * fps;
|
|
89
|
+
for (const f of [...fs].sort((a, b) => a.t - b.t)) {
|
|
90
|
+
const u = out[out.length - 1];
|
|
91
|
+
const ult = u && u.focos[u.focos.length - 1];
|
|
92
|
+
const gap = ult ? f.t - ult.t : Infinity;
|
|
93
|
+
if (u && gap <= juntar * fps && !(Math.hypot(f.x - ult.x, f.y - ult.y) > longe && gap > 2 * R + segura * fps)) { u.focos.push(f); continue; }
|
|
94
|
+
out.push({ focos: [f] });
|
|
95
|
+
}
|
|
96
|
+
for (const s of out) {
|
|
97
|
+
s.de = Math.max(0, Math.round(s.focos[0].t - antes * fps - R));
|
|
98
|
+
s.ate = Math.round(s.focos[s.focos.length - 1].t + segura * fps);
|
|
99
|
+
}
|
|
100
|
+
// Um trecho não invade a saída do anterior.
|
|
101
|
+
for (let i = 1; i < out.length; i++) out[i - 1].ate = Math.max(out[i - 1].focos[out[i - 1].focos.length - 1].t, Math.min(out[i - 1].ate, out[i].de - R));
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A câmera, quadro a quadro: [{z, x, y}] para `dur` quadros. `pos(f)` = onde
|
|
107
|
+
* o cursor está no quadro f (ou null). Zona morta = fração da meia-vista.
|
|
108
|
+
*/
|
|
109
|
+
function camera(dur, ts, pos, { fps, zoom = 1.8, rampa = 0.5, zonaMorta = 0.45, hz = 1.4 } = {}) {
|
|
110
|
+
const R = Math.max(1, Math.round(rampa * fps));
|
|
111
|
+
const out = [];
|
|
112
|
+
const w = 2 * Math.PI * hz;
|
|
113
|
+
const dt = 1 / fps;
|
|
114
|
+
let cam = { x: 0.5, y: 0.5 };
|
|
115
|
+
let vel = { x: 0, y: 0 };
|
|
116
|
+
let alvo = { ...cam };
|
|
117
|
+
let s = null;
|
|
118
|
+
for (let f = 0; f < dur; f++) {
|
|
119
|
+
const novo = ts.find((t) => f >= t.de && f < t.ate + R);
|
|
120
|
+
if (novo !== s) {
|
|
121
|
+
s = novo;
|
|
122
|
+
if (s) { cam = { x: s.focos[0].x, y: s.focos[0].y }; alvo = { ...cam }; vel = { x: 0, y: 0 }; }
|
|
123
|
+
}
|
|
124
|
+
let z = 1;
|
|
125
|
+
if (s) {
|
|
126
|
+
const p = f < s.de + R ? (f - s.de) / R : f >= s.ate ? 1 - (f - s.ate) / R : 1;
|
|
127
|
+
z = 1 + (zoom - 1) * p * p * (3 - 2 * p);
|
|
128
|
+
// Segue o cursor só entre o primeiro e o último foco: antes, mira o foco; depois, segura.
|
|
129
|
+
const c = f >= s.focos[0].t && f <= s.focos[s.focos.length - 1].t ? pos(f) : null;
|
|
130
|
+
const meia = 0.5 / zoom;
|
|
131
|
+
if (c) {
|
|
132
|
+
for (const k of ['x', 'y']) {
|
|
133
|
+
const d = c[k] - alvo[k];
|
|
134
|
+
const zm = zonaMorta * meia;
|
|
135
|
+
if (Math.abs(d) > zm) alvo[k] = c[k] - Math.sign(d) * zm;
|
|
136
|
+
alvo[k] = lim(alvo[k], meia, 1 - meia);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of ['x', 'y']) {
|
|
140
|
+
vel[k] += (w * w * (alvo[k] - cam[k]) - 2 * w * vel[k]) * dt; // mola crítica: chega sem passar
|
|
141
|
+
cam[k] += vel[k] * dt;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const meiaZ = 0.5 / z;
|
|
145
|
+
out.push({ z, x: lim(cam.x, meiaZ, 1 - meiaZ), y: lim(cam.y, meiaZ, 1 - meiaZ) });
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Ramer–Douglas–Peucker numa série: os quadros que bastam para a reta não errar mais que `tol`. */
|
|
151
|
+
function enxugar(v, tol) {
|
|
152
|
+
const manter = new Set([0, v.length - 1]);
|
|
153
|
+
const pilha = [[0, v.length - 1]];
|
|
154
|
+
while (pilha.length) {
|
|
155
|
+
const [a, b] = pilha.pop();
|
|
156
|
+
let pior = -1;
|
|
157
|
+
let max = tol;
|
|
158
|
+
for (let i = a + 1; i < b; i++) {
|
|
159
|
+
const d = Math.abs(v[i] - (v[a] + ((v[b] - v[a]) * (i - a)) / (b - a)));
|
|
160
|
+
if (d > max) { max = d; pior = i; }
|
|
161
|
+
}
|
|
162
|
+
if (pior >= 0) { manter.add(pior); pilha.push([a, pior], [pior, b]); }
|
|
163
|
+
}
|
|
164
|
+
return [...manter].sort((x, y) => x - y);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Câmera → keyframes {zoom, fx, fy} em quadros do clipe ({frame, value, interp}). */
|
|
168
|
+
function keyframes(cam, { tol = 0.002 } = {}) {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const [prop, k] of [['zoom', 'z'], ['fx', 'x'], ['fy', 'y']]) {
|
|
171
|
+
const v = cam.map((c) => c[k]);
|
|
172
|
+
out[prop] = enxugar(v, prop === 'zoom' ? 0.01 : tol).map((f) => ({ frame: f, value: Math.round(v[f] * 10000) / 10000, interp: 'linear' }));
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Tudo junto. `amostras` com t em QUADROS do clipe e x,y em 0–1 do clipe.
|
|
179
|
+
* Devolve {focos, trechos, cam, keys}.
|
|
180
|
+
*/
|
|
181
|
+
function planejar(amostras, dur, opts) {
|
|
182
|
+
const fs = focos(amostras.map((a) => ({ ...a, t: a.t / opts.fps })), opts).map((f) => ({ ...f, t: Math.round(f.t * opts.fps) }));
|
|
183
|
+
const ts = trechos(fs, opts);
|
|
184
|
+
const comPos = amostras.filter((a) => a.x !== null && a.x !== undefined);
|
|
185
|
+
let i = 0;
|
|
186
|
+
const pos = (f) => { // a última posição conhecida até f (f só cresce)
|
|
187
|
+
while (i + 1 < comPos.length && comPos[i + 1].t <= f) i++;
|
|
188
|
+
return comPos[i] && comPos[i].t <= f ? comPos[i] : null;
|
|
189
|
+
};
|
|
190
|
+
const cam = camera(dur, ts, pos, opts);
|
|
191
|
+
return { focos: fs, trechos: ts, cam, keys: keyframes(cam) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── o ffmpeg mede ────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/** Quadros cinza pequenos da origem [de, ate) segundos, a `fps` por segundo. */
|
|
197
|
+
function quadrosCinza(arquivo, de, ate, { fps = 15, w = 160, h = 90 } = {}) {
|
|
198
|
+
const video = require('../estudio/video.js');
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const p = spawn(video.ffmpeg(), ['-hide_banner', '-loglevel', 'error', '-ss', String(de), '-t', String(ate - de), '-i', arquivo,
|
|
201
|
+
'-vf', `fps=${fps},scale=${w}:${h}:flags=area,format=gray`, '-f', 'rawvideo', '-']);
|
|
202
|
+
const partes = [];
|
|
203
|
+
p.stdout.on('data', (d) => partes.push(d));
|
|
204
|
+
p.on('error', reject);
|
|
205
|
+
p.on('close', (code) => {
|
|
206
|
+
if (code !== 0) return reject(new Error('não consegui ler os quadros'));
|
|
207
|
+
const b = Buffer.concat(partes);
|
|
208
|
+
const out = [];
|
|
209
|
+
for (let k = 0; k + w * h <= b.length; k += w * h) out.push(b.subarray(k, k + w * h));
|
|
210
|
+
resolve(out);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── a operação do editor ─────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/** Ponto da ORIGEM (0–1) → ponto do clipe (0–1), depois do encaixe (cover/contain). */
|
|
218
|
+
function paraClipe(tl, clip, asset, x, y) {
|
|
219
|
+
if (clip.fit === 'stretch') return { x, y };
|
|
220
|
+
const bw = clip.transform.w * tl.width;
|
|
221
|
+
const bh = clip.transform.h * tl.height;
|
|
222
|
+
const aw = asset.width || bw;
|
|
223
|
+
const ah = asset.height || bh;
|
|
224
|
+
const esc = clip.fit === 'contain' ? Math.min(bw / aw, bh / ah) : Math.max(bw / aw, bh / ah);
|
|
225
|
+
return { x: lim(0.5 + ((x - 0.5) * aw * esc) / bw, 0, 1), y: lim(0.5 + ((y - 0.5) * ah * esc) / bh, 0, 1) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const OPS = {
|
|
229
|
+
auto_zoom: {
|
|
230
|
+
uso: 'auto_zoom {id?,zoom?:1.8,cursor?:[{t(s da origem),x,y(0–1),click?}]|cursorFile?,deadZone?:0.45,remove?} — gravação de tela: entra no clique (sem cursor: onde o movimento para), segue com zona morta e mola, sai na pausa',
|
|
231
|
+
async run(ctx, a) {
|
|
232
|
+
const T = require('./timeline.js');
|
|
233
|
+
const E = require('./edits.js');
|
|
234
|
+
const { mutar } = require('./ops.js');
|
|
235
|
+
if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."}');
|
|
236
|
+
const primeiro = T.todosOsClipes(ctx.tl).find((x) => x.clip.type === 'video');
|
|
237
|
+
if (!a.id && !primeiro) throw T.erro('não há clipe de vídeo');
|
|
238
|
+
const { clip } = T.achar(ctx.tl, a.id || primeiro.clip.id);
|
|
239
|
+
if (a.remove) return mutar(ctx, 'auto_zoom', (tl) => { for (const p of ['zoom', 'fx', 'fy']) E.keyframes(tl, clip.id, p, []); });
|
|
240
|
+
const asset = ctx.tl.assets[clip.asset];
|
|
241
|
+
const fps = ctx.tl.fps;
|
|
242
|
+
const { de, ate } = T.janelaOrigem(ctx.tl, clip);
|
|
243
|
+
let cru;
|
|
244
|
+
let fonte;
|
|
245
|
+
if (a.cursor || a.cursorFile) {
|
|
246
|
+
cru = doCursor(a.cursor || JSON.parse(require('fs').readFileSync(a.cursorFile, 'utf8')));
|
|
247
|
+
fonte = cru.some((x) => x.click) ? 'cliques' : 'cursor';
|
|
248
|
+
} else {
|
|
249
|
+
const mfps = 15;
|
|
250
|
+
const h = 2 * Math.round((80 * (asset.height || 9)) / (asset.width || 16));
|
|
251
|
+
cru = movimento(await quadrosCinza(asset.path, de, ate, { fps: mfps, w: 160, h }), 160, h, mfps).map((x) => ({ ...x, t: x.t + de }));
|
|
252
|
+
fonte = 'movimento';
|
|
253
|
+
}
|
|
254
|
+
const amostras = [];
|
|
255
|
+
for (const x of cru) {
|
|
256
|
+
const f = T.origemParaQuadro(ctx.tl, clip, x.t);
|
|
257
|
+
if (f === null) continue;
|
|
258
|
+
const p = x.x === null ? { x: null, y: null } : paraClipe(ctx.tl, clip, asset, x.x, x.y);
|
|
259
|
+
amostras.push({ ...x, ...p, t: f - clip.startFrame });
|
|
260
|
+
}
|
|
261
|
+
const pl = planejar(amostras, clip.durationFrames, { fps, zoom: lim(Number(a.zoom) || 1.8, 1.1, 3), zonaMorta: a.deadZone ?? 0.45 });
|
|
262
|
+
const zooms = pl.trechos.map((t) => [t.de + clip.startFrame, t.ate + clip.startFrame]);
|
|
263
|
+
if (!pl.focos.length) return { ok: true, nada: true, fonte, focos: 0, aviso: 'nenhum clique nem parada do cursor: nada para aproximar' };
|
|
264
|
+
return mutar(ctx, 'auto_zoom', (tl) => {
|
|
265
|
+
for (const [p, ks] of Object.entries(pl.keys)) E.keyframes(tl, clip.id, p, ks.map((k) => ({ ...k, frame: k.frame + clip.startFrame })));
|
|
266
|
+
return { fonte, focos: pl.focos.length, zooms };
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Sem linha de exemplo: a ajuda principal cabe em 60 linhas, e o `uso` já mostra a forma.
|
|
273
|
+
const EX = {};
|
|
274
|
+
|
|
275
|
+
module.exports = { movimento, doCursor, focos, trechos, camera, enxugar, keyframes, planejar, quadrosCinza, paraClipe, OPS, EX };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0-beta.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/hibrido.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -57,11 +57,9 @@ Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
|
|
|
57
57
|
- **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
|
|
58
58
|
app nasce do tamanho e da COR do ícone.
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Pedido de motion (ou sem referências): `estilo: "gerativo"`, só o roteiro
|
|
64
|
-
(`fala`, `destaque`); `roteiro.semente` sorteia outro, `modelo3d` traz o 3D.
|
|
60
|
+
Sem imagem é o padrão (foto só se pedirem). Decida `clima`, o impacto (`destaque`, ≤1 em 3 partes), o
|
|
61
|
+
respiro e o `estilo` deste roteiro: `gerativo`, `fluido`, `impacto`, `vox`, `marca`, `lancamento`; vazio, gira.
|
|
62
|
+
`hibrido` só com imagem pedida. Leia os avisos da revisão.
|
|
65
63
|
|
|
66
64
|
## Cor e tipo
|
|
67
65
|
|