primocode 11.3.3 → 11.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/LICENSES/edit2-terceiros.md +31 -0
- package/LICENSES/models-dev-MIT.txt +25 -0
- package/README.md +2 -2
- package/editor/SKILL.md +24 -0
- package/editor/THIRD_PARTY_NOTICES.md +1 -0
- package/editor/assets/baixar.json +9 -1
- package/editor/helpers/df_limpar.py +29 -0
- package/editor/helpers/edit2.py +155 -0
- package/editor/helpers/enquadrar_camera.py +77 -0
- package/editor/helpers/face_layout.py +20 -0
- package/editor/helpers/face_mediapipe.py +147 -0
- package/editor/helpers/instalar_df.sh +7 -0
- package/editor/helpers/legenda_ass.py +124 -0
- package/editor/helpers/look_ffmpeg.py +105 -0
- package/editor/helpers/motor_palavras.py +157 -0
- package/editor/helpers/primeiro_passe.py +214 -0
- package/editor/helpers/som_edit2.py +119 -0
- package/editor/helpers/transcribe.py +31 -2
- package/editor/pyproject.toml +9 -0
- package/editor/references/edit2.md +22 -0
- package/editor/uv.lock +3342 -214
- package/kits/3d/LICENSES/README.md +11 -0
- package/kits/3d/animacoes.js +87 -0
- package/kits/3d/base.js +44 -0
- package/kits/3d/catalogo.js +38 -0
- package/kits/3d/cenario.js +162 -0
- package/kits/3d/exemplos/asfalto.mjs +91 -0
- package/kits/3d/maquinas.js +97 -0
- package/kits/3d/montador.js +142 -0
- package/kits/3d/package.json +1 -0
- package/kits/3d/personagem.js +86 -0
- package/kits/3d/remotion/Maquete.jsx +42 -0
- package/kits/3d/remotion/Root.jsx +10 -0
- package/kits/3d/render.mjs +73 -0
- package/lib/cli/comando-api.js +30 -8
- package/lib/cli/modelos-lista.js +58 -0
- package/lib/cli/sessao.js +11 -0
- package/lib/cli/subcomandos.js +3 -2
- package/lib/outra-pasta.js +1 -1
- package/lib/permissao-sistema.js +60 -0
- package/lib/permission/perigo.js +2 -1
- package/lib/permission/permissao.js +1 -0
- package/lib/permission/sem-perguntar.js +29 -0
- package/lib/plugin/desktop.js +5 -4
- package/lib/plugin/estudio/cota.js +4 -2
- package/lib/plugin/estudio/dialogo3d.js +71 -0
- package/lib/plugin/estudio/effectcraft.js +1 -1
- package/lib/plugin/estudio/glifos.js +2 -0
- package/lib/plugin/estudio/gsap-local.js +43 -0
- package/lib/plugin/estudio/index.js +63 -10
- package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
- package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
- package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
- package/lib/plugin/estudio/maquina-diretor.js +1 -1
- package/lib/plugin/estudio/maquina-elementos.js +2 -2
- package/lib/plugin/estudio/maquina.js +26 -8
- package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
- package/lib/plugin/estudio/motion-plano/critico.js +59 -0
- package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
- package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
- package/lib/plugin/estudio/motion-plano/index.js +75 -0
- package/lib/plugin/estudio/motion-plano/llm.js +59 -0
- package/lib/plugin/estudio/motion-plano/motor.js +50 -0
- package/lib/plugin/estudio/motion-plano/passos.js +38 -0
- package/lib/plugin/estudio/motion-plano/pele.js +36 -0
- package/lib/plugin/estudio/motion-plano/plano.js +152 -0
- package/lib/plugin/estudio/motion-plano/regua.js +100 -0
- package/lib/plugin/estudio/personagem.js +3 -1
- package/lib/plugin/estudio/referencias.js +1 -1
- package/lib/provider/catalogo.js +161 -0
- package/lib/provider/local/catalogo-embutido.json.gz +0 -0
- package/lib/provider/local/cerebro.json +1 -1
- package/lib/provider/local/entrada.js +10 -0
- package/lib/provider/local/motor.js +140 -27
- package/lib/provider/local/orcamento.js +193 -0
- package/lib/provider/local/provedor.js +127 -14
- package/lib/provider/local/recorte.js +170 -0
- package/lib/provider/local/revezamento.js +121 -0
- package/lib/session/runner/act.js +28 -3
- package/lib/skill/catalogo.js +9 -1
- package/lib/tool/reparar-entrada.js +1 -0
- package/lib/tool/tools.js +58 -1
- package/lib/tui/ui.js +1 -1
- package/package.json +2 -2
- package/vendor/fontes/ttf/ORIGEM.txt +1 -0
- package/vendor/fontes/ttf/inter-800.ttf +0 -0
- package/vendor/gsap/ORIGEM.txt +6 -0
- package/vendor/gsap/gsap.min.js +11 -0
- package/voz/primo_voz/chat.html +206 -5
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* destrava.jsx — as PEÇAS do vídeo do Destrava (Remotion, destrava-essencial-pc/video/src/scenes/Kinetic.tsx) como componentes do motor.
|
|
2
|
+
|
|
3
|
+
O plano de motion (motion-plano) aciona cada peça por cena com `d.dz`:
|
|
4
|
+
frase texto que se monta palavra a palavra na hora da fala; a palavra-chave em DEGRADÊ (P.ac -> P.ac2) letra a letra.
|
|
5
|
+
d.modo: suave (palavra-a-palavra: sobe + desfoca, como o Word.tsx) · escala (nasce grande e assenta) · mascara (a linha varre).
|
|
6
|
+
cor quadro de cor cheia: o círculo de cor cresce do centro (clip circle do S8), com a frase por cima.
|
|
7
|
+
foto cartão de foto arredondado (raio 36, sombra funda): voa em 3D e passeia; com 2+ fotos vira carrossel.
|
|
8
|
+
marca o fecho: a marca REAL (logo do kit ou o nome) em degradê, só na última cena.
|
|
9
|
+
Tudo lê a pele da cena (P: bg, fg, ac, ac2, fontes), nunca cor fixa. Tempos e curvas vêm do style.json / Kinetic.tsx do Destrava:
|
|
10
|
+
palavra: 12 quadros, desfoque 10, sobe 0,28·tam, mola (11, 140, 0,7); cartão: 24 quadros, sobe, gira 12°, escala de 0,92. */
|
|
11
|
+
|
|
12
|
+
function kDCor2(P) { return P.ac2 || P.ac; }
|
|
13
|
+
/* a palavra em degradê: uma camada por letra, cada uma da cor do degradê naquele ponto (o After Effects do kit não pinta texto com rampa) */
|
|
14
|
+
function kDLetras(comp, str, o) {
|
|
15
|
+
var tam = o.tam, fonte = o.fonte, x0 = o.x - o.w / 2, i, cam = [], advAnt = 0, n = str.length;
|
|
16
|
+
var xRef = kMedir(comp, 'x', fonte, tam, o.trk);
|
|
17
|
+
for (i = 0; i < n; i++) {
|
|
18
|
+
var advFim = kMedir(comp, str.substr(0, i + 1) + 'x', fonte, tam, o.trk) - xRef;
|
|
19
|
+
var xc = x0 + (advAnt + advFim) / 2, cor = kMisturar(o.c1, o.c2, n > 1 ? i / (n - 1) : 0);
|
|
20
|
+
var L = comp.layers.addText(str.charAt(i)); L.name = 'dz-letra ' + str.charAt(i);
|
|
21
|
+
if (o.parent) kPai(L, o.parent);
|
|
22
|
+
kDoc(L, fonte, tam, cor, { tracking: o.trk });
|
|
23
|
+
kP(L).setValue([xc, o.y]);
|
|
24
|
+
cam.push({ L: L, x: xc, w: advFim - advAnt });
|
|
25
|
+
advAnt = advFim;
|
|
26
|
+
}
|
|
27
|
+
return cam;
|
|
28
|
+
}
|
|
29
|
+
/* escurece a cor até ler sobre o fundo (contraste ≥ 3,5): o degradê sobre o centro claro do quadro de cor */
|
|
30
|
+
function kDLegivel(cor, fundo) { var k = 0; while (k < 0.9 && kContr(kMisturar(cor, '#000000', k), fundo) < 3.5) k += 0.1; return kMisturar(cor, '#000000', k); }
|
|
31
|
+
function kDEntra(comp, L, modo, ti, x, y, w, tam, idx, txt) {
|
|
32
|
+
kMarcaEntrada(ti);
|
|
33
|
+
L.comment = ti.toFixed(3);
|
|
34
|
+
if (modo == 'mascara') { kAnimaPalavra(comp, L, 'mascara', ti, x, y, w, tam, 0, idx, txt); return; }
|
|
35
|
+
if (modo == 'escala') {
|
|
36
|
+
mola(kS(L), ti, [165, 165], [100, 100], { quique: 0.35, dur: 0.7 });
|
|
37
|
+
curva(kO(L), [[ti, 0, null, 30], [ti + 0.2, 100, 60, null]]);
|
|
38
|
+
kBlur(L, 9, 0, ti, 0.42);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// suave: cinza -> tinta não existe aqui (a cor já é a final); sobe 0,28·tam numa mola mansa e sai do desfoque em ~0,4 s
|
|
42
|
+
mola(kP(L), ti, [x, y + tam * 0.28], [x, y], { quique: 0.2, dur: 0.62 });
|
|
43
|
+
curva(kO(L), [[ti, 0, null, 30], [ti + 0.4, 100, 60, null]]);
|
|
44
|
+
kBlur(L, 10, 0, ti, 0.4);
|
|
45
|
+
}
|
|
46
|
+
/* kDFrase(comp, P, str, o): {cx, cy, tam, maxW, parent, t0, modo, cor, passo, maxLinhas}. Devolve {fim, h, tam, tempos}. */
|
|
47
|
+
function kDFrase(comp, P, str, o) {
|
|
48
|
+
var fonte = P.fonteTitulo, trk = -20, pal = kPalavras(str), i, tam = o.tam, ws, sw, linhas, tent = 0, ts = [], kwMais = 1.18, tamMax = tam;
|
|
49
|
+
if (!pal.length) return { fim: o.t0 || 0, h: 0, tam: tam, tempos: [] };
|
|
50
|
+
var maxW = o.maxW, maxLin = o.maxLinhas || 3;
|
|
51
|
+
for (;;) {
|
|
52
|
+
// a palavra-chave é maior que o resto da frase (o herói do Destrava); frase de uma palavra só fica como está
|
|
53
|
+
ts = []; tamMax = tam; for (i = 0; i < pal.length; i++) { ts.push(pal[i].kw && pal.length > 1 ? Math.min(tam * Math.min(kwMais, Math.max(1, 0.98 * maxW / Math.max(1, kMedir(comp, pal[i].t, fonte, tam, trk)))), 410 * (o.u || 1)) : tam); if (ts[i] > tamMax) tamMax = ts[i]; }
|
|
54
|
+
ws = []; for (i = 0; i < pal.length; i++) ws.push(kMedir(comp, pal[i].t, fonte, ts[i], trk));
|
|
55
|
+
sw = Math.max(tam * 0.27, kMedir(comp, 'a a', fonte, tam, trk) - kMedir(comp, 'aa', fonte, tam, trk));
|
|
56
|
+
var mx = 0; for (i = 0; i < ws.length; i++) if (ws[i] > mx) mx = ws[i];
|
|
57
|
+
linhas = kQuebra(ws, sw, maxW);
|
|
58
|
+
if (linhas.length > 1) { // linhas de tamanho parecido
|
|
59
|
+
var tot = 0; for (i = 0; i < ws.length; i++) tot += ws[i] + sw;
|
|
60
|
+
var l2 = kQuebra(ws, sw, Math.min(maxW, Math.max(mx, tot / linhas.length * 1.12)));
|
|
61
|
+
if (l2.length <= linhas.length) linhas = l2;
|
|
62
|
+
}
|
|
63
|
+
if ((mx <= maxW && linhas.length <= maxLin) || tent > 16 || tam * 0.92 < (o.min || 40)) break;
|
|
64
|
+
tam *= 0.92; tent++;
|
|
65
|
+
}
|
|
66
|
+
var lh = tamMax * 1.12, bloco = linhas.length * lh, l, k, idx = 0, lastT = o.t0 || 0, tempos = [], fim = o.t0 || 0, passo = o.passo || 0.16;
|
|
67
|
+
for (l = 0; l < linhas.length; l++) {
|
|
68
|
+
var lw = 0; for (k = 0; k < linhas[l].length; k++) lw += ws[linhas[l][k]] + (k ? sw : 0);
|
|
69
|
+
var xs = o.cx - lw / 2, yb = o.cy - bloco / 2 + lh * l + lh * 0.5 + tamMax * 0.33;
|
|
70
|
+
for (k = 0; k < linhas[l].length; k++) {
|
|
71
|
+
var j = linhas[l][k], w = ws[j], xc = xs + w / 2; xs += w + sw;
|
|
72
|
+
var ti = (o.t0 || 0) + idx * passo;
|
|
73
|
+
if (KIT.voz) { var tv = kFalaBusca(pal[j].t); ti = tv != null ? tv : Math.max(lastT + 0.08, 0.25); }
|
|
74
|
+
lastT = ti;
|
|
75
|
+
if (pal[j].kw) {
|
|
76
|
+
var ls = kDLetras(comp, pal[j].t, { tam: ts[j], fonte: fonte, x: xc, y: yb, w: w, parent: o.parent, c1: o.c1 || P.ac, c2: o.c2 || kDCor2(P), trk: trk }), q;
|
|
77
|
+
for (q = 0; q < ls.length; q++) kDEntra(comp, ls[q].L, o.modo, ti + (o.modo == 'mascara' ? 0 : q * 0.022), ls[q].x, yb, ls[q].w, ts[j], idx, pal[j].t.charAt(q));
|
|
78
|
+
} else {
|
|
79
|
+
var L = comp.layers.addText(pal[j].t); L.name = 'dz-palavra ' + pal[j].t;
|
|
80
|
+
if (o.parent) kPai(L, o.parent);
|
|
81
|
+
kDoc(L, fonte, tam, o.cor || P.fg, { tracking: trk });
|
|
82
|
+
kP(L).setValue([xc, yb]);
|
|
83
|
+
kDEntra(comp, L, o.modo, ti, xc, yb, w, ts[j], idx, pal[j].t);
|
|
84
|
+
}
|
|
85
|
+
tempos.push(ti); fim = Math.max(fim, ti + 0.45); idx++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
var wMax = 0; for (l = 0; l < linhas.length; l++) { var lw2 = 0; for (k = 0; k < linhas[l].length; k++) lw2 += ws[linhas[l][k]] + (k ? sw : 0); if (lw2 > wMax) wMax = lw2; }
|
|
89
|
+
return { fim: fim, h: bloco, w: wMax, tam: tam, tamMax: tamMax, tempos: tempos };
|
|
90
|
+
}
|
|
91
|
+
function kDPasso(comp, n) { return Math.max(0.12, Math.min(0.3, (comp.duration * 0.5) / Math.max(1, n))); }
|
|
92
|
+
/* o brilho rosado sutil do fundo creme (a pele do Destrava): um círculo de luz do destaque, bem fraco, embaixo; só quando a pele é a do Destrava */
|
|
93
|
+
function kDBrilho(comp, P, g) {
|
|
94
|
+
if (!(KIT.pele == 'destrava')) return;
|
|
95
|
+
var s = comp.layers.addSolid(kCor(P.bg), 'brilho-creme', g.W, g.H, 1);
|
|
96
|
+
try {
|
|
97
|
+
var rp = kFx(s, 'ADBE Ramp');
|
|
98
|
+
rp.property('ADBE Ramp-0001').setValue([g.cx, g.H * 0.8]); rp.property('ADBE Ramp-0002').setValue(kCor(kMisturar(P.bg, P.ac, 0.09)));
|
|
99
|
+
rp.property('ADBE Ramp-0003').setValue([g.cx, g.H * 0.8 + Math.max(g.W, g.H) * 0.62]); rp.property('ADBE Ramp-0004').setValue(kCor(P.bg));
|
|
100
|
+
rp.property('ADBE Ramp-0005').setValue(2);
|
|
101
|
+
} catch (e) { }
|
|
102
|
+
}
|
|
103
|
+
function kDModo(d) { return d.modo == 'mascara' || d.modo == 'escala' ? d.modo : 'suave'; }
|
|
104
|
+
function kDTam(d, g) { return Math.max(70, Math.min(430, (d.tam || 200) * g.u)); }
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------- frase
|
|
107
|
+
function cenaDFrase(comp, P, d) {
|
|
108
|
+
var g = kPre(comp, P), W = g.W, H = g.H, cam = g.cam, n = kPalavras(d.titulo || '').length;
|
|
109
|
+
kDBrilho(comp, P, g);
|
|
110
|
+
var r = kDFrase(comp, P, d.titulo || '', { cx: g.cx, cy: g.v ? H * 0.44 : g.cy, tam: kDTam(d, g), min: Math.min(kDTam(d, g), 150 * g.u), u: g.u, maxW: W * (1 - 2 * KIT.MX) * 0.98, parent: cam.no, t0: 0.14, modo: kDModo(d), passo: kDPasso(comp, n) });
|
|
111
|
+
if (r.tempos.length) { som(r.tempos[0], 'pop', 0.5); if (r.tempos.length > 3) som(r.tempos[r.tempos.length - 1], 'tique', 0.35); }
|
|
112
|
+
KIT.rects.push({ espaco: 't', cena: KIT.cenaIdx, x0: g.cx - r.w / 2, x1: g.cx + r.w / 2, y0: (g.v ? H * 0.44 : g.cy) - r.h / 2, y1: (g.v ? H * 0.44 : g.cy) + r.h / 2, t0: 0, t1: 1, tam: r.tam, texto: false, livre: true, sobrepoe: true, nome: 'frase' });
|
|
113
|
+
cam.empurra(0.6, 0, 0, 1.0).empurra(comp.duration - 0.2, 0, 0, 1.06);
|
|
114
|
+
fecha(comp);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------- quadro de cor cheia (o círculo de cor cresce, como o card do S8)
|
|
118
|
+
function cenaDCor(comp, P, d) {
|
|
119
|
+
var g = kPre(comp, P), W = g.W, H = g.H, cam = g.cam, cy = g.v ? H * 0.44 : g.cy, n = kPalavras(d.titulo || '').length;
|
|
120
|
+
var centro = kMisturar(P.ac, '#FFFFFF', 0.82), borda = kMisturar(kDCor2(P), '#000000', 0.18);
|
|
121
|
+
var s = comp.layers.addSolid(kCor(P.ac), 'fundo-cor', W, H, 1);
|
|
122
|
+
try {
|
|
123
|
+
var rp = kFx(s, 'ADBE Ramp');
|
|
124
|
+
rp.property('ADBE Ramp-0001').setValue([g.cx, cy]); rp.property('ADBE Ramp-0002').setValue(centro);
|
|
125
|
+
rp.property('ADBE Ramp-0003').setValue([g.cx, cy + Math.max(W, H) * 0.62]); rp.property('ADBE Ramp-0004').setValue(borda);
|
|
126
|
+
rp.property('ADBE Ramp-0005').setValue(2);
|
|
127
|
+
} catch (e) { }
|
|
128
|
+
var m = s.property('ADBE Mask Parade').addProperty('ADBE Mask Atom'), mp = m.property('ADBE Mask Shape'), R = Math.sqrt(W * W + H * H);
|
|
129
|
+
mp.setValueAtTime(kFrame(0.05), kCircShape(g.cx, cy, 4)); mp.setValueAtTime(kFrame(0.05) + 0.55, kCircShape(g.cx, cy, R * 0.62));
|
|
130
|
+
var ink = kContr(centro, P.fg) >= 4.5 ? P.fg : (kLum(centro) > 0.5 ? '#14110F' : '#FFFFFF');
|
|
131
|
+
var r = kDFrase(comp, P, d.titulo || '', { cx: g.cx, cy: cy, tam: kDTam(d, g), min: Math.min(kDTam(d, g), 150 * g.u), u: g.u, maxW: W * (1 - 2 * KIT.MX) * 0.98, parent: cam.no, t0: 0.4, modo: d.modo == 'mascara' ? 'mascara' : 'escala', cor: ink, c1: kDLegivel(P.ac, centro), c2: kDLegivel(kDCor2(P), centro), passo: kDPasso(comp, n) });
|
|
132
|
+
som(0.1, 'sobe', 0.5); if (r.tempos.length) som(r.tempos[0], 'pop', 0.6);
|
|
133
|
+
cam.empurra(0.6, 0, 0, 1.0).empurra(comp.duration - 0.2, 0, 0, 1.05);
|
|
134
|
+
fecha(comp);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------- cartão de foto arredondado (+ carrossel)
|
|
138
|
+
function kDCartao(comp, P, arq, w, h, u, t0, dur, zoomDe, zoomPara) {
|
|
139
|
+
var it = kFotoImp(arq);
|
|
140
|
+
if (!it) return null;
|
|
141
|
+
var inner = novaComp('dz-foto ' + KIT.cenaIdx + ' ' + Math.round(w), Math.round(w), Math.round(h), dur, KIT.fps), ease = [60, 60];
|
|
142
|
+
var im = inner.layers.add(it); im.name = 'foto-img';
|
|
143
|
+
var ki = Math.max(w / it.width, h / it.height) * 100;
|
|
144
|
+
kP(im).setValue([w / 2, h / 2]);
|
|
145
|
+
curva(kS(im), [[0, [ki * zoomDe, ki * zoomDe], ease[0], ease[1]], [dur, [ki * zoomPara, ki * zoomPara], ease[0], ease[1]]]);
|
|
146
|
+
var card = comp.layers.add(inner); card.name = 'dz-cartao';
|
|
147
|
+
kMascaraRect(card, 0, 0, w, h, 36 * u);
|
|
148
|
+
return card;
|
|
149
|
+
}
|
|
150
|
+
function cenaDFoto(comp, P, d) {
|
|
151
|
+
var g = kPre(comp, P), W = g.W, H = g.H, u = g.u, cam = g.cam, dur = comp.duration, F = d.foto, T0 = 0.1, i, sg = d.sentido || 1;
|
|
152
|
+
kDBrilho(comp, P, g);
|
|
153
|
+
if (!F || !F.arquivo) { KIT.avisos.push('cena ' + KIT.cenaIdx + ': sem foto, a cena virou frase'); return cenaDFrase(comp, P, d); }
|
|
154
|
+
var todas = [F], outras = KIT.fotos || [];
|
|
155
|
+
for (i = 0; i < outras.length && todas.length < 4; i++) if (outras[i] && outras[i].arquivo && outras[i].arquivo != F.arquivo) todas.push(outras[i]);
|
|
156
|
+
var carrossel = todas.length >= 2, cw = g.v ? W * (carrossel ? 0.7 : 0.88) : W * 0.4, ch = g.v ? H * (carrossel ? 0.4 : 0.5) : H * 0.62, cy = g.v ? H * 0.4 : g.cy;
|
|
157
|
+
var gap = cw * 1.1, x0 = g.cx, cards = [], c;
|
|
158
|
+
for (i = 0; i < todas.length; i++) {
|
|
159
|
+
c = kDCartao(comp, P, todas[i].arquivo, cw, ch, u, T0, dur, 1.04, 1.1 + 0.02 * i);
|
|
160
|
+
if (!c) continue;
|
|
161
|
+
kPai(c, cam.no); kS(c).setValue([100, 100]);
|
|
162
|
+
var sombra = forma(comp, 'retangulo', { x: 0, y: 0, w: cw, h: ch, r: 36 * u, cor: '#000000', alfa: 38, parent: cam.no, nome: 'sombra-foto' });
|
|
163
|
+
kFx(sombra, 'ADBE Gaussian Blur 2').property('ADBE Gaussian Blur 2-0001').setValue(38 * u);
|
|
164
|
+
cards.push({ c: c, s: sombra, i: i });
|
|
165
|
+
}
|
|
166
|
+
for (i = 0; i < cards.length; i++) {
|
|
167
|
+
c = cards[i];
|
|
168
|
+
var xa = x0 + (i - 0) * gap * sg, xb = x0 + (i - (cards.length - 1) * 0.0) * gap * sg;
|
|
169
|
+
if (carrossel) { // o carrossel anda: cada cartão passa pelo centro, ease, sem parar
|
|
170
|
+
var passo = (cards.length - 1) * gap, ini = x0 + i * gap, fim = ini - passo * 0.62;
|
|
171
|
+
curva(kP(c.c), [[0, [ini * 1, cy], 60, 60], [dur, [fim, cy], 60, 60]]);
|
|
172
|
+
curva(kP(c.s), [[0, [ini, cy + 34 * u], 60, 60], [dur, [fim, cy + 34 * u], 60, 60]]);
|
|
173
|
+
} else {
|
|
174
|
+
// um cartão: voa de longe (X +300, Y 8°, gira -38°) e assenta; depois a câmera cresce 5% (S4)
|
|
175
|
+
mola(kP(c.c), T0, [x0 + W * 0.3, cy], [x0, cy], { quique: 0.3, dur: 0.75 });
|
|
176
|
+
kP(c.s).setValue([x0, cy + 34 * u]);
|
|
177
|
+
curva(kO(c.s), [[T0, 0, null, 30], [T0 + 0.5, 38, 60, null]]);
|
|
178
|
+
curva(kS(c.c), [[T0, [92, 92], 60, 60], [T0 + 0.7, [100, 100], 60, 60], [dur, [105, 105], 60, 60]]);
|
|
179
|
+
}
|
|
180
|
+
curva(kO(c.c), [[T0, 0, null, 30], [T0 + 0.28, 100, 60, null]]);
|
|
181
|
+
try {
|
|
182
|
+
c.c.threeDLayer = true;
|
|
183
|
+
if (carrossel) curva(kRot(c.c, 'Y'), [[0, -16 * sg, 60, 60], [dur, 16 * sg, 60, 60]]);
|
|
184
|
+
else { curva(kRot(c.c, 'Y'), [[T0, -38 * sg, 60, 60], [T0 + 0.8, 0, 60, 60]]); curva(kRot(c.c, 'X'), [[T0, 8, 60, 60], [T0 + 0.8, 0, 60, 60]]); }
|
|
185
|
+
} catch (e) { KIT.avisos.push('cena ' + KIT.cenaIdx + ': 3D da foto indisponível'); }
|
|
186
|
+
}
|
|
187
|
+
som(T0, 'impacto', 0.5); som(T0 + 0.45, 'sobe', 0.4);
|
|
188
|
+
var n = kPalavras(d.titulo || '').length;
|
|
189
|
+
if (d.titulo) kDFrase(comp, P, d.titulo, { cx: g.cx, cy: g.v ? H * 0.76 : g.cy, tam: Math.min(kDTam(d, g), 150 * u), min: 56 * u, u: u, maxW: g.v ? W * 0.84 : W * 0.4, parent: cam.no, t0: T0 + 0.5, modo: 'suave', passo: kDPasso(comp, n), maxLinhas: 2 });
|
|
190
|
+
cam.empurra(0.8, 0, 0, 1.0).empurra(comp.duration - 0.2, 0, 0, 1.04);
|
|
191
|
+
fecha(comp);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------- fecho com a marca real
|
|
195
|
+
function cenaDMarca(comp, P, d) {
|
|
196
|
+
var g = kPre(comp, P), W = g.W, H = g.H, u = g.u, cam = g.cam, marca = d.botao || '', T0 = 0.15, yM = g.v ? H * 0.4 : g.cy * 0.8;
|
|
197
|
+
kDBrilho(comp, P, g);
|
|
198
|
+
var fimM = T0;
|
|
199
|
+
if (d.logo) { // a logo real do kit de marca, revelada por varredura (S9)
|
|
200
|
+
imagem(comp, d.logo, { x: g.cx, y: yM, w: W * 0.7, h: W * 0.7 * 0.35, parent: cam.no, nome: 'dz-logo', entrada: 'sobe', t0: T0 });
|
|
201
|
+
fimM = T0 + 0.6;
|
|
202
|
+
} else if (marca) {
|
|
203
|
+
var rm = kDFrase(comp, P, '*' + marca.replace(/\*/g, '') + '*', { cx: g.cx, cy: yM, tam: 260 * u, maxW: W * 0.84, parent: cam.no, t0: T0, modo: 'mascara', maxLinhas: 2, passo: 0.2 });
|
|
204
|
+
fimM = rm.fim;
|
|
205
|
+
}
|
|
206
|
+
var n = kPalavras(d.titulo || '').length, sem = d.titulo ? d.titulo.replace(/\*/g, '') : '';
|
|
207
|
+
if (sem) kDFrase(comp, P, sem, { cx: g.cx, cy: g.v ? H * 0.62 : g.cy * 1.3, tam: 110 * u, min: 56 * u, u: u, maxW: W * 0.84, parent: cam.no, t0: Math.max(T0 + 0.5, fimM * 0.7), modo: 'suave', cor: P.fg, passo: kDPasso(comp, n), maxLinhas: 2 });
|
|
208
|
+
som(T0, 'pop', 0.6);
|
|
209
|
+
cam.empurra(0.6, 0, 0, 1.0).empurra(comp.duration - 0.2, 0, 0, 1.05);
|
|
210
|
+
fecha(comp);
|
|
211
|
+
}
|
|
@@ -354,7 +354,7 @@ function montarDirecao({ args, assunto, meta, duracao, hist, rnd, temElemento })
|
|
|
354
354
|
cenas.push({ c, tipo, papel });
|
|
355
355
|
});
|
|
356
356
|
// a FOTO do pedido: pelo menos uma cena "foto" (a de abertura e/ou a de destaque), nunca duas coladas
|
|
357
|
-
if (nFotosPedido > 0) colocarFotos(cenas, nFotosPedido);
|
|
357
|
+
if (nFotosPedido > 0 && !args._planoDasEtapas) colocarFotos(cenas, nFotosPedido); // com o plano das etapas, quem põe foto é o plano
|
|
358
358
|
// o assunto aparece em OBJETOS: pelo menos 2 cenas com primitiva que usa o elemento real (as que o modelo fixou ficam como estão)
|
|
359
359
|
if (temElemento) {
|
|
360
360
|
const nao = new Set(meta.nao || []);
|
|
@@ -105,7 +105,7 @@ function termosDoAssunto({ assunto, pedidos = [], meta = null }) {
|
|
|
105
105
|
* Reúne `minimo` a `max` elementos reais. `deps.icone3d/animacao` são injetáveis (testes). Nunca lança.
|
|
106
106
|
* Devolve {ok, elementos, creditos, faltou}.
|
|
107
107
|
*/
|
|
108
|
-
async function reunir({ assunto, pedidos = [], meta = null, pasta, minimo = 3, max = 6, rede = true, logos = [], site = null, animados = true, deps = {} } = {}) {
|
|
108
|
+
async function reunir({ assunto, pedidos = [], meta = null, pasta, minimo = 3, max = 6, rede = true, logos = [], site = null, animados = true, reserva = true, deps = {} } = {}) {
|
|
109
109
|
if (!pasta) return { ok: false, error: 'faltou a pasta do projeto.', elementos: [] };
|
|
110
110
|
const ic3 = deps.icone3d || icone3d, an = deps.animacao || animacao;
|
|
111
111
|
const termos = termosDoAssunto({ assunto, pedidos, meta });
|
|
@@ -122,7 +122,7 @@ async function reunir({ assunto, pedidos = [], meta = null, pasta, minimo = 3, m
|
|
|
122
122
|
if (animados && rede && meta && meta.animado) {
|
|
123
123
|
for (const t of meta.animado.slice(0, 1)) { const e = await an(t, pasta).catch(() => null); if (tem(e) && elementos.length < max + 1) elementos.push(e); }
|
|
124
124
|
}
|
|
125
|
-
for (const t of ['sparkles', 'star', 'check mark button', 'light bulb']) { // última reserva: ainda é "real", só menos específico
|
|
125
|
+
for (const t of (reserva ? ['sparkles', 'star', 'check mark button', 'light bulb'] : [])) { // reserva: false = nada de enfeite sem relação com o assunto // última reserva: ainda é "real", só menos específico
|
|
126
126
|
if (elementos.length >= minimo) break;
|
|
127
127
|
const e = await ic3(t, pasta).catch(() => null); if (tem(e)) elementos.push(e);
|
|
128
128
|
}
|
|
@@ -87,20 +87,33 @@ function cartaoDestaque(bg, fg, ac, brilho) {
|
|
|
87
87
|
return { cardAc: cor, sobreCardAc: melhorCor(cor, [fg, bg, '#FFFFFF', '#14110F']) };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** O segundo tom do degradê (palavra-chave): o destaque com o matiz girado ~-40°, igual magenta → índigo do Destrava; sem contraste de leitura, cai no meio-termo com a tinta. */
|
|
91
|
+
function segundoTom(ac, bg, fg) {
|
|
92
|
+
const c = (rgb(ac) || [255, 255, 255]).map((v) => v / 255), mx = Math.max(...c), mn = Math.min(...c), l = (mx + mn) / 2, d = mx - mn;
|
|
93
|
+
if (d < 0.02) return misturar(ac, fg, 0.5); // cinza: sem matiz pra girar
|
|
94
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
95
|
+
let h = mx === c[0] ? ((c[1] - c[2]) / d) % 6 : mx === c[1] ? (c[2] - c[0]) / d + 2 : (c[0] - c[1]) / d + 4;
|
|
96
|
+
h = (((h * 60 - 40) % 360) + 360) % 360;
|
|
97
|
+
const k = (1 - Math.abs(2 * l - 1)) * s, x = k * (1 - Math.abs(((h / 60) % 2) - 1)), m = l - k / 2;
|
|
98
|
+
const [r, g, b] = h < 60 ? [k, x, 0] : h < 120 ? [x, k, 0] : h < 180 ? [0, k, x] : h < 240 ? [0, x, k] : h < 300 ? [x, 0, k] : [k, 0, x];
|
|
99
|
+
const out = hex([(r + m) * 255, (g + m) * 255, (b + m) * 255]);
|
|
100
|
+
return contraste(out, bg) >= 3 ? out : misturar(ac, fg, 0.5);
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
/** As 3 variações da paleta: normal, fundo profundo e fundo de destaque, sempre com contraste de leitura. */
|
|
91
104
|
function variacoes(base) {
|
|
92
105
|
const fundo = base.fundo, tinta = base.tinta, dest = base.destaque;
|
|
93
106
|
const claroBg = luminancia(fundo) >= luminancia(tinta) ? fundo : tinta;
|
|
94
107
|
const escuroBg = claroBg === fundo ? tinta : fundo;
|
|
95
|
-
const fix = (bg, fg, ac) => {
|
|
108
|
+
const fix = (bg, fg, ac, ac2Dado) => {
|
|
96
109
|
if (contraste(fg, bg) < 4.5) fg = melhorCor(bg, ['#FFFFFF', '#111111']);
|
|
97
|
-
if (contraste(ac, bg) < 3) ac = melhorCor(bg, [dest, fg, tinta, fundo]);
|
|
110
|
+
if (contraste(ac, bg) < 3 && !(base.destaque2 && ac === dest)) ac = melhorCor(bg, [dest, fg, tinta, fundo]); // a pele do Destrava (destaque2) mantém o rosa dela: 2,8:1 no creme, em texto enorme
|
|
98
111
|
const mudo = misturar(bg, fg, 0.66);
|
|
99
112
|
const brilho = misturar(bg, ac, 0.32);
|
|
100
|
-
return { bg, fg, ac, mudo: contraste(mudo, bg) >= 3 ? mudo : fg, card: misturar(bg, fg, 0.1), brilho, ...cartaoDestaque(bg, fg, ac, brilho) };
|
|
113
|
+
return { bg, fg, ac, ac2: ac2Dado && contraste(ac2Dado, bg) >= 3 ? ac2Dado : segundoTom(ac, bg, fg), mudo: contraste(mudo, bg) >= 3 ? mudo : fg, card: misturar(bg, fg, 0.1), brilho, ...cartaoDestaque(bg, fg, ac, brilho) };
|
|
101
114
|
};
|
|
102
115
|
return {
|
|
103
|
-
normal: fix(fundo, tinta, dest),
|
|
116
|
+
normal: fix(fundo, tinta, dest, base.destaque2),
|
|
104
117
|
fundo: fix(escuroBg, claroBg, dest),
|
|
105
118
|
destaque: (() => { const fg = melhorCor(dest, [claroBg, escuroBg]), ac = melhorCor(dest, [escuroBg, claroBg]); return fix(dest, contraste(fg, dest) >= 3 ? fg : melhorCor(dest, ['#FFFFFF', '#111111']), contraste(ac, dest) >= 3 ? ac : melhorCor(dest, ['#FFFFFF', '#111111'])); })(),
|
|
106
119
|
};
|
|
@@ -171,6 +184,7 @@ function plano(args = {}, opc = {}) {
|
|
|
171
184
|
fundo: rgb(cores.fundo) ? cores.fundo : base.fundo,
|
|
172
185
|
tinta: rgb(cores.tinta) ? cores.tinta : base.tinta,
|
|
173
186
|
destaque: rgb(cores.destaque) ? cores.destaque : base.destaque,
|
|
187
|
+
...(args.pele === 'destrava' && rgb(cores.destaque2) ? { destaque2: cores.destaque2 } : {}),
|
|
174
188
|
};
|
|
175
189
|
const v = variacoes(paleta);
|
|
176
190
|
// o ciclo de fundos: o fundo da marca domina; o de destaque e o invertido entram de contraponto (nunca iguais em fila)
|
|
@@ -183,6 +197,7 @@ function plano(args = {}, opc = {}) {
|
|
|
183
197
|
if (i === cenas.length - 1 && cenas.length > 3) p = i > 0 && cenas[i - 1].pal.bg === F.bg ? (cenas[i - 1].pal.bg !== N.bg ? N : D) : F; // o CTA fecha no fundo invertido, a não ser que a cena anterior já o use
|
|
184
198
|
c.pal = { ...p };
|
|
185
199
|
});
|
|
200
|
+
if (args.pele === 'destrava') cenas.forEach((c) => { c.pal = { ...N }; }); // a pele do Destrava: todo quadro no creme; a cor entra só no quadro de cor cheia e no degradê
|
|
186
201
|
cenas.forEach((c, i) => { if (cenas[i + 1]) c.proxBg = cenas[i + 1].pal.bg; });
|
|
187
202
|
|
|
188
203
|
const tip = diretor.TIPOGRAFIAS[dir.tipografia];
|
|
@@ -193,7 +208,7 @@ function plano(args = {}, opc = {}) {
|
|
|
193
208
|
cenas.forEach((c) => { const e = (diretor.PRIMITIVAS[c.tipo] || {}).entra || 2; c.f = Math.round(Math.max(1, Math.min(2.2, e / (0.36 * c.dur))) * 100) / 100; c.elemento = !!(diretor.PRIMITIVAS[c.tipo] || {}).elemento; });
|
|
194
209
|
const duracao = Math.round(cenas.reduce((s, c) => s + c.dur, 0) * 100) / 100;
|
|
195
210
|
cenas.forEach((c, i) => { c.inicio = Math.round(cenas.slice(0, i).reduce((s, x) => s + x.dur, 0) * 1000) / 1000; });
|
|
196
|
-
return { w, h, fps: 30, formato, fontes, tipo, paleta, trocas: dir.trocas, cenas, ajuste: 1, receita: receita ? receita.id : null, duracao,
|
|
211
|
+
return { w, h, fps: 30, formato, fontes, tipo, paleta, pele: args.pele === 'destrava' ? 'destrava' : null, trocas: dir.trocas, cenas, ajuste: 1, receita: receita ? receita.id : null, duracao,
|
|
197
212
|
camera: dir.camera, elementos, fotos, bpm: 0, voz: null, legenda: null, direcao: dir.assinatura, semente: dir.semente, variacoes: v };
|
|
198
213
|
}
|
|
199
214
|
|
|
@@ -253,7 +268,7 @@ function ajustarCortes(pl, batidas, { tolerancia = 0.28 } = {}) {
|
|
|
253
268
|
const lerKit = (n) => fs.readFileSync(path.join(KIT, n), 'utf8');
|
|
254
269
|
/** O JSX completo: o kit, a guarda de layout, as primitivas, as cenas e a chamada `montar(PLANO)`. */
|
|
255
270
|
function script(pl) {
|
|
256
|
-
return lerKit('base.jsx') + '\n' + lerKit('guarda.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
|
|
271
|
+
return lerKit('base.jsx') + '\n' + lerKit('guarda.jsx') + '\n' + lerKit('primitivas.jsx') + '\n' + lerKit('destrava.jsx') + '\n' + lerKit('cenas.jsx') + '\nmontar(' + JSON.stringify(pl) + ');\n';
|
|
257
272
|
}
|
|
258
273
|
/** Os tempos medidos de cada cena (início de cada uma e o instante de conferir). */
|
|
259
274
|
function tempos(pl) {
|
|
@@ -354,6 +369,7 @@ async function acharTrilha(pasta, busca) {
|
|
|
354
369
|
/** As fontes para o render: o peso que a família tem (as de peso único vão em 400). */
|
|
355
370
|
const SO_400 = new Set(['Anton', 'Archivo Black', 'Instrument Serif', 'Bebas Neue', 'Caveat']);
|
|
356
371
|
function fontesDoPlano(pl) {
|
|
372
|
+
if (pl.pele === 'destrava') return [{ familia: 'Inter', peso: 800 }]; // a pele do Destrava é Inter 800 em tudo (o arquivo vai embutido, sem rede)
|
|
357
373
|
const f = pl.fontes, it = pl.tipo && pl.tipo.realce === 'italico';
|
|
358
374
|
const so = (fam) => SO_400.has(fam);
|
|
359
375
|
return [
|
|
@@ -408,7 +424,9 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
408
424
|
const meta = diretor.achar(assunto + ' ' + (args.metafora || ''));
|
|
409
425
|
const pedidos = Array.isArray(args.elementos) ? args.elementos : (args.elementos ? [args.elementos] : []);
|
|
410
426
|
const reunir = deps.elementos || require('./maquina-elementos.js').reunir;
|
|
411
|
-
|
|
427
|
+
// com o plano das etapas só entra elemento que o usuário pediu (elementos, logos, site): enfeite sem relação com o assunto (a estrela 3D de "última reserva") não entra
|
|
428
|
+
const soPedido = !!args._planoDasEtapas && !pedidos.length && !(args.logos && args.logos.length) && !args.site;
|
|
429
|
+
const el = soPedido ? { elementos: [] } : await reunir({ assunto, pedidos, meta, pasta, rede: !semRede, logos: args.logos, site: args.site, minimo: args._planoDasEtapas ? 0 : 3, max: 6, reserva: !args._planoDasEtapas });
|
|
412
430
|
const elementos = (el && el.elementos) || [];
|
|
413
431
|
|
|
414
432
|
// 2. a direção (primitivas, transições, câmera, tipografia, paleta) com semente e conferência de assinatura
|
|
@@ -418,7 +436,7 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
418
436
|
let pl = plano({ ...args, assunto, gosto: gostoP }, { receita, historico, elementos, fotos });
|
|
419
437
|
const avisosDeMotor = fotosRecusadas.map((x) => 'foto recusada: ' + x.motivo);
|
|
420
438
|
if (fotos.length && !pl.cenas.some((c) => c.tipo === 'foto')) avisosDeMotor.push('as fotos não entraram em nenhuma cena (o roteiro fixou todas as cenas com outro tipo).');
|
|
421
|
-
if (elementos.length < 3) avisosDeMotor.push(`só ${elementos.length} elemento(s) real(is) do assunto (o pedido é ≥ 3)`);
|
|
439
|
+
if (elementos.length < 3 && !soPedido) avisosDeMotor.push(`só ${elementos.length} elemento(s) real(is) do assunto (o pedido é ≥ 3)`);
|
|
422
440
|
|
|
423
441
|
// 3. a narração (opcional) manda no tempo
|
|
424
442
|
let voz = null;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ajustar.js — etapa 1 do motion: AJUSTAR O PEDIDO.
|
|
3
|
+
*
|
|
4
|
+
* O pedido do usuário ("faz um motion pra minha barbearia") vira {objetivo, publico, tom, virada, fatos}.
|
|
5
|
+
* A virada é a frase que quebra a expectativa. Se falta algo essencial
|
|
6
|
+
* (foto real do assunto, marca, data), sai até 4 perguntas no formato do `perguntar` (pergunta + 2 a 4
|
|
7
|
+
* opções); quem responde é `ctx.escolher` (o mesmo picker do terminal). Sem terminal ou pulou: segue com o que tem.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { pedirJson } = require('./llm.js');
|
|
12
|
+
const Fatos = require('./fatos.js');
|
|
13
|
+
|
|
14
|
+
const MAX_PERGUNTAS = 4;
|
|
15
|
+
|
|
16
|
+
const SISTEMA = `Você é o diretor de criação de um estúdio de motion graphics em PT-BR. Recebe o pedido cru de um usuário leigo e o reescreve como briefing.
|
|
17
|
+
Responda SÓ JSON:
|
|
18
|
+
{"objetivo":"o que o vídeo precisa fazer acontecer (1 frase)",
|
|
19
|
+
"publico":"quem vai ver (1 frase)",
|
|
20
|
+
"tom":"3 a 5 palavras (ex.: direto, bem-humorado, confiante)",
|
|
21
|
+
"virada":"UMA frase curta que quebra a expectativa. Padrão: a pergunta que o público faria (ou o clichê do assunto) -> a resposta que quebra essa expectativa, específica DESTE assunto. Escreva a pergunta e a resposta juntas, com ' -> '. Invente a virada a partir do pedido; não reaproveite frase pronta.",
|
|
22
|
+
"fatos":{"datas":[],"nomes":[],"marca":"","local":"","oferta":"","outros":[]},
|
|
23
|
+
"faltando":[{"campo":"foto|marca|data|local|oferta|outro","pergunta":"pergunta curta em PT-BR","opcoes":[{"label":"resposta curta","desc":"o que acontece se escolher"}]}]}
|
|
24
|
+
Regras:
|
|
25
|
+
- "fatos" só leva o que o usuário DISSE. Nunca invente data, nome, preço, endereço ou número. O que não foi dito fica vazio.
|
|
26
|
+
- "virada" vira a DOR ou o clichê do público, nunca afirma coisa nova sobre o negócio: NÃO diga que está cheio, que é o melhor, que tem fila, promoção ou qualidade. A resposta da virada nunca inventa lotação, preço, data, número ou diferencial.
|
|
27
|
+
- "faltando": só o que é ESSENCIAL e muda o vídeo (foto real do assunto, nome da marca, data/hora, local). No máximo ${MAX_PERGUNTAS} itens, cada um com 2 a 4 opções (a primeira é a recomendada). Se o pedido já tem tudo, [].
|
|
28
|
+
- Não pergunte gosto estético (cor, fonte): isso a gente decide.`;
|
|
29
|
+
|
|
30
|
+
const lista = (v) => (Array.isArray(v) ? v : v ? [v] : []).map((x) => String(x || '').trim()).filter(Boolean).slice(0, 8);
|
|
31
|
+
const txt = (v, n) => String(v || '').replace(/\s+/g, ' ').trim().slice(0, n);
|
|
32
|
+
|
|
33
|
+
/** Confere e limpa a resposta do modelo. Puro. */
|
|
34
|
+
function normalizar(j, pedido) {
|
|
35
|
+
j = j && typeof j === 'object' ? j : {};
|
|
36
|
+
const f = j.fatos && typeof j.fatos === 'object' ? j.fatos : {};
|
|
37
|
+
const faltando = (Array.isArray(j.faltando) ? j.faltando : []).map((q) => {
|
|
38
|
+
const opcoes = (Array.isArray(q && q.opcoes) ? q.opcoes : []).map((o) => (typeof o === 'string' ? { label: o, desc: '' } : { label: txt(o && o.label, 60), desc: txt(o && o.desc, 90) })).filter((o) => o.label).slice(0, 4);
|
|
39
|
+
return { campo: txt(q && q.campo, 20) || 'outro', pergunta: txt(q && q.pergunta, 120), opcoes };
|
|
40
|
+
}).filter((q) => q.pergunta && q.opcoes.length >= 2).slice(0, MAX_PERGUNTAS);
|
|
41
|
+
return {
|
|
42
|
+
pedido: txt(pedido, 600),
|
|
43
|
+
objetivo: txt(j.objetivo, 200), publico: txt(j.publico, 200), tom: txt(j.tom, 80),
|
|
44
|
+
virada: txt(j.virada, 160),
|
|
45
|
+
fatos: { datas: lista(f.datas), nomes: lista(f.nomes), marca: txt(f.marca, 60), local: txt(f.local, 80), oferta: txt(f.oferta, 120), outros: lista(f.outros) },
|
|
46
|
+
faltando,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A resposta da virada com fato que o usuário não disse (número, data, preço, nome, superlativo): devolve a lista ou []. Puro. */
|
|
51
|
+
function fatoNaVirada(b, pedido) {
|
|
52
|
+
const resp = String(b.virada || '').split(/->|→/)[1] || '';
|
|
53
|
+
return Fatos.limpar(resp, Fatos.fonte(pedido)).removidos;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** O que falta de verdade no briefing (para teste e para o aviso final). */
|
|
57
|
+
function validar(b) {
|
|
58
|
+
const erros = [];
|
|
59
|
+
if (!b.objetivo) erros.push('sem objetivo');
|
|
60
|
+
if (!b.publico) erros.push('sem público');
|
|
61
|
+
if (!b.tom) erros.push('sem tom');
|
|
62
|
+
if (!b.virada || b.virada.length < 6) erros.push('sem virada');
|
|
63
|
+
else if (!/->|→/.test(b.virada)) erros.push('virada sem seta (pergunta -> resposta)');
|
|
64
|
+
else if (b.virada.split(/->|→/)[1].trim().split(/\s+/).length > 4) erros.push('virada com resposta longa (máx. 4 palavras, sem fato novo)');
|
|
65
|
+
return erros;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Faz as perguntas (uma por vez, no formato do `perguntar`). Devolve [{campo, pergunta, resposta}] só do que foi respondido. */
|
|
69
|
+
async function perguntar(faltando, escolher) {
|
|
70
|
+
const out = [];
|
|
71
|
+
if (typeof escolher !== 'function') return out;
|
|
72
|
+
for (const q of faltando) {
|
|
73
|
+
let r = null;
|
|
74
|
+
try { r = await escolher(q.pergunta, q.opcoes.map((o) => o.label), { livre: true }); } catch { r = null; }
|
|
75
|
+
if (r == null) continue; // pulou: segue com o que tem
|
|
76
|
+
const resposta = typeof r === 'number' ? q.opcoes[r] && q.opcoes[r].label : String(r).trim();
|
|
77
|
+
if (resposta) out.push({ campo: q.campo, pergunta: q.pergunta, resposta });
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Junta as respostas nos fatos. Puro. */
|
|
83
|
+
function aplicarRespostas(b, respostas) {
|
|
84
|
+
const fatos = { ...b.fatos, datas: [...b.fatos.datas], nomes: [...b.fatos.nomes], outros: [...b.fatos.outros] };
|
|
85
|
+
for (const r of respostas) {
|
|
86
|
+
if (r.campo === 'data') fatos.datas.push(r.resposta);
|
|
87
|
+
else if (r.campo === 'marca') fatos.marca = fatos.marca || r.resposta;
|
|
88
|
+
else if (r.campo === 'local') fatos.local = fatos.local || r.resposta;
|
|
89
|
+
else if (r.campo === 'oferta') fatos.oferta = fatos.oferta || r.resposta;
|
|
90
|
+
else fatos.outros.push(`${r.campo}: ${r.resposta}`);
|
|
91
|
+
}
|
|
92
|
+
return { ...b, fatos, respostas };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} pedido
|
|
97
|
+
* @param {object} deps { llm, escolher, fotos } `fotos` = caminhos já achados no pedido (referencias.imagens())
|
|
98
|
+
* @returns {{ok, briefing, perguntas, respostas}}
|
|
99
|
+
*/
|
|
100
|
+
async function ajustarPedido(pedido, deps = {}) {
|
|
101
|
+
const llm = deps.llm;
|
|
102
|
+
if (!llm) return { ok: false, error: 'sem modelo para ajustar o pedido.' };
|
|
103
|
+
const fotos = lista(deps.fotos);
|
|
104
|
+
const j = await pedirJson(llm, [
|
|
105
|
+
{ role: 'system', content: SISTEMA },
|
|
106
|
+
{ role: 'user', content: `Pedido do usuário: "${txt(pedido, 600)}"\n${fotos.length ? `Fotos que o usuário já mandou: ${fotos.length}.` : 'O usuário não mandou foto nenhuma.'}` },
|
|
107
|
+
], { temperatura: 0.7 });
|
|
108
|
+
let b = normalizar(j, pedido);
|
|
109
|
+
// virada comprida costuma carregar fato inventado: pede de novo UMA vez, com o defeito dito
|
|
110
|
+
const fato = fatoNaVirada(b, pedido);
|
|
111
|
+
const defeito = validar(b).find((e) => /virada/.test(e) && e !== 'sem virada') || (fato.length ? `a resposta da virada traz fato que o usuário não disse (${fato.join(', ')}); sem número, data, preço, nome ou promessa` : null);
|
|
112
|
+
if (defeito) {
|
|
113
|
+
const j2 = await pedirJson(llm, [
|
|
114
|
+
{ role: 'system', content: SISTEMA },
|
|
115
|
+
{ role: 'user', content: `Pedido do usuário: "${txt(pedido, 600)}"` },
|
|
116
|
+
{ role: 'assistant', content: JSON.stringify(j) },
|
|
117
|
+
{ role: 'user', content: `Defeito: ${defeito}. A resposta da virada tem de 1 a 4 palavras e não afirma nada novo sobre o negócio (uma palavra que quebra a expectativa daquele assunto). Devolva o JSON inteiro de novo.` },
|
|
118
|
+
], { temperatura: 0.5 }).catch(() => null);
|
|
119
|
+
if (j2) b = normalizar(j2, pedido);
|
|
120
|
+
// ainda sujo: a resposta fica só com o que sobra depois do filtro (ou o briefing cai)
|
|
121
|
+
const ainda = fatoNaVirada(b, pedido);
|
|
122
|
+
if (ainda.length) {
|
|
123
|
+
const [perg, resp] = b.virada.split(/->|→/);
|
|
124
|
+
const l = Fatos.limpar(resp, Fatos.fonte(pedido));
|
|
125
|
+
if (l.frase) b.virada = `${perg.trim()} -> ${l.frase.replace(/\*/g, '')}`;
|
|
126
|
+
else return { ok: false, error: `briefing incompleto: virada com fato inventado (${ainda.join(', ')})`, briefing: b };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (fotos.length) b.faltando = b.faltando.filter((q) => q.campo !== 'foto');
|
|
130
|
+
const erros = validar(b);
|
|
131
|
+
if (erros.some((e) => /virada|objetivo/.test(e))) return { ok: false, error: `briefing incompleto: ${erros.join(', ')}`, briefing: b };
|
|
132
|
+
const perguntas = b.faltando;
|
|
133
|
+
const respostas = await perguntar(perguntas, deps.escolher);
|
|
134
|
+
if (respostas.length) b = aplicarRespostas(b, respostas);
|
|
135
|
+
b.fotos = fotos;
|
|
136
|
+
return { ok: true, briefing: b, perguntas, respostas, puladas: perguntas.length - respostas.length };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { fatoNaVirada, ajustarPedido, normalizar, validar, perguntar, aplicarRespostas, MAX_PERGUNTAS, SISTEMA };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* critico.js — só a parte MECÂNICA do crítico do motion 2.0 (copiada de primocode-motion-2, motion2/critico.js,
|
|
3
|
+
* sem depender daquela branch): folha de contato, flash branco e imagem parada. O olho de visão não veio.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const ffmpeg = () => process.env.FFMPEG_PATH || 'ffmpeg';
|
|
11
|
+
const ffprobe = () => (process.env.FFMPEG_PATH ? process.env.FFMPEG_PATH.replace(/ffmpeg$/, 'ffprobe') : 'ffprobe');
|
|
12
|
+
|
|
13
|
+
function duracaoDe(video) {
|
|
14
|
+
const r = spawnSync(ffprobe(), ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', video], { encoding: 'utf8' });
|
|
15
|
+
return parseFloat(r.stdout) || 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** n quadros igualmente espaçados num mosaico (2 linhas). */
|
|
19
|
+
function folha(video, saida, { n = 8, largura = 270 } = {}) {
|
|
20
|
+
const d = duracaoDe(video);
|
|
21
|
+
if (!d) return { ok: false, error: 'não consegui medir o vídeo' };
|
|
22
|
+
const cols = Math.ceil(n / 2);
|
|
23
|
+
const r = spawnSync(ffmpeg(), ['-v', 'error', '-y', '-i', video, '-vf', `fps=${n}/${d.toFixed(3)},scale=${largura}:-1,tile=${cols}x2`, '-frames:v', '1', saida], { encoding: 'utf8' });
|
|
24
|
+
return { ok: r.status === 0 && fs.existsSync(saida), arquivo: saida, duracao: d, error: r.stderr };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** quadros 24x42 em cinza a 6 por segundo */
|
|
28
|
+
function luminancias(video, fps = 6) {
|
|
29
|
+
const r = spawnSync(ffmpeg(), ['-v', 'error', '-i', video, '-vf', `fps=${fps},scale=24:42,format=gray`, '-f', 'rawvideo', '-'], { maxBuffer: 1 << 28 });
|
|
30
|
+
const buf = r.stdout || Buffer.alloc(0), T = 24 * 42, q = [];
|
|
31
|
+
for (let i = 0; i + T <= buf.length; i += T) q.push(buf.subarray(i, i + T));
|
|
32
|
+
return q;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Contas sobre os quadros (puro, testável): flash branco (média > 235) e imagem parada por mais de 1 s. */
|
|
36
|
+
function contas(q, fps = 6) {
|
|
37
|
+
const motivos = [];
|
|
38
|
+
if (!q.length) return { ok: false, motivos: ['não li o vídeo'], quadros: 0 };
|
|
39
|
+
const media = (b) => b.reduce((a, v) => a + v, 0) / b.length;
|
|
40
|
+
// flash = quadro quase branco que se destaca dos outros: na pele creme do Destrava (≈ 239) TODO quadro é claro, e isso não é flash
|
|
41
|
+
const ms = q.map(media), ord = [...ms].sort((a, b) => a - b), mediana = ord[Math.floor(ord.length / 2)];
|
|
42
|
+
const brancos = ms.map((m, i) => (m > 235 && m - mediana > 8 ? i : -1)).filter((i) => i >= 0);
|
|
43
|
+
if (brancos.length) motivos.push(`flash branco perto de ${(brancos[0] / fps).toFixed(1)} s`);
|
|
44
|
+
let parado = 0, pior = 0;
|
|
45
|
+
for (let i = 3; i < q.length; i++) {
|
|
46
|
+
const a = q[i - 3], b = q[i];
|
|
47
|
+
let dif = 0;
|
|
48
|
+
for (let k = 0; k < a.length; k++) dif += Math.abs(a[k] - b[k]);
|
|
49
|
+
dif /= a.length;
|
|
50
|
+
parado = dif < 0.6 ? parado + 1 / fps : 0;
|
|
51
|
+
pior = Math.max(pior, parado);
|
|
52
|
+
}
|
|
53
|
+
if (pior > 1) motivos.push(`imagem parada por ${pior.toFixed(1)} s`);
|
|
54
|
+
return { ok: !motivos.length, motivos, quadros: q.length };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const mecanico = (video) => contas(luminancias(video));
|
|
58
|
+
|
|
59
|
+
module.exports = { folha, mecanico, contas, luminancias, duracaoDe };
|