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.
Files changed (89) hide show
  1. package/LICENSES/edit2-terceiros.md +31 -0
  2. package/LICENSES/models-dev-MIT.txt +25 -0
  3. package/README.md +2 -2
  4. package/editor/SKILL.md +24 -0
  5. package/editor/THIRD_PARTY_NOTICES.md +1 -0
  6. package/editor/assets/baixar.json +9 -1
  7. package/editor/helpers/df_limpar.py +29 -0
  8. package/editor/helpers/edit2.py +155 -0
  9. package/editor/helpers/enquadrar_camera.py +77 -0
  10. package/editor/helpers/face_layout.py +20 -0
  11. package/editor/helpers/face_mediapipe.py +147 -0
  12. package/editor/helpers/instalar_df.sh +7 -0
  13. package/editor/helpers/legenda_ass.py +124 -0
  14. package/editor/helpers/look_ffmpeg.py +105 -0
  15. package/editor/helpers/motor_palavras.py +157 -0
  16. package/editor/helpers/primeiro_passe.py +214 -0
  17. package/editor/helpers/som_edit2.py +119 -0
  18. package/editor/helpers/transcribe.py +31 -2
  19. package/editor/pyproject.toml +9 -0
  20. package/editor/references/edit2.md +22 -0
  21. package/editor/uv.lock +3342 -214
  22. package/kits/3d/LICENSES/README.md +11 -0
  23. package/kits/3d/animacoes.js +87 -0
  24. package/kits/3d/base.js +44 -0
  25. package/kits/3d/catalogo.js +38 -0
  26. package/kits/3d/cenario.js +162 -0
  27. package/kits/3d/exemplos/asfalto.mjs +91 -0
  28. package/kits/3d/maquinas.js +97 -0
  29. package/kits/3d/montador.js +142 -0
  30. package/kits/3d/package.json +1 -0
  31. package/kits/3d/personagem.js +86 -0
  32. package/kits/3d/remotion/Maquete.jsx +42 -0
  33. package/kits/3d/remotion/Root.jsx +10 -0
  34. package/kits/3d/render.mjs +73 -0
  35. package/lib/cli/comando-api.js +30 -8
  36. package/lib/cli/modelos-lista.js +58 -0
  37. package/lib/cli/sessao.js +11 -0
  38. package/lib/cli/subcomandos.js +3 -2
  39. package/lib/outra-pasta.js +1 -1
  40. package/lib/permissao-sistema.js +60 -0
  41. package/lib/permission/perigo.js +2 -1
  42. package/lib/permission/permissao.js +1 -0
  43. package/lib/permission/sem-perguntar.js +29 -0
  44. package/lib/plugin/desktop.js +5 -4
  45. package/lib/plugin/estudio/cota.js +4 -2
  46. package/lib/plugin/estudio/dialogo3d.js +71 -0
  47. package/lib/plugin/estudio/effectcraft.js +1 -1
  48. package/lib/plugin/estudio/glifos.js +2 -0
  49. package/lib/plugin/estudio/gsap-local.js +43 -0
  50. package/lib/plugin/estudio/index.js +63 -10
  51. package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
  52. package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
  53. package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
  54. package/lib/plugin/estudio/maquina-diretor.js +1 -1
  55. package/lib/plugin/estudio/maquina-elementos.js +2 -2
  56. package/lib/plugin/estudio/maquina.js +26 -8
  57. package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
  58. package/lib/plugin/estudio/motion-plano/critico.js +59 -0
  59. package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
  60. package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
  61. package/lib/plugin/estudio/motion-plano/index.js +75 -0
  62. package/lib/plugin/estudio/motion-plano/llm.js +59 -0
  63. package/lib/plugin/estudio/motion-plano/motor.js +50 -0
  64. package/lib/plugin/estudio/motion-plano/passos.js +38 -0
  65. package/lib/plugin/estudio/motion-plano/pele.js +36 -0
  66. package/lib/plugin/estudio/motion-plano/plano.js +152 -0
  67. package/lib/plugin/estudio/motion-plano/regua.js +100 -0
  68. package/lib/plugin/estudio/personagem.js +3 -1
  69. package/lib/plugin/estudio/referencias.js +1 -1
  70. package/lib/provider/catalogo.js +161 -0
  71. package/lib/provider/local/catalogo-embutido.json.gz +0 -0
  72. package/lib/provider/local/cerebro.json +1 -1
  73. package/lib/provider/local/entrada.js +10 -0
  74. package/lib/provider/local/motor.js +140 -27
  75. package/lib/provider/local/orcamento.js +193 -0
  76. package/lib/provider/local/provedor.js +127 -14
  77. package/lib/provider/local/recorte.js +170 -0
  78. package/lib/provider/local/revezamento.js +121 -0
  79. package/lib/session/runner/act.js +28 -3
  80. package/lib/skill/catalogo.js +9 -1
  81. package/lib/tool/reparar-entrada.js +1 -0
  82. package/lib/tool/tools.js +58 -1
  83. package/lib/tui/ui.js +1 -1
  84. package/package.json +2 -2
  85. package/vendor/fontes/ttf/ORIGEM.txt +1 -0
  86. package/vendor/fontes/ttf/inter-800.ttf +0 -0
  87. package/vendor/gsap/ORIGEM.txt +6 -0
  88. package/vendor/gsap/gsap.min.js +11 -0
  89. 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
- const el = await reunir({ assunto, pedidos, meta, pasta, rede: !semRede, logos: args.logos, site: args.site, minimo: 3, max: 6 });
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 };