primocode 10.1.0 → 10.2.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/catalogo.js +3 -3
- package/lib/estudio/assets.js +75 -1
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +52 -12
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/edicao.js +34 -16
- package/lib/estudio/estilo.js +17 -6
- package/lib/estudio/fluido.js +82 -13
- package/lib/estudio/index.js +122 -30
- package/lib/estudio/kit/fluido.jsx +130 -9
- package/lib/estudio/referencias.js +36 -25
- package/lib/estudio/remotion.js +2 -0
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variedade.js +71 -17
- 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
|
@@ -23,6 +23,7 @@ import { CURVAS_KIT, alfa } from './base';
|
|
|
23
23
|
const ed = CURVAS_KIT.editorial, cam = CURVAS_KIT.camera;
|
|
24
24
|
const c01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
25
25
|
const lerp = (a, b, k) => a + (b - a) * k;
|
|
26
|
+
const lum = (hex) => { const n = parseInt(String(hex).slice(1), 16); return (0.299 * (n >> 16 & 255) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255)) / 255; };
|
|
26
27
|
const entra = (t) => t * t;
|
|
27
28
|
const SANS = (f) => '"' + f + '", system-ui, sans-serif';
|
|
28
29
|
const SERIF = (f) => '"' + f + '", Georgia, serif';
|
|
@@ -52,7 +53,7 @@ function medir(t, tam, peso, serifa, P, mono) {
|
|
|
52
53
|
|
|
53
54
|
/* Um bloco de texto: quebra palavra a palavra (largura medida), cada linha
|
|
54
55
|
centrada em cx (ou alinhada à esquerda/direita), o bloco centrado em cy. */
|
|
55
|
-
function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', cor, mono = false }) {
|
|
56
|
+
function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', cor, corAcento = null, mono = false }) {
|
|
56
57
|
const esp = medir(' ', tam, peso, false, P, mono);
|
|
57
58
|
const ws = f.palavras.map((w) => ({ ...w, w: medir(w.t, w.acento ? tam * 1.14 : tam, peso, w.acento, P, mono) }));
|
|
58
59
|
const linhas = [[]];
|
|
@@ -67,7 +68,7 @@ function bloco(P, f, { tam, peso = 400, max = 1e9, cx, cy, alinha = 'centro', co
|
|
|
67
68
|
const top = cy - h / 2;
|
|
68
69
|
const x0 = alinha === 'centro' ? cx - w / 2 : alinha === 'esq' ? cx : cx - w;
|
|
69
70
|
return {
|
|
70
|
-
f, tam, peso, cor, esp, lh, w, h, top, cx, cy, mono, left: x0,
|
|
71
|
+
f, tam, peso, cor, corAcento, esp, lh, w, h, top, cx, cy, mono, left: x0,
|
|
71
72
|
linhas: linhas.map((l, i) => ({ palavras: l, y: top + i * lh, x: alinha === 'centro' ? cx - larg(l) / 2 : alinha === 'esq' ? cx : cx - larg(l) })),
|
|
72
73
|
};
|
|
73
74
|
}
|
|
@@ -108,6 +109,60 @@ function leiaute(P) {
|
|
|
108
109
|
ent = [0.3, 0.8];
|
|
109
110
|
}
|
|
110
111
|
const poe = (bl) => { out.blocos.push(bl); return bl; };
|
|
112
|
+
/* A IMAGEM (fluido.js escolhe a batida e o jeito): o texto se arruma em
|
|
113
|
+
volta dela, e o portador vira um ponto discreto enquanto a foto manda. */
|
|
114
|
+
if (b.imagem && pr) {
|
|
115
|
+
const j = b.imagem.jeito;
|
|
116
|
+
const t0 = Math.max(b.de + 0.05, Math.min(...b.frases.map((f) => f.palavras[0].em)));
|
|
117
|
+
const [sb] = fr('sub');
|
|
118
|
+
const branco = '#ffffff';
|
|
119
|
+
let foto, tY;
|
|
120
|
+
if (j === 'cartao') {
|
|
121
|
+
const cw = deitado ? W * 0.36 : W * 0.74, ch = deitado ? H * 0.62 : H * 0.42;
|
|
122
|
+
foto = { x: deitado ? cx + W * 0.2 : cx, y: deitado ? cy : cy + H * 0.12, w: cw, h: ch };
|
|
123
|
+
tY = deitado ? cy : cy - H * 0.24;
|
|
124
|
+
} else if (j === 'faixa') {
|
|
125
|
+
foto = { x: cx, y: deitado ? cy - H * 0.08 : cy - H * 0.06, w: W, h: deitado ? H * 0.46 : H * 0.34 };
|
|
126
|
+
tY = foto.y + foto.h / 2 + 130 * u;
|
|
127
|
+
} else { foto = { x: cx, y: cy, w: W, h: H }; tY = j === 'vazado' ? cy : cy + H * (deitado ? 0.22 : 0.2); }
|
|
128
|
+
const tx2 = j === 'cartao' && deitado ? cx - W * 0.2 : cx;
|
|
129
|
+
const sobreFoto = j === 'janela' || j === 'circulo';
|
|
130
|
+
let tam = (j === 'vazado' ? 200 : sobreFoto ? 92 : 76) * u;
|
|
131
|
+
if (j === 'vazado') {
|
|
132
|
+
const l100 = pr.palavras.reduce((s2, w) => s2 + medir(w.t, 100, 700, false, P), 0) + medir(' ', 100, 700, false, P) * (pr.palavras.length - 1);
|
|
133
|
+
tam = Math.min(360 * u, (W * 0.84 / (1 + 0.022 * (bi + 1))) / Math.max(1, l100) * 100);
|
|
134
|
+
}
|
|
135
|
+
const larg2 = j === 'cartao' && deitado ? W * 0.34 : W * 0.84;
|
|
136
|
+
const tb = poe(bloco(P, pr, { tam, peso: j === 'vazado' ? 700 : 400, max: larg2, cx: tx2, cy: tY, cor: sobreFoto ? branco : T, corAcento: sobreFoto ? branco : null }));
|
|
137
|
+
if (sb) poe(bloco(P, sb, { tam: 34 * u, max: larg2, cx: tx2, cy: tY + tb.h / 2 + 56 * u, cor: sobreFoto ? alfa(branco, 0.82) : S }));
|
|
138
|
+
out.imagem = { jeito: j, src: b.imagem.src, foto, t0, texto: tb };
|
|
139
|
+
// O portador: no círculo ele É a foto (cresce); nos outros, vira um ponto e some.
|
|
140
|
+
if (j === 'circulo') vai(Math.max(0, t0 - 0.5), t0 + 0.2, forma(cx, cy, 0, 0, { solid: 1, ac: 1, op: 0 }));
|
|
141
|
+
else vai(ent[0], ent[1], forma(tx2, tY - tb.h / 2 - 60 * u, 10 * u, 10 * u, { solid: 1, ac: 1, op: j === 'vazado' ? 1 : 0 }));
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/* A PALAVRA GIGANTE (fluido.js marca): a frase curta enche a largura,
|
|
145
|
+
sem pílula, peso fino. Na VIRADA o portador cresce até cobrir a tela
|
|
146
|
+
de acento, e o texto troca para a cor que lê sobre ele. */
|
|
147
|
+
if (b.gigante && pr) {
|
|
148
|
+
const base = 100;
|
|
149
|
+
const larguraA100 = pr.palavras.reduce((s2, w) => s2 + medir(w.t, w.acento ? base * 1.14 : base, 300, w.acento, P), 0) + medir(' ', base, 300, false, P) * (pr.palavras.length - 1);
|
|
150
|
+
// Cabe já descontando o zoom que a câmera terá nesta batida (1 + 0,022 por batida) e a deriva.
|
|
151
|
+
const zoomAqui = 1 + 0.022 * (bi + 1);
|
|
152
|
+
const tam = Math.min(330 * u, Math.max(96 * u, (W * 0.8 / zoomAqui) / Math.max(1, larguraA100) * base));
|
|
153
|
+
const sobre = b.virada ? (lum(pal.acentos[0]) > 0.55 ? pal.fundo[0] : '#ffffff') : T;
|
|
154
|
+
const acentoSobre = b.virada ? sobre : pal.acentos[0];
|
|
155
|
+
const g = poe(bloco(P, pr, { tam, peso: 300, max: W * 0.84 / zoomAqui, cx, cy, cor: sobre, corAcento: acentoSobre }));
|
|
156
|
+
const [sb] = fr('sub');
|
|
157
|
+
if (sb) poe(bloco(P, sb, { tam: 34 * u, max: W * 0.7, cx, cy: cy + g.h / 2 + 70 * u, cor: b.virada ? sobre : S }));
|
|
158
|
+
// A virada começa com a PRIMEIRA frase que aparece (a voz pode dizer o sub antes do título).
|
|
159
|
+
const t0 = Math.max(b.de + 0.1, Math.min(...b.frases.map((f) => f.palavras[0].em)));
|
|
160
|
+
if (b.virada) {
|
|
161
|
+
const D = Math.hypot(W, H) * 1.08;
|
|
162
|
+
vai(Math.max(0, t0 - 0.55), t0 - 0.05, forma(cx, cy, D, D, { solid: 1, ac: 1 }));
|
|
163
|
+
} else vai(ent[0], ent[1], forma(cx, cy - g.h / 2 - 70 * u, 14 * u, 14 * u, { solid: 1, ac: 1 }));
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
111
166
|
switch (b.tipo) {
|
|
112
167
|
case 'abertura': {
|
|
113
168
|
const t = pr && poe(bloco(P, pr, { tam: (lado === 'centro' ? 96 : 80) * u, max: larg, cx: tx, cy: ty, cor: T }));
|
|
@@ -146,7 +201,9 @@ function leiaute(P) {
|
|
|
146
201
|
out.extra = { pilulas, tt };
|
|
147
202
|
// O portador vira a PRIMEIRA pílula; as outras nascem na ponta da curva.
|
|
148
203
|
const p0 = pilulas[0];
|
|
149
|
-
|
|
204
|
+
// A primeira pílula nasce com o primeiro item (antes, ficava vazia e enorme esperando a voz).
|
|
205
|
+
const i0 = p0 ? p0.f.palavras[0].em : b.de;
|
|
206
|
+
if (p0) vai(Math.max(ent[0], i0 - 0.4), Math.max(ent[1], i0 + 0.1), forma(p0.x, p0.y, p0.w, p0.h, { fill: 1, stroke: 0.35 }));
|
|
150
207
|
else vai(ent[0], ent[1], forma(cx, cy, 12 * u, 12 * u, { solid: 1, ac: 1 }));
|
|
151
208
|
break;
|
|
152
209
|
}
|
|
@@ -220,8 +277,12 @@ function leiaute(P) {
|
|
|
220
277
|
const h = umaLinha ? (t ? t.tam * 2 : 60 * u) : t.h;
|
|
221
278
|
if (sb) poe(umaLinha ? bloco(P, sb, { tam: 34 * u, max: larg, cx: tx, cy: ty + h / 2 + 62 * u, cor: S })
|
|
222
279
|
: bloco(P, sb, { tam: 34 * u, max: larg, cx: t.left, cy: ty + h / 2 + 50 * u, alinha: 'esq', cor: S }));
|
|
223
|
-
|
|
224
|
-
|
|
280
|
+
/* A pílula nasce com a primeira palavra, não antes: entrando em b.de,
|
|
281
|
+
ela ficava VAZIA até a voz chegar na frase (teste de 08/10). */
|
|
282
|
+
const p0 = pr ? pr.palavras[0].em : b.de;
|
|
283
|
+
const entT = [Math.max(ent[0], p0 - 0.4), Math.max(ent[1], p0 + 0.1)];
|
|
284
|
+
if (umaLinha) vai(entT[0], entT[1], forma(tx, ty, (t ? t.w : 200 * u) + 110 * u, h, { fill: 1, stroke: 0.35 }));
|
|
285
|
+
else vai(entT[0], entT[1], forma(t.left - 40 * u, ty, 6 * u, t.h * 0.92, { solid: 1, ac: 1, r: 3 }));
|
|
225
286
|
}
|
|
226
287
|
}
|
|
227
288
|
return out;
|
|
@@ -261,7 +322,7 @@ function Palavras({ bl, sai, t, P, op = 1, de = null }) {
|
|
|
261
322
|
filter: (1 - k) + o > 0.02 ? 'blur(' + ((1 - k) * 8 + o * 6).toFixed(2) + 'px)' : undefined,
|
|
262
323
|
fontFamily: w.acento ? SERIF(P.fontes.serifa) : bl.mono ? MONO(P.fontes.mono) : SANS(P.fontes.texto), fontStyle: w.acento ? 'italic' : 'normal',
|
|
263
324
|
fontWeight: w.acento ? 400 : bl.peso, fontSize: w.acento ? bl.tam * 1.14 : bl.tam, letterSpacing: w.acento || bl.mono ? 0 : '-0.02em',
|
|
264
|
-
color: w.acento ? pal.acentos[0] : bl.cor,
|
|
325
|
+
color: w.acento ? (bl.corAcento || pal.acentos[0]) : bl.cor,
|
|
265
326
|
}}>{w.t}</span>
|
|
266
327
|
);
|
|
267
328
|
})}
|
|
@@ -340,6 +401,55 @@ function Cartao({ c, ui, de: de0, sai, t, P, u: u0 }) {
|
|
|
340
401
|
);
|
|
341
402
|
}
|
|
342
403
|
|
|
404
|
+
// ── a foto: cinco jeitos de entrar, nenhum repetido no vídeo ────────────
|
|
405
|
+
function Foto({ im, b, sai, t, P, u, W, H }) {
|
|
406
|
+
const k = ed(c01((t - (im.t0 - 0.45)) / 0.75)); // a entrada
|
|
407
|
+
const o = entra(c01((t - sai) / 0.4)); // a saída
|
|
408
|
+
if (k <= 0.001 || o >= 0.999) return null;
|
|
409
|
+
const vida = c01((t - im.t0) / Math.max(1, b.ate - im.t0)); // o empurrão lento enquanto ela fica
|
|
410
|
+
const src = staticFile(im.src);
|
|
411
|
+
const f = im.foto;
|
|
412
|
+
const img = (extra = {}) => <Img src={src} style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', objectFit: 'cover', ...extra }} />;
|
|
413
|
+
const escurece = <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, rgba(0,0,0,0.05) 30%, rgba(0,0,0,0.62) 100%)' }} />;
|
|
414
|
+
if (im.jeito === 'janela') {
|
|
415
|
+
return <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, overflow: 'hidden', opacity: 1 - o,
|
|
416
|
+
clipPath: 'inset(0 ' + ((1 - k) * 100).toFixed(2) + '% 0 0)' }}>
|
|
417
|
+
{img({ transform: 'scale(' + (1.14 - 0.08 * vida) + ') translateX(' + ((1 - k) * -6) + '%)' })}{escurece}</div>;
|
|
418
|
+
}
|
|
419
|
+
if (im.jeito === 'circulo') {
|
|
420
|
+
const r = Math.hypot(W, H) * 0.55 * k * (1 - o);
|
|
421
|
+
return <div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, overflow: 'hidden',
|
|
422
|
+
clipPath: 'circle(' + r.toFixed(1) + 'px at ' + W / 2 + 'px ' + H / 2 + 'px)' }}>
|
|
423
|
+
{img({ transform: 'scale(' + (1.2 - 0.12 * k - 0.05 * vida) + ')' })}{escurece}</div>;
|
|
424
|
+
}
|
|
425
|
+
if (im.jeito === 'cartao') {
|
|
426
|
+
const gira = (1 - k) * -38 + Math.sin(t * 0.6) * 3;
|
|
427
|
+
return <div style={{ position: 'absolute', left: f.x - f.w / 2, top: f.y - f.h / 2, width: f.w, height: f.h, borderRadius: 28 * u, overflow: 'hidden',
|
|
428
|
+
opacity: k * (1 - o), boxShadow: '0 ' + 50 * u + 'px ' + 90 * u + 'px -' + 30 * u + 'px rgba(0,0,0,0.55)',
|
|
429
|
+
transform: 'perspective(' + 1500 * u + 'px) translateY(' + ((1 - k) * 140 * u - o * 60 * u) + 'px) rotateY(' + gira + 'deg) rotateX(' + (6 * (1 - k)) + 'deg) scale(' + (0.86 + 0.14 * k) + ')' }}>
|
|
430
|
+
{img({ transform: 'scale(' + (1.08 - 0.06 * vida) + ')' })}</div>;
|
|
431
|
+
}
|
|
432
|
+
if (im.jeito === 'faixa') {
|
|
433
|
+
const meio = (1 - k + o) * 50;
|
|
434
|
+
return <div style={{ position: 'absolute', left: 0, top: f.y - f.h / 2, width: W, height: f.h, overflow: 'hidden',
|
|
435
|
+
clipPath: 'inset(' + meio.toFixed(2) + '% 0 ' + meio.toFixed(2) + '% 0)' }}>
|
|
436
|
+
{img({ transform: 'scale(1.12) translateX(' + (-4 + 8 * vida) + '%)' })}</div>;
|
|
437
|
+
}
|
|
438
|
+
// vazado: as letras do título, com a foto por dentro (SVG: o texto é o recorte).
|
|
439
|
+
const tb = im.texto;
|
|
440
|
+
const id = 'vz' + b.i;
|
|
441
|
+
const fam = '"' + P.fontes.texto + '", system-ui, sans-serif';
|
|
442
|
+
return <svg width={W} height={H} style={{ position: 'absolute', left: 0, top: 0, opacity: (1 - o) }}>
|
|
443
|
+
<defs><clipPath id={id}>{tb.linhas.map((l, li) => (
|
|
444
|
+
<text key={li} x={l.x} y={l.y + tb.lh * 0.8} fontFamily={fam} fontWeight={700} fontSize={tb.tam} letterSpacing={-0.02 * tb.tam}>
|
|
445
|
+
{l.palavras.map((w) => w.t).join(' ')}</text>))}</clipPath></defs>
|
|
446
|
+
<g clipPath={'url(#' + id + ')'} style={{ opacity: k }}>
|
|
447
|
+
<image href={src} x={-W * 0.06 * (1 - vida)} y={tb.top - tb.h * 0.6} width={W * 1.12} height={tb.h * 2.2} preserveAspectRatio="xMidYMid slice"
|
|
448
|
+
transform={'translate(0 ' + ((1 - k) * 40 * u) + ')'} />
|
|
449
|
+
</g>
|
|
450
|
+
</svg>;
|
|
451
|
+
}
|
|
452
|
+
|
|
343
453
|
// ── a peça ───────────────────────────────────────────────────────────────
|
|
344
454
|
export function Fluido({ el, t }) {
|
|
345
455
|
const P = el.partitura;
|
|
@@ -371,12 +481,13 @@ export function Fluido({ el, t }) {
|
|
|
371
481
|
const so = P.logo && ult.tipo === 'cta' ? 1 - ed(c01((t - ult.de - 0.65) / 0.5)) : 1;
|
|
372
482
|
|
|
373
483
|
const camadas = [];
|
|
374
|
-
L.batidas.forEach(({ b, blocos, extra, risca, cartao, comando }, i) => {
|
|
484
|
+
L.batidas.forEach(({ b, blocos, extra, risca, cartao, comando, imagem }, i) => {
|
|
375
485
|
const corte = b.transicao === 'corte' || (L.batidas[i + 1] && L.batidas[i + 1].b.transicao === 'corte');
|
|
376
486
|
const ultima = i === L.batidas.length - 1;
|
|
377
487
|
const sai = ultima ? 1e9 : b.ate - (corte ? 0 : 0.15), fimVis = b.ate + (corte ? 0 : 0.4);
|
|
378
488
|
if (t < b.de - 0.3 || t > fimVis || (corte && t >= b.ate && !ultima)) return;
|
|
379
489
|
if (cartao) camadas.push(<Cartao key={'k' + i} c={cartao} ui={b.ui} de={b.de} sai={sai} t={t} P={P} u={u} />);
|
|
490
|
+
if (imagem) camadas.push(<Foto key={'f' + i} im={imagem} b={b} sai={sai} t={t} P={P} u={u} W={W} H={H} />);
|
|
380
491
|
// A lista: as curvas que se desenham de uma pílula à próxima, e as pílulas que nascem na ponta.
|
|
381
492
|
if (b.tipo === 'lista' && extra) {
|
|
382
493
|
const ps = extra.pilulas;
|
|
@@ -398,17 +509,27 @@ export function Fluido({ el, t }) {
|
|
|
398
509
|
background: sup, border: (1.2 * u) + 'px solid ' + alfa(pal.tinta, 0.2), opacity: kp * o, transform: 'scale(' + (0.7 + 0.3 * kp) + ')',
|
|
399
510
|
boxShadow: pal.escuro ? undefined : '0 14px 34px -14px ' + alfa(pal.tinta, 0.3) }} />);
|
|
400
511
|
}
|
|
401
|
-
|
|
512
|
+
/* O marcador de cada pílula muda de vídeo para vídeo (P.marcador): o
|
|
513
|
+
brilho de quatro pontas em toda lista era vício (08/10). */
|
|
402
514
|
const kd = ed(c01((t - p.f.de) / 0.5));
|
|
403
|
-
|
|
515
|
+
const mk = P.marcador || 'brilho';
|
|
516
|
+
const mx0 = p.x - p.w / 2 + 34 * u, my0 = p.y - p.ic / 2;
|
|
517
|
+
if (mk === 'brilho') camadas.push(<svg key={'d' + i + j} width={p.ic} height={p.ic} viewBox="-1 -1 2 2" style={{ position: 'absolute', left: mx0, top: my0,
|
|
404
518
|
opacity: kd * o, transform: 'rotate(' + ((1 - kd) * -90 + Math.sin(t * 1.3 + j) * 6) + 'deg) scale(' + kd + ')' }}>
|
|
405
519
|
<path d={BRILHO4} fill={pal.acentos[0]} />
|
|
406
520
|
</svg>);
|
|
521
|
+
else if (mk === 'numero') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0 - 4 * u, top: p.y - p.ic * 0.62, height: p.ic * 1.24, lineHeight: p.ic * 1.24 + 'px',
|
|
522
|
+
fontFamily: MONO(P.fontes.mono), fontSize: p.ic * 0.78, color: pal.acentos[0], opacity: kd * o, transform: 'translateY(' + (1 - kd) * 10 * u + 'px)' }}>{String(j + 1).padStart(2, '0')}</div>);
|
|
523
|
+
else if (mk === 'traco') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0, top: p.y - 1.5 * u, width: p.ic * kd, height: 3 * u, borderRadius: 3,
|
|
524
|
+
background: pal.acentos[0], opacity: o }} />);
|
|
525
|
+
else if (mk === 'ponto') camadas.push(<div key={'d' + i + j} style={{ position: 'absolute', left: mx0 + p.ic * 0.3, top: p.y - p.ic * 0.2, width: p.ic * 0.4, height: p.ic * 0.4, borderRadius: 99,
|
|
526
|
+
background: pal.acentos[0], opacity: o, transform: 'scale(' + kd + ')' }} />);
|
|
407
527
|
});
|
|
408
528
|
}
|
|
409
529
|
for (const bl of blocos) {
|
|
410
530
|
const f = bl.f;
|
|
411
531
|
if (b.tipo === 'passos' && extra) continue;
|
|
532
|
+
if (imagem && imagem.jeito === 'vazado' && bl === imagem.texto) continue; // as letras são a foto (Foto desenha)
|
|
412
533
|
const op = f.papel === 'nao' && risca ? 1 - 0.55 * ed(c01((t - risca) / 0.4)) : 1;
|
|
413
534
|
if (f.papel === 'numero' && b.numero) {
|
|
414
535
|
const k = ed(c01((t - f.de) / 1.1));
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
21
|
-
let ATUAL = { modo: null, escopo: '' };
|
|
21
|
+
let ATUAL = { modo: null, escopo: '', explicito: false };
|
|
22
22
|
|
|
23
|
-
function definir(modo, escopo = '') {
|
|
24
|
-
ATUAL = { modo: ['aberto', 'fechado', 'sem'].includes(modo) ? modo : null, escopo: String(escopo || '').trim() };
|
|
23
|
+
function definir(modo, escopo = '', explicito = true) {
|
|
24
|
+
ATUAL = { modo: ['aberto', 'fechado', 'sem'].includes(modo) ? modo : null, escopo: String(escopo || '').trim(), explicito: !!explicito };
|
|
25
25
|
return ATUAL;
|
|
26
26
|
}
|
|
27
27
|
function atual() { return ATUAL; }
|
|
28
|
-
function limpar() { ATUAL = { modo: null, escopo: '' }; }
|
|
28
|
+
function limpar() { ATUAL = { modo: null, escopo: '', explicito: false }; }
|
|
29
|
+
|
|
30
|
+
/** Sem foto nem vídeo de fora: o "sem" e também ninguém ter dito nada (o padrão, 07/10). */
|
|
31
|
+
function semImagem({ modo } = ATUAL) { return modo !== 'aberto' && modo !== 'fechado'; }
|
|
29
32
|
|
|
30
33
|
/** Os termos do escopo fechado: "site:anthropic.com, claude ai, sonnet" → lista. */
|
|
31
34
|
function termos(escopo = ATUAL.escopo) {
|
|
@@ -41,33 +44,40 @@ function entender(resposta) {
|
|
|
41
44
|
return null;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
/* O MODO DEDUZIDO DO PEDIDO.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
/* O MODO DEDUZIDO DO PEDIDO. O padrão é SEM IMAGEM: "por padrão, NÃO
|
|
48
|
+
usar imagens. Somente quando o usuário pedir explicitamente" (Joel, 07/10).
|
|
49
|
+
O menu 1/2/3 do terminal saiu; quem quer imagem diz no pedido:
|
|
50
|
+
"adicione imagens de apoio", "quero imagens durante o vídeo",
|
|
51
|
+
"use imagens reais", "web aberta" → aberto
|
|
52
|
+
"só do site x.com", "usa apenas anthropic.com e …" → fechado (o escopo)
|
|
53
|
+
"só motion", "sem referência", "sem foto" → sem (explícito)
|
|
54
|
+
o resto → sem (o padrão) */
|
|
55
|
+
const PEDE_IMAGEM = /(adicion|coloc|use|usa|usar|quero|bota|pon|insir|inclu|com)\w*\s+(\w+\s+){0,3}(imagens?|fotos?|b-?roll|videos? de (apoio|banco)|cenas reais)\b|(imagens?|fotos?) (de apoio|reais|durante|para ilustrar|pra ilustrar|da (web|internet))|escopo aberto|web aberta|pesquis\w* (a vontade|na web|na internet)|busc\w* (imagens|fotos|videos) na (web|internet)/;
|
|
56
|
+
const NEGA_IMAGEM = /so (o )?motion|motion puro|sem refer|sem (foto|imagem|imagens|video de banco|pesquisa)|nao (pesquis|busqu)|so anima|(nao|nada de) (quero |use |usar |coloc\w* )?(imagens?|fotos?)/;
|
|
57
|
+
|
|
52
58
|
function inferir(texto) {
|
|
53
|
-
const t = String(texto || '').toLowerCase().normalize('NFD').replace(/[
|
|
54
|
-
if (
|
|
59
|
+
const t = String(texto || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
60
|
+
if (NEGA_IMAGEM.test(t)) return { modo: 'sem', escopo: '', explicito: true };
|
|
55
61
|
const dominios = String(texto || '').match(/\b[a-z0-9-]+(\.[a-z0-9-]+)+\.?[a-z]{2,}(\/[^\s,]*)?/gi) || [];
|
|
56
62
|
if (/\b(so|apenas|somente|unicamente)\b[^.]{0,30}\b(site|sites|link|links|fonte|fontes|pagina|de|do|da|em)\b/.test(t) && dominios.length) {
|
|
57
63
|
return { modo: 'fechado', escopo: dominios.join(', '), explicito: true };
|
|
58
64
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
return { modo: 'aberto', escopo: '', explicito: false };
|
|
65
|
+
if (PEDE_IMAGEM.test(t)) return { modo: 'aberto', escopo: '', explicito: true };
|
|
66
|
+
return { modo: 'sem', escopo: '', explicito: false };
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/** A instrução para o agente, conforme o modo. */
|
|
66
|
-
function instrucao({ modo, escopo } = ATUAL) {
|
|
67
|
-
|
|
70
|
+
function instrucao({ modo, escopo, explicito } = ATUAL) {
|
|
71
|
+
// O vídeo sem imagem é motion: o agente escolhe o estilo de motion que serve ao roteiro, não um fixo.
|
|
72
|
+
const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som. Escolha em roteiro.estilo o motion que serve a este '
|
|
73
|
+
+ 'roteiro (gerativo, fluido, impacto, vox, marca, lancamento) ou deixe vazio para o rodízio — não repita sempre o mesmo.';
|
|
74
|
+
if (modo === 'sem' && explicito) {
|
|
68
75
|
return '[REFERÊNCIAS: NENHUMA] A pessoa quer SÓ MOTION: não pesquise (nada de pesquisar_referencia nem estudio_site), '
|
|
69
|
-
+ 'não busque foto nem vídeo, não use estudio_midia de banco. Escreva o roteiro com o que o pedido já diz
|
|
70
|
-
|
|
76
|
+
+ 'não busque foto nem vídeo, não use estudio_midia de banco. Escreva o roteiro com o que o pedido já diz. ' + motion;
|
|
77
|
+
}
|
|
78
|
+
if (modo === 'sem') {
|
|
79
|
+
return '[SEM IMAGEM — o padrão] A pessoa não pediu imagem: não busque foto nem vídeo, não use estudio_midia de banco '
|
|
80
|
+
+ 'nem "busca" nas partes. Pesquisar FATOS em texto (pesquisar_referencia) pode, se o assunto pedir. ' + motion;
|
|
71
81
|
}
|
|
72
82
|
if (modo === 'fechado') {
|
|
73
83
|
const t = termos(escopo);
|
|
@@ -77,8 +87,9 @@ function instrucao({ modo, escopo } = ATUAL) {
|
|
|
77
87
|
+ `No roteiro, ponha roteiro.busca = ${JSON.stringify(t)} para o b-roll.`;
|
|
78
88
|
}
|
|
79
89
|
if (modo === 'aberto') {
|
|
80
|
-
return '[REFERÊNCIAS: ESCOPO ABERTO] Pesquise o assunto à vontade (pesquisar_referencia; estudio_site se for de uma marca) '
|
|
81
|
-
+ '
|
|
90
|
+
return '[REFERÊNCIAS: ESCOPO ABERTO — a pessoa pediu imagens] Pesquise o assunto à vontade (pesquisar_referencia; estudio_site se for de uma marca). '
|
|
91
|
+
+ 'A imagem é um elemento da narrativa, não o vídeo inteiro: dê "busca" (em inglês: gente, lugar, tela, objeto de verdade) '
|
|
92
|
+
+ 'SÓ nas partes em que uma imagem real mostra o que o texto não mostra. Não troque de imagem a cada frase; o resto é motion.';
|
|
82
93
|
}
|
|
83
94
|
return '';
|
|
84
95
|
}
|
|
@@ -102,4 +113,4 @@ function filtrar(r, { modo, escopo } = ATUAL) {
|
|
|
102
113
|
return { ...r, fontes: refs.length, referencias: refs, ...(fora ? { foraDoEscopo: fora } : {}) };
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
module.exports = { definir, atual, limpar, termos, entender, inferir, instrucao, dominios, filtrar };
|
|
116
|
+
module.exports = { definir, atual, limpar, semImagem, termos, entender, inferir, instrucao, dominios, filtrar };
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -719,6 +719,8 @@ export const Video = () => {
|
|
|
719
719
|
return vs[vs.length - 1].v; }
|
|
720
720
|
: (plano.audio.trilha.volume ?? 0.18)}
|
|
721
721
|
loop={plano.audio.trilha.loop !== false}
|
|
722
|
+
/* O início da faixa que casa as batidas com os cortes (batida.js). */
|
|
723
|
+
startFrom={Math.round((plano.audio.trilha.inicio || 0) * fps)}
|
|
722
724
|
/>
|
|
723
725
|
)}
|
|
724
726
|
{/* Os TRECHOS: pedaços de áudio no tempo certo — a narração partida entre
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* revisor.js — a revisão do plano antes do render: o olho de editor que
|
|
3
|
+
* procura receita, excesso e pressa, e conserta o que dá sem perguntar.
|
|
4
|
+
*
|
|
5
|
+
* "Antes de finalizar, procure padrões repetitivos, escolhas previsíveis,
|
|
6
|
+
* templates rígidos, excesso de efeitos, excesso de imagens, cortes
|
|
7
|
+
* artificiais, problemas de ritmo." (Joel, 07/10)
|
|
8
|
+
*
|
|
9
|
+
* plano.js confere se o plano RODA; este confere se ele está BOM. O que tem
|
|
10
|
+
* conserto seguro é consertado aqui (e dito); o que pede decisão vira aviso,
|
|
11
|
+
* que vai para o agente e para as notas do projeto:
|
|
12
|
+
* conserta a mesma transição 3+ vezes seguidas → a do meio vira corte seco
|
|
13
|
+
* conserta o MESMO efeito sonoro colado nele (< 0,3 s) → fica um (camadas de sons diferentes são desenho, ficam)
|
|
14
|
+
* conserta câmera mexendo em 4 cenas seguidas → a mais fraca para (respiro)
|
|
15
|
+
* avisa texto que não dá tempo de ler (0,3 s/palavra, mínimo 0,8 s)
|
|
16
|
+
* avisa a mesma sequência de elementos em 4+ cenas seguidas (cara de template)
|
|
17
|
+
* avisa imagem sem a pessoa pedir, ou imagem em mais da metade das cenas
|
|
18
|
+
* conserta efeito sonoro demais (mais de 1 a cada 1,5 s) → ficam os mais altos
|
|
19
|
+
* Puro: o plano entra, o plano consertado e a lista saem.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
25
|
+
|
|
26
|
+
const tipoDaTransicao = (c) => (c && c.transicao ? (c.transicao.tipo || c.transicao) : 'corte');
|
|
27
|
+
const mexe = (cam) => {
|
|
28
|
+
if (!cam) return false;
|
|
29
|
+
return Object.values(cam).some((ks) => Array.isArray(ks) && ks.length > 1 && ks.some((k) => Math.abs(Number(k.v) - Number(ks[0].v)) > 0.005));
|
|
30
|
+
};
|
|
31
|
+
const forcaDaCamera = (cam) => Math.max(0, ...Object.values(cam || {}).filter(Array.isArray)
|
|
32
|
+
.map((ks) => Math.max(...ks.map((k) => Number(k.v) || 0)) - Math.min(...ks.map((k) => Number(k.v) || 0))));
|
|
33
|
+
const palavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
34
|
+
// Imagem ou vídeo DE FORA: o vídeo da própria gravação (deOrigem) e o recorte da pessoa não contam.
|
|
35
|
+
const imagemDeFora = (el) => el && (el.tipo === 'imagem' || (el.tipo === 'video' && el.deOrigem == null && !/pessoa-/.test(el.src || '')));
|
|
36
|
+
|
|
37
|
+
function revisar(plano, { semImagem = false } = {}) {
|
|
38
|
+
const consertos = [], avisos = [];
|
|
39
|
+
const cenas = (plano && plano.cenas) || [];
|
|
40
|
+
if (!cenas.length) return { plano, consertos, avisos };
|
|
41
|
+
|
|
42
|
+
// 1. A mesma transição (que não seja corte seco) 3+ vezes seguidas: receita.
|
|
43
|
+
for (let i = 2; i < cenas.length; i++) {
|
|
44
|
+
const t = tipoDaTransicao(cenas[i]);
|
|
45
|
+
if (t !== 'corte' && t === tipoDaTransicao(cenas[i - 1]) && t === tipoDaTransicao(cenas[i - 2])) {
|
|
46
|
+
cenas[i - 1].transicao = { tipo: 'corte' };
|
|
47
|
+
consertos.push(`cena ${i}: "${t}" 3 vezes seguidas — a do meio virou corte seco`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Câmera mexendo em 4 cenas seguidas: a de movimento mais fraco para.
|
|
52
|
+
for (let i = 3; i < cenas.length; i++) {
|
|
53
|
+
const janela = [i - 3, i - 2, i - 1, i];
|
|
54
|
+
if (janela.every((k) => mexe(cenas[k].camera))) {
|
|
55
|
+
const k = janela.filter((j) => j > 0 && !cenas[j].impacto) // a abertura (o gancho) e o golpe não param
|
|
56
|
+
|
|
57
|
+
// Empate: a mais à frente — parar a primeira da janela fazia a janela seguinte pedir outra, e paravam todas (08/10).
|
|
58
|
+
.sort((a, b) => forcaDaCamera(cenas[a].camera) - forcaDaCamera(cenas[b].camera) || b - a)[0];
|
|
59
|
+
if (k == null) continue;
|
|
60
|
+
const cam = cenas[k].camera;
|
|
61
|
+
for (const prop of Object.keys(cam)) if (Array.isArray(cam[prop]) && cam[prop].length) cam[prop] = [{ t: 0, v: cam[prop][0].v }, { t: 1, v: cam[prop][0].v }];
|
|
62
|
+
consertos.push(`cena ${k + 1}: 4 cenas seguidas com a câmera mexendo — esta respira`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Efeitos sonoros: colados um no outro, fica o mais alto; e a densidade.
|
|
67
|
+
const efeitos = ((plano.audio && plano.audio.efeitos) || []).slice().sort((a, b) => (a.t || 0) - (b.t || 0));
|
|
68
|
+
if (efeitos.length) {
|
|
69
|
+
const fica = [];
|
|
70
|
+
for (const e of efeitos) {
|
|
71
|
+
const ult = fica[fica.length - 1];
|
|
72
|
+
if (ult && e.src === ult.src && (e.t || 0) - (ult.t || 0) < 0.3) {
|
|
73
|
+
if ((e.volume ?? 1) > (ult.volume ?? 1)) fica[fica.length - 1] = e;
|
|
74
|
+
consertos.push(`efeito em ${r2(e.t || 0)}s colado no de ${r2(ult.t || 0)}s — ficou um`);
|
|
75
|
+
} else fica.push(e);
|
|
76
|
+
}
|
|
77
|
+
const total = cenas.reduce((n, c) => n + (Number(c.duracao) || 0), 0);
|
|
78
|
+
/* Som demais vira ruído: acima de 1 a cada 1,5 s, ficam os mais altos
|
|
79
|
+
(o desenho de som marca o mais importante com volume), um a cada 0,7 s. */
|
|
80
|
+
let final = fica;
|
|
81
|
+
if (total > 0 && fica.length / total > 1 / 1.5) {
|
|
82
|
+
const teto = Math.max(1, Math.floor(total / 1.5));
|
|
83
|
+
const guardados = [];
|
|
84
|
+
for (const e of fica.slice().sort((a, b) => (b.volume ?? 1) - (a.volume ?? 1))) {
|
|
85
|
+
if (guardados.length >= teto) break;
|
|
86
|
+
if (guardados.every((g) => Math.abs((g.t || 0) - (e.t || 0)) >= 0.7)) guardados.push(e);
|
|
87
|
+
}
|
|
88
|
+
final = guardados.sort((a, b) => (a.t || 0) - (b.t || 0));
|
|
89
|
+
consertos.push(`${fica.length} efeitos sonoros em ${r2(total)}s eram ruído — ficaram os ${final.length} mais fortes`);
|
|
90
|
+
}
|
|
91
|
+
plano.audio.efeitos = final;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. Texto que não dá tempo de ler.
|
|
95
|
+
cenas.forEach((c, i) => {
|
|
96
|
+
const n = (c.elementos || []).filter((e) => e && e.tipo === 'texto').reduce((s, e) => s + palavras(e.texto), 0);
|
|
97
|
+
const precisa = Math.max(0.8, 0.3 * n);
|
|
98
|
+
if (n && Number(c.duracao) < precisa) avisos.push(`cena ${i + 1}: ${n} palavras em ${c.duracao}s (precisa de ${r2(precisa)}s para ler)`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 5. A mesma sequência de elementos em 4+ cenas seguidas: cara de template.
|
|
102
|
+
const assinatura = (c) => (c.elementos || []).map((e) => e && (e.tipo + (e.peca ? ':' + e.peca : ''))).join(',');
|
|
103
|
+
for (let i = 0, j = 1; j <= cenas.length; j++) {
|
|
104
|
+
if (j < cenas.length && assinatura(cenas[j]) === assinatura(cenas[i])) continue;
|
|
105
|
+
if (j - i >= 4 && assinatura(cenas[i])) avisos.push(`cenas ${i + 1} a ${j}: a mesma montagem (${assinatura(cenas[i])}) ${j - i} vezes seguidas — varie a composição`);
|
|
106
|
+
i = j;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 6. Imagem: sem pedido, nenhuma; com pedido, não em toda cena.
|
|
110
|
+
const comImagem = cenas.filter((c) => (c.elementos || []).some(imagemDeFora) || (c.fundo && (c.fundo.tipo === 'imagem' || c.fundo.tipo === 'video'))).length;
|
|
111
|
+
if (semImagem && comImagem) avisos.push(`${comImagem} cena(s) com imagem de fora, mas a pessoa não pediu imagem`);
|
|
112
|
+
else if (cenas.length >= 4 && comImagem / cenas.length > 0.5) avisos.push(`imagem em ${comImagem} de ${cenas.length} cenas — a imagem é um momento, não o vídeo inteiro`);
|
|
113
|
+
|
|
114
|
+
return { plano, consertos, avisos };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { revisar };
|
package/lib/estudio/variedade.js
CHANGED
|
@@ -21,23 +21,75 @@ const os = require('os');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
|
|
23
23
|
const PASTA = path.join(os.homedir(), '.primocode', 'studio');
|
|
24
|
-
const MEMORIA = { trilha: 16, sfx: 40, broll: 80, icones: 4, estilo: 8, '3d': 24 };
|
|
24
|
+
const MEMORIA = { trilha: 16, sfx: 40, broll: 80, icones: 4, estilo: 8, '3d': 24, 'icone-nome': 30, 'paleta-fluido': 6 };
|
|
25
|
+
|
|
26
|
+
/* A DIVERSIDADE POR PONTOS (07/10): "o sistema não deve simplesmente proibir
|
|
27
|
+
repetição; escolhas usadas recentemente recebem penalidade, as muito usadas
|
|
28
|
+
recebem penalidade maior, as antigas podem voltar, e o que é adequado ao
|
|
29
|
+
conteúdo continua tendo prioridade" (Joel). Antes era uma janela fixa: o
|
|
30
|
+
que saía da janela voltava como se nunca tivesse sido usado.
|
|
31
|
+
pontos = PESO_ADEQUACAO × adequação − recência − frequência
|
|
32
|
+
- recência: 1 para a última usada, cai pela metade a cada MEIA_VIDA_USOS
|
|
33
|
+
escolhas do mesmo tipo (exp), e some quando sai da lista;
|
|
34
|
+
- frequência: log2(1 + usos), com os usos perdendo metade a cada
|
|
35
|
+
MEIA_VIDA_DIAS — o muito usado pesa mais, e o tempo perdoa;
|
|
36
|
+
- adequação: 0 a 1, de quem chama (o clima, o assunto). Com peso 2, o que
|
|
37
|
+
serve ao roteiro ganha do que só é novo: a variedade não quebra a coerência. */
|
|
38
|
+
const PESO_ADEQUACAO = 2;
|
|
39
|
+
const PESO_FREQUENCIA = 0.25;
|
|
40
|
+
const MEIA_VIDA_DIAS = 21;
|
|
41
|
+
const EMPATE = 0.05; // dentro disso, é sorteio
|
|
25
42
|
|
|
26
43
|
function arquivo(tipo) { return path.join(PASTA, `${tipo}-usadas.json`); }
|
|
44
|
+
function arquivoHistorico(tipo) { return path.join(PASTA, `${tipo}-historico.json`); }
|
|
27
45
|
|
|
28
46
|
function usadas(tipo) {
|
|
29
47
|
try { const l = JSON.parse(fs.readFileSync(arquivo(tipo), 'utf8')); return Array.isArray(l) ? l : []; } catch { return []; }
|
|
30
48
|
}
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
/** { id: { n: usos, t: ms da última } } — a frequência, que a lista de recentes não guarda. */
|
|
51
|
+
function historico(tipo) {
|
|
52
|
+
try { const h = JSON.parse(fs.readFileSync(arquivoHistorico(tipo), 'utf8')); return h && typeof h === 'object' && !Array.isArray(h) ? h : {}; } catch { return {}; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const usosHoje = (reg, agora) => (reg ? reg.n * Math.pow(0.5, Math.max(0, agora - reg.t) / (MEIA_VIDA_DIAS * 864e5)) : 0);
|
|
56
|
+
|
|
57
|
+
function marcar(tipo, id, agora = Date.now()) {
|
|
33
58
|
if (!id) return;
|
|
34
59
|
try {
|
|
35
60
|
fs.mkdirSync(PASTA, { recursive: true });
|
|
36
|
-
|
|
61
|
+
// A lista de recentes é maior que a janela de antes: a recência decai, não corta.
|
|
62
|
+
const l = [id, ...usadas(tipo).filter((x) => x !== id)].slice(0, (MEMORIA[tipo] || 20) * 2);
|
|
37
63
|
fs.writeFileSync(arquivo(tipo), JSON.stringify(l));
|
|
64
|
+
const h = historico(tipo);
|
|
65
|
+
h[id] = { n: usosHoje(h[id], agora) + 1, t: agora };
|
|
66
|
+
// Guarda os 400 mais recentes, para o arquivo não crescer sem fim.
|
|
67
|
+
const ids = Object.keys(h).sort((x, y) => h[y].t - h[x].t).slice(0, 400);
|
|
68
|
+
fs.writeFileSync(arquivoHistorico(tipo), JSON.stringify(Object.fromEntries(ids.map((k) => [k, h[k]]))));
|
|
38
69
|
} catch { /* sem memória: só perde a garantia de não repetir */ }
|
|
39
70
|
}
|
|
40
71
|
|
|
72
|
+
/** Quanto pesa ter usado `id`: recência + frequência. 0 = nunca usado. */
|
|
73
|
+
function penalidade(id, tipo, { lista = usadas(tipo), hist = historico(tipo), agora = Date.now() } = {}) {
|
|
74
|
+
const pos = lista.indexOf(id);
|
|
75
|
+
const meiaVida = Math.max(1, (MEMORIA[tipo] || 20) / 4);
|
|
76
|
+
const recencia = pos < 0 ? 0 : Math.pow(0.5, pos / meiaVida);
|
|
77
|
+
return recencia + PESO_FREQUENCIA * Math.log2(1 + usosHoje(hist[id], agora));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Os pontos de cada opção: adequação menos penalidade. `adequacao` é uma
|
|
82
|
+
* função (opção → 0..1) ou um objeto {id: 0..1}; sem ela, tudo serve igual (1).
|
|
83
|
+
*/
|
|
84
|
+
function pontuar(opcoes, tipo, { prefixo = '', adequacao = null, chave = (o) => o, agora = Date.now() } = {}) {
|
|
85
|
+
const lista = usadas(tipo), hist = historico(tipo);
|
|
86
|
+
const fit = (o) => {
|
|
87
|
+
const v = typeof adequacao === 'function' ? adequacao(o) : adequacao && typeof adequacao === 'object' ? adequacao[chave(o)] : 1;
|
|
88
|
+
return Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0;
|
|
89
|
+
};
|
|
90
|
+
return opcoes.map((o) => ({ opcao: o, pontos: PESO_ADEQUACAO * fit(o) - penalidade(prefixo + chave(o), tipo, { lista, hist, agora }) }));
|
|
91
|
+
}
|
|
92
|
+
|
|
41
93
|
function embaralhar(lista, aleatorio = Math.random) {
|
|
42
94
|
const a = [...lista];
|
|
43
95
|
for (let i = a.length - 1; i > 0; i--) {
|
|
@@ -47,25 +99,27 @@ function embaralhar(lista, aleatorio = Math.random) {
|
|
|
47
99
|
return a;
|
|
48
100
|
}
|
|
49
101
|
|
|
50
|
-
/** Os itens da busca na ordem de tentar:
|
|
51
|
-
function ordenar(itens, tipo, aleatorio = Math.random) {
|
|
52
|
-
const ja = new Set(usadas(tipo));
|
|
102
|
+
/** Os itens da busca na ordem de tentar: mais pontos primeiro; quase empatados, sorteados. */
|
|
103
|
+
function ordenar(itens, tipo, aleatorio = Math.random, adequacao = null) {
|
|
53
104
|
const validos = (itens || []).filter((x) => x && x.url);
|
|
54
|
-
return
|
|
105
|
+
return pontuar(validos, tipo, { adequacao, chave: (x) => x.url })
|
|
106
|
+
.map((p) => ({ ...p, pontos: p.pontos + aleatorio() * EMPATE }))
|
|
107
|
+
.sort((a, b) => b.pontos - a.pontos)
|
|
108
|
+
.map((p) => p.opcao);
|
|
55
109
|
}
|
|
56
110
|
|
|
57
111
|
/**
|
|
58
|
-
* Uma das opções,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
112
|
+
* Uma das opções, pelos pontos: as nunca usadas primeiro, a recente e a muito
|
|
113
|
+
* usada por último, e a adequada (opcional) acima de todas. Entre as quase
|
|
114
|
+
* empatadas, sorteia. Marca a escolhida na memória do tipo. Com `prefixo`,
|
|
115
|
+
* várias listas dividem o mesmo arquivo ('acabamento:fosco').
|
|
61
116
|
*/
|
|
62
|
-
function sortear(opcoes, tipo, { prefixo = '', aleatorio = Math.random } = {}) {
|
|
117
|
+
function sortear(opcoes, tipo, { prefixo = '', aleatorio = Math.random, adequacao = null } = {}) {
|
|
63
118
|
if (!opcoes || !opcoes.length) return null;
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const escolha = [...novas, ...velhas][0];
|
|
119
|
+
const pts = pontuar(opcoes, tipo, { prefixo, adequacao });
|
|
120
|
+
const melhor = Math.max(...pts.map((p) => p.pontos));
|
|
121
|
+
const quase = pts.filter((p) => p.pontos >= melhor - EMPATE).map((p) => p.opcao);
|
|
122
|
+
const escolha = quase[Math.floor(aleatorio() * quase.length) % quase.length];
|
|
69
123
|
marcar(tipo, prefixo + escolha);
|
|
70
124
|
return escolha;
|
|
71
125
|
}
|
|
@@ -179,5 +233,5 @@ function trocarIcones(plano, troca = {}) {
|
|
|
179
233
|
return n;
|
|
180
234
|
}
|
|
181
235
|
|
|
182
|
-
module.exports = { usadas, marcar, embaralhar, ordenar, sortear, climaDe, termosTrilha, termosSfx, termosBroll, buscarSemRepetir,
|
|
236
|
+
module.exports = { usadas, historico, marcar, penalidade, pontuar, embaralhar, ordenar, sortear, climaDe, termosTrilha, termosSfx, termosBroll, buscarSemRepetir,
|
|
183
237
|
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]}'`);
|