primocode 9.10.0 → 10.0.9

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/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. package/voz/primo_voz/orbe.py +0 -406
@@ -0,0 +1,329 @@
1
+ /**
2
+ * auto.js — `primocode video auto`: o vídeo é o pedido.
3
+ *
4
+ * "Quero que ele leia o que eu falo no vídeo e aplique cada pedido no
5
+ * momento em que eu digo." (01/10)
6
+ *
7
+ * A pessoa grava falando o que quer ("separe em camadas", "coloca o logo do
8
+ * Claude aqui", "com o estalar de dedos me coloca numa praia") e o editor
9
+ * acha cada pedido na transcrição e o ancora no que o rastro viu: a palavra,
10
+ * a mão que aparece logo depois, o estalo que vem em seguida, o juntar das
11
+ * mãos. Nada de quadro escrito à mão.
12
+ *
13
+ * planejar(dados) → [{op, args(segundos da origem), motivo}] pura (o teste roda)
14
+ * OPS.auto.run → junta os dados, executa o plano, devolve o que fez
15
+ *
16
+ * Tudo em SEGUNDOS DA ORIGEM até a hora de executar: os cortes (silêncio,
17
+ * fim) vão por último e a legenda depois deles, já nos cortes.
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const PED = require('./pedido.js');
23
+
24
+ const tira = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
25
+ const r2 = (n) => Math.round(n * 100) / 100;
26
+
27
+ /* O que o Whisper ouve no lugar do que a pessoa disse — visto nos dois
28
+ primeiros vídeos de verdade: "cloud", "Claudio", "xgbt", "estravar",
29
+ "instruar de dedos". Normaliza antes de qualquer regra. */
30
+ const OUVIDO = [
31
+ [/\b(claudio|claudia|cloud|clode|clod|claud|cloude)\b/g, 'claude'],
32
+ [/\b(x ?gbt|x ?gpt|chat ?gbt|chat ?gpt|chat ?jpt|tchat ?gpt|chatgbt|gbt)\b/g, 'chatgpt'],
33
+ [/\bgoogle\s+(chrome|drive)\b/g, '$1'],
34
+ [/\b(estr[ae]l\w*|estrav\w*|estal\w*|instru\w*|estral\w*)\s+(?:os|nos|dos|de|o|do)?\s*dedos?\b/g, 'estalar o dedo'],
35
+ [/\b(estal\w*|estral\w*)\b/g, 'estalar'],
36
+ ];
37
+ function normal(t) {
38
+ let s = tira(t).replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim();
39
+ for (const [re, por] of OUVIDO) s = s.replace(re, por);
40
+ return s;
41
+ }
42
+
43
+ /* "colocar um foguete aqui na palma da minha mão": qualquer coisa na mão, não
44
+ só as logos. Vira o modelo, se houver, ou a placa com o nome. */
45
+ const NA_MAO = /\b(?:coloc\w*|po[er]\w*|bot\w*|aparec\w*)\s+(?:um|uma|o|a)\s+([a-z]{3,20})(?:\s+[a-z]{3,20})?\s+(?:aqui\s+)?(?:na|em|sobre|nas)\s+(?:a\s+)?(?:palma|mao|maos)\b/g;
46
+ const NAO_OBJETO = /^(logo|imagem|foto|fundo|legenda|praia|cerebro|video|mao|coisa)$/;
47
+ function objetoNaMao(t) {
48
+ const out = [];
49
+ let m;
50
+ NA_MAO.lastIndex = 0;
51
+ while ((m = NA_MAO.exec(t))) if (!NAO_OBJETO.test(m[1])) out.push({ object: m[1], pos: m.index, mao: null });
52
+ return out;
53
+ }
54
+
55
+ /** As palavras viram frases: ponto final, ou pausa de mais de 0,7 s. */
56
+ function frases(palavras) {
57
+ const out = [];
58
+ let atual = [];
59
+ const fecha = () => { if (atual.length) out.push({ ws: atual, de: atual[0].de, ate: atual[atual.length - 1].ate }); atual = []; };
60
+ palavras.forEach((p, i) => {
61
+ const ant = palavras[i - 1];
62
+ if (ant && p.de - ant.ate > 0.7) fecha();
63
+ atual.push(p);
64
+ if (/[.!?]$/.test(String(p.texto).trim())) fecha();
65
+ });
66
+ fecha();
67
+ for (const f of out) f.t = normal(f.ws.map((w) => w.texto).join(' '));
68
+ return out;
69
+ }
70
+
71
+ /** O segundo em que a frase diz `re` (a primeira palavra que casa), ou o começo dela. */
72
+ function quando(f, re) {
73
+ for (let i = 0; i < f.ws.length; i++) {
74
+ const um = normal(f.ws[i].texto);
75
+ const dois = normal(f.ws.slice(i, i + 3).map((w) => w.texto).join(' '));
76
+ if (re.test(um) || (re.test(dois) && !re.test(normal(f.ws.slice(i + 1, i + 3).map((w) => w.texto).join(' '))))) return f.ws[i].de;
77
+ }
78
+ return f.de;
79
+ }
80
+
81
+ const RE = {
82
+ camadas: /\bcamadas?\b|\btres dimens|\b(tir\w*|remov\w*|recort\w*|separ\w*)\s+(?:me\s+)?(?:o\s+|do\s+|meu\s+)?fundo\b|\bme\s+(tir\w*|recort\w*)\s+do\s+fundo\b/,
83
+ volta: /\bvolt\w*\b|\bnormal\b/,
84
+ estalo: /\bestalar\b/,
85
+ juntar: /\bjunt\w*\b|\buni\w*\b/,
86
+ cerebro: /\bcerebro\b/,
87
+ silencio: /\bsilencios?\b/,
88
+ fim: /\b(encerr|acab|termin|finaliz)\w*\b/,
89
+ legenda: /\blegendas?\b/,
90
+ acima: /\b(em cima|acima|encima|sobre|por cima)\s+da\s+(?:minha\s+)?cabeca\b|\baqui em cima\b|\bla em cima\b/,
91
+ imagem: /\b(?:imagem|foto|fundo)\s+(?:de|do|da)\s+(?:(?:um|uma)\s+)?([a-z]{3,20})\b/,
92
+ };
93
+
94
+ /** O fundo pedido na frase: um lugar conhecido, ou "imagem de um tubarão". */
95
+ function fundoPedido(t) {
96
+ const l = PED.lugar(t);
97
+ if (l && !/^(tras|atras|aqui)$/.test(l)) return l;
98
+ const m = t.match(RE.imagem);
99
+ return m && !/^(tras|atras|aqui|novo|nova)$/.test(m[1]) ? m[1] : null;
100
+ }
101
+
102
+ /**
103
+ * dados: {palavras:[{texto,de,ate}], estalos:[{t,score,mao}], juntaAbre:[{junta,abre}],
104
+ * maos:{left:[[de,ate]],right:[[de,ate]]}, silencios:[{de,ate}], dur} — tudo em segundos.
105
+ * → {passos:[{op,args,motivo}], legenda:{color?,position?,desde?}}
106
+ */
107
+ function planejar(d) {
108
+ const fs = frases(d.palavras || []);
109
+ const passos = [];
110
+ const bons = (d.estalos || []).filter((e) => e.mao || e.score >= 20);
111
+ const usados = new Set();
112
+ /* O estalo da janela é o MAIS FORTE dela, não o primeiro: o dedo que se
113
+ prepara faz um "tec" fraco antes do estalo de verdade (visto: 10,3 s
114
+ com 17 antes do 11,8 s com 21). */
115
+ const estaloDepois = (t, janela) => {
116
+ const c = bons.filter((x) => x.t >= t - 0.2 && x.t <= t + janela && !usados.has(x.t)).sort((a, b) => b.score - a.score)[0];
117
+ if (!c) return undefined;
118
+ usados.add(c.t);
119
+ return c.t;
120
+ };
121
+ const ehPedido = (f) => RE.camadas.test(f.t) || !!fundoPedido(f.t) || PED.objetosCitados(f.t).length || RE.juntar.test(f.t) || RE.silencio.test(f.t) || RE.fim.test(f.t);
122
+
123
+ // 1. As camadas em 3D: da palavra até o "volta" (ou o próximo pedido), 2,5–5 s.
124
+ const iCam = fs.findIndex((f) => RE.camadas.test(f.t) && !fundoPedido(f.t));
125
+ if (iCam >= 0) {
126
+ const de = Math.max(0, quando(fs[iCam], /camada|fundo|tir|recort|separ/) - 0.2);
127
+ const prox = fs.slice(iCam + 1).find((f) => f.de > de + 1 && (RE.volta.test(f.t) || ehPedido(f)));
128
+ const ate = Math.min(d.dur, de + 5, Math.max(de + 2.5, prox ? prox.de : de + 4));
129
+ passos.push({ op: 'separate_layers', args: { show3d: true, from: r2(de), to: r2(ate) }, motivo: `"${fs[iCam].t}"` });
130
+ }
131
+
132
+ // 2. Os objetos: na palavra, na mão que aparece logo ali (a outra, se a primeira já tem um).
133
+ const objetos = [];
134
+ const maoEm = (t, ocupadas) => {
135
+ const cand = [];
136
+ for (const lado of ['left', 'right']) {
137
+ for (const [a, b] of (d.maos && d.maos[lado]) || []) {
138
+ if (b < t - 0.2 || a > t + 3) continue;
139
+ cand.push({ lado, a: Math.max(a, t), b });
140
+ }
141
+ }
142
+ cand.sort((x, y) => (ocupadas.has(x.lado) - ocupadas.has(y.lado)) || x.a - y.a);
143
+ return cand[0] || null;
144
+ };
145
+ for (const f of fs) {
146
+ for (const o of [...PED.objetosCitados(f.t), ...objetoNaMao(f.t)]) {
147
+ const nome = o.object === 'chatgpt' ? /chatgpt|gpt/ : new RegExp(o.object.slice(0, 5));
148
+ const t = quando(f, nome);
149
+ if (objetos.some((x) => x.object === o.object && Math.abs(x.t - t) < 4)) continue;
150
+ const ocupadas = new Set(objetos.filter((x) => x.ate === null || x.ate > t).map((x) => x.mao));
151
+ const m = maoEm(t, ocupadas);
152
+ const mao = o.mao === 'left' || o.mao === 'right' ? o.mao : m ? m.lado : (ocupadas.has('left') ? 'right' : 'auto');
153
+ objetos.push({ object: o.object, t, de: m && m.lado === mao ? m.a : t, mao, ate: null, motivo: `"${f.t}"` });
154
+ }
155
+ }
156
+
157
+ // 3. Juntar as mãos (com objeto na mão): a junção que vem depois da frase; o cérebro se for dito.
158
+ let fusao = null;
159
+ if (objetos.length) {
160
+ const iJ = fs.findIndex((f) => (RE.juntar.test(f.t) || RE.cerebro.test(f.t)) && f.ate > objetos[0].t);
161
+ if (iJ >= 0) {
162
+ const f = fs[iJ];
163
+ const ev = (d.juntaAbre || []).find((e) => e.junta >= f.de - 1.5 && e.junta > objetos[0].de + 0.5);
164
+ if (ev) {
165
+ const cerebro = [f, fs[iJ + 1]].some((x) => x && RE.cerebro.test(x.t));
166
+ fusao = { junta: ev.junta, abre: ev.abre, then: cerebro ? 'brain' : 'none', motivo: `"${f.t}"` };
167
+ }
168
+ }
169
+ }
170
+ for (const o of objetos) {
171
+ o.ate = fusao && fusao.junta > o.de ? fusao.junta : Math.min(d.dur, o.de + 4);
172
+ if (o.ate > o.de + 0.3) passos.push({ op: 'attach_3d', args: { object: o.object, hand: o.mao, from: r2(o.de), to: r2(o.ate) }, motivo: o.motivo });
173
+ }
174
+ if (fusao) passos.push({ op: 'merge_on_join', args: { junta: r2(fusao.junta), ...(fusao.abre !== null ? { abre: r2(fusao.abre) } : {}), then: fusao.then }, motivo: fusao.motivo });
175
+
176
+ // 4. Fundos novos: no estalo, se a frase (ou a anterior) fala dele, ou se um estalo vem logo depois; senão na palavra.
177
+ const fundos = [];
178
+ fs.forEach((f, i) => {
179
+ const onde = fundoPedido(f.t);
180
+ if (!onde || !/\b(coloc\w*|poe|pon\w*|bot\w*|lev\w*|troc\w*|mud\w*|fundo|imagem|foto|me\s+em)\b/.test(f.t)) return;
181
+ const t = quando(f, new RegExp(onde.split(' ')[0].slice(0, 5)));
182
+ /* Um estalo logo depois do lugar é o gatilho, dito ou não. Se o estalo
183
+ foi dito ANTES do lugar ("se estalar os dedos ela coloca um
184
+ tubarão"), ele pode vir uns segundos depois da frase. */
185
+ const antes = f.t.slice(0, f.t.indexOf(onde.split(' ')[0]));
186
+ const falaEstalo = RE.estalo.test(antes) || (fs[i - 1] && RE.estalo.test(fs[i - 1].t) && !fundoPedido(fs[i - 1].t));
187
+ let e = estaloDepois(t, 3);
188
+ if (e === undefined && falaEstalo) e = estaloDepois(f.de, 10);
189
+ // "só aqui", "somente aqui": o fundo vale para a frase, depois volta o original.
190
+ const so = /\b(somente|so|apenas)\s+(aqui|agora|nessa parte|nesse momento)\b/.test(f.t) ? Math.min(d.dur, f.ate + 0.6) : null;
191
+ fundos.push({ with: onde, at: e !== undefined ? e : t, ...(so ? { ate: so } : {}), motivo: `"${f.t}"` + (e !== undefined ? ' (no estalo)' : '') });
192
+ });
193
+ fundos.forEach((b, i) => {
194
+ const until = b.ate !== undefined ? b.ate : fundos[i + 1] ? fundos[i + 1].at : null;
195
+ passos.push({ op: 'replace_background', args: { with: b.with, at: r2(b.at), ...(until !== null ? { until: r2(until) } : {}) }, motivo: b.motivo });
196
+ });
197
+
198
+ // 5. Legenda: sempre (é reels); a cor e a posição se forem pedidas, a partir do estalo se ele for citado.
199
+ const legenda = {};
200
+ const iL = fs.findIndex((f) => RE.legenda.test(f.t));
201
+ if (iL >= 0) {
202
+ const janela = fs.slice(iL, iL + 3);
203
+ const cor = janela.map((f) => PED.corPedida(f.t)).find(Boolean);
204
+ if (cor) legenda.color = cor;
205
+ const fp = janela.find((f) => RE.acima.test(f.t));
206
+ if (fp) {
207
+ legenda.position = 'above_head';
208
+ if (RE.estalo.test(fp.t)) { const e = estaloDepois(fp.de, 8); if (e !== undefined) legenda.desde = e; }
209
+ // "a legenda que tá aqui… aqui em cima": sobe quando ela diz onde.
210
+ else if (/\b(aqui|la) em cima\b/.test(fp.t)) legenda.desde = quando(fp, /cima/);
211
+ } else {
212
+ /* "a legenda daqui ficar aqui", apontando: o pedido é mudar de lugar, e
213
+ o lugar que se aponta em quem fala para a câmera é perto do rosto —
214
+ a legenda sobe para a cabeça no segundo "aqui". (01/10) */
215
+ const fd = janela.find((f) => /\bdaqui\b[\s\S]*\b(ficar|ir|pra|para)\b[\s\S]*\baqui\b|\b(mud|muda|mover|move|pass)\w*\b[\s\S]*\baqui\b/.test(f.t));
216
+ if (fd) {
217
+ legenda.position = 'above_head';
218
+ const aquis = fd.ws.filter((w) => /^aqui/.test(normal(w.texto)));
219
+ legenda.desde = (aquis[aquis.length - 1] || fd.ws[0]).de;
220
+ }
221
+ }
222
+ }
223
+
224
+ // 6. Cortes, por último: o silêncio que a pessoa cita, e o fim que ela anuncia.
225
+ const cortes = [];
226
+ const iS = fs.findIndex((f) => RE.silencio.test(f.t) && /\b(cort\w*|tir\w*|remov\w*|apag\w*|retir\w*)\b/.test(f.t));
227
+ if (iS >= 0) {
228
+ const t0 = fs[iS].de;
229
+ const sil = juntarSilencios(d.silencios || []).filter((s) => s.de >= t0 && s.de <= t0 + 15 && s.ate - s.de >= 1.5)
230
+ .sort((a, b) => (b.ate - b.de) - (a.ate - a.de))[0];
231
+ if (sil) cortes.push({ de: r2(sil.de + 0.1), ate: r2(sil.ate - 0.1), motivo: `"${fs[iS].t}" (${r2(sil.ate - sil.de)} s parado)` });
232
+ }
233
+ const iF = fs.findIndex((f) => RE.fim.test(f.t) && /\bvideo\b/.test(f.t));
234
+ if (iF >= 0) {
235
+ const f = fs[iF];
236
+ const ultima = fs[fs.length - 1].ate;
237
+ const e = RE.estalo.test(f.t) ? estaloDepois(f.de, 6) : undefined;
238
+ const fim = Math.max(ultima, e !== undefined ? e : 0) + 0.4;
239
+ if (d.dur - fim > 0.8) cortes.push({ de: r2(fim), ate: r2(d.dur), motivo: `"${f.t}"` });
240
+ }
241
+ if (cortes.length) passos.push({ op: 'cortes', args: { trechos: cortes.map(({ de, ate }) => [de, ate]) }, motivo: cortes.map((c) => c.motivo).join(' · ') });
242
+
243
+ return { passos, legenda };
244
+ }
245
+
246
+ /** Silêncios separados por um respiro (< 0,4 s) são um só. */
247
+ function juntarSilencios(sil) {
248
+ const out = [];
249
+ for (const s of [...sil].sort((a, b) => a.de - b.de)) {
250
+ const u = out[out.length - 1];
251
+ if (u && s.de - u.ate < 0.4) u.ate = Math.max(u.ate, s.ate);
252
+ else out.push({ de: s.de, ate: s.ate });
253
+ }
254
+ return out;
255
+ }
256
+
257
+ const OPS = {
258
+ auto: {
259
+ uso: 'auto {asset?,dryRun?} — O VÍDEO É O PEDIDO: lê o que a pessoa FALA na gravação ("separe em camadas", "coloca o logo do Claude aqui", "com o estalar de dedos me coloca numa praia", "junta e vira um cérebro", "cortou o silêncio", "o vídeo acaba") e aplica cada efeito no momento em que ela diz, ancorado na palavra, na mão, no estalo e no juntar das mãos; legenda sempre. Use logo depois do import quando pedirem "edite esse vídeo" sem dizer o quê',
260
+ async run(ctx, a) {
261
+ const T = require('./timeline.js');
262
+ const C = require('./camadas.js');
263
+ const R = require('./rastro.js');
264
+ const RT = require('./rastrear.js');
265
+ const TR = require('./transcript.js');
266
+ const FXO = require('./ops-fx.js');
267
+ const tl = ctx.tl;
268
+ if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
269
+ const asset = C.base(tl, a.asset);
270
+ const inicio = Date.now();
271
+ // O lento e independente junto: rastreio (MediaPipe em paralelo) e transcrição.
272
+ const [rs, tr] = await Promise.all([RT.garantir(ctx.p.dir, asset), TR.obter(ctx.p.dir, asset)]);
273
+ if (!rs.ok) return rs;
274
+ if (!tr.ok) return { ok: false, error: 'sem a fala não sei o que fazer: ' + tr.error };
275
+ const fps = rs.rastro.fps;
276
+ const trilhas = R.trilhasDasMaos(rs.rastro);
277
+ const trechos = (t) => R.trechosVisiveis(t, Math.round(fps * 0.3)).map(([x, y]) => [x / fps, y / fps]);
278
+ const dados = {
279
+ palavras: tr.palavras,
280
+ estalos: (await FXO.estalosDe(ctx, asset)).map((e) => ({ t: e.t, score: e.score, mao: e.mao })),
281
+ juntaAbre: R.juntaEAbre(trilhas, { rastro: rs.rastro }).map((e) => ({ junta: e.junta / fps, abre: e.abre === null ? null : e.abre / fps })),
282
+ maos: { left: trechos(trilhas.left), right: trechos(trilhas.right) },
283
+ silencios: await require('./render.js').silenciosDe(ctx.p.dir, asset, { threshold: -35, minSilence: 0.8 }),
284
+ dur: asset.seconds,
285
+ };
286
+ const plano = planejar(dados);
287
+ const q = (s) => C.origemParaQuadro(ctx.tl, asset.id, s);
288
+ const ver = plano.passos.map((p) => ({ op: p.op, motivo: p.motivo, args: p.args }));
289
+ if (a.dryRun) return { ok: true, plano: ver, legenda: plano.legenda, segundos: 'da gravação' };
290
+
291
+ const { OPS: TODAS } = require('./ops.js');
292
+ const feitos = [];
293
+ const roda = async (op, args, motivo) => {
294
+ const res = await TODAS[op].run(ctx, args);
295
+ feitos.push({ op, motivo, ok: !!res && res.ok !== false, ...(res && res.ok === false ? { error: res.error } : {}) });
296
+ return res;
297
+ };
298
+ for (const p of plano.passos) {
299
+ const g = p.args;
300
+ if (p.op === 'separate_layers') await roda(p.op, { show3d: true, from: q(g.from), to: q(g.to) }, p.motivo);
301
+ else if (p.op === 'attach_3d') await roda(p.op, { object: g.object, hand: g.hand, from: q(g.from), to: q(g.to) }, p.motivo);
302
+ else if (p.op === 'merge_on_join') await roda(p.op, { junta: q(g.junta), ...(g.abre !== undefined ? { abre: q(g.abre) } : {}), then: g.then }, p.motivo);
303
+ else if (p.op === 'replace_background') await roda(p.op, { with: g.with, at: q(g.at), ...(g.until !== undefined ? { until: q(g.until) } : {}) }, p.motivo);
304
+ else if (p.op === 'cortes') {
305
+ const ranges = g.trechos.map(([de, ate]) => [q(de), Math.min(T.duracao(ctx.tl), q(Math.min(ate, asset.seconds - 0.001)) + 1)]).filter(([x, y]) => x !== null && y > x);
306
+ if (ranges.length) await roda('ripple_delete_ranges', { ranges }, p.motivo);
307
+ }
308
+ }
309
+ // A legenda, já nos cortes; a posição a partir do estalo, se ele foi citado.
310
+ await roda('add_captions', { maxWords: 3, animation: 'highlightPop' }, 'legenda (reels)');
311
+ const L = plano.legenda;
312
+ if (L.color || L.position) {
313
+ const leg = T.todosOsClipes(ctx.tl).filter((x) => x.clip.caption).map((x) => x.clip);
314
+ const desde = L.desde !== undefined ? q(L.desde) : null;
315
+ if (L.color) await roda('style_captions', { color: L.color }, 'cor pedida');
316
+ const ids = leg.filter((c) => desde === null || c.startFrame >= desde).map((c) => c.id);
317
+ if (L.position && ids.length) await roda('style_captions', { ids, position: L.position }, desde !== null ? 'em cima da cabeça, desde o momento dito' : 'em cima da cabeça');
318
+ }
319
+ const falhou = feitos.filter((f) => !f.ok);
320
+ return { ok: !falhou.length, feitos, tookS: Math.round((Date.now() - inicio) / 100) / 10,
321
+ ...(falhou.length ? { error: falhou.map((f) => `${f.op}: ${f.error}`).join('; ') } : {}),
322
+ depois: 'inspect para OLHAR os momentos; export renderiza (o 3D sai no export)' };
323
+ },
324
+ },
325
+ };
326
+
327
+ const EX = { auto: '{}' };
328
+
329
+ module.exports = { planejar, frases, normal, fundoPedido, juntarSilencios, OPS, EX };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * camadas.js — as peças comuns das operações "de efeito" (separar, trocar
3
+ * fundo, objeto na mão, legenda na cabeça): achar a fala, espelhar clipes
4
+ * sobre ela, ordenar as faixas por papel e converter "o estalo", "quando eu
5
+ * falar camadas" ou um quadro em SEGUNDO DA ORIGEM.
6
+ *
7
+ * ── POR QUE TUDO MORA EM SEGUNDO DA ORIGEM ───────────────────────────────
8
+ * O que se rastreia (a mão, a cabeça, o estalo) aconteceu na GRAVAÇÃO. Se a
9
+ * receita de um efeito guardasse quadro da timeline, o primeiro corte de
10
+ * silêncio a deixaria apontando para o lugar errado. Guardando o segundo da
11
+ * origem, o efeito é redesenhado em cima da timeline que existir — e a
12
+ * camada gerada entra "espelhando" os clipes da fala: o corte que pega a
13
+ * fala pega a camada no mesmo quadro.
14
+ *
15
+ * ── AS FAIXAS TÊM PAPEL ──────────────────────────────────────────────────
16
+ * De cima para baixo: textos, objetos 3D, o momento das camadas, a pessoa
17
+ * recortada, o fundo novo e, por baixo de tudo, a gravação (que segue
18
+ * levando o áudio). `arrumar` impõe essa ordem depois de cada operação, em
19
+ * qualquer ordem que o pedido venha.
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const T = require('./timeline.js');
25
+
26
+ const ORDEM = { objetos: 1, camadas3d: 2, pessoa: 3, fundo: 4 };
27
+
28
+ /** A gravação: a mídia de vídeo pedida, ou a primeira com áudio que está na timeline. */
29
+ function base(tl, ref) {
30
+ if (ref) {
31
+ const a = T.acharAsset(tl, ref);
32
+ if (a.kind !== 'video' || a.gerado) throw T.erro(`a mídia ${a.id} não é uma gravação de vídeo`);
33
+ return a;
34
+ }
35
+ const usados = new Set(clipesBase(tl).map((c) => c.asset));
36
+ const lista = Object.values(tl.assets).filter((a) => a.kind === 'video' && !a.gerado && usados.has(a.id));
37
+ const achada = lista.find((a) => a.hasAudio) || lista[0];
38
+ if (!achada) throw T.erro('não há gravação de vídeo na timeline: comece com import {"path":"..."}');
39
+ return achada;
40
+ }
41
+
42
+ /** Os clipes da gravação "de verdade": faixa de vídeo sem papel. */
43
+ function clipesBase(tl, assetId) {
44
+ const out = [];
45
+ for (const t of tl.tracks) {
46
+ if (t.type !== 'video' || t.role) continue;
47
+ for (const c of t.clips) if (c.type === 'video' && (!assetId || c.asset === assetId) && !(tl.assets[c.asset] || {}).gerado) out.push(c);
48
+ }
49
+ return out.sort((a, b) => a.startFrame - b.startFrame);
50
+ }
51
+
52
+ /** Quadro da timeline → segundo da origem da gravação (ou null fora dela). */
53
+ function quadroParaOrigem(tl, assetId, f) {
54
+ for (const c of clipesBase(tl, assetId)) {
55
+ if (f >= c.startFrame && f < T.fim(c)) return T.segundos(tl, c.trimStartFrame + (f - c.startFrame) * (c.speed || 1));
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /** Segundo da origem → quadro da timeline (null se o trecho foi cortado). */
61
+ function origemParaQuadro(tl, assetId, seg) {
62
+ for (const c of clipesBase(tl, assetId)) {
63
+ const f = T.origemParaQuadro(tl, c, seg);
64
+ if (f !== null && f < T.fim(c)) return f;
65
+ }
66
+ // O fim exato da gravação (o "até o fim") cai no fim do último clipe.
67
+ const ult = clipesBase(tl, assetId).slice(-1)[0];
68
+ if (ult && Math.abs(T.janelaOrigem(tl, ult).ate - seg) < 0.5 / tl.fps) return T.fim(ult);
69
+ return null;
70
+ }
71
+
72
+ /** O clipe da gravação que está no quadro f (para saber o encaixe na tela). */
73
+ function clipeNoQuadro(tl, assetId, f) {
74
+ return clipesBase(tl, assetId).find((c) => f >= c.startFrame && f < T.fim(c)) || null;
75
+ }
76
+
77
+ function arrumar(tl) {
78
+ const rank = (t) => (t.type === 'text' ? 0 : t.type === 'audio' ? 9 : ORDEM[t.role] || 5);
79
+ tl.tracks = tl.tracks.map((t, i) => ({ t, i })).sort((a, b) => rank(a.t) - rank(b.t) || a.i - b.i).map((x) => x.t);
80
+ return tl;
81
+ }
82
+
83
+ /** A faixa com aquele papel (cria se não houver). Devolve o índice já na ordem certa. */
84
+ function faixa(tl, papel) {
85
+ let t = tl.tracks.find((x) => x.role === papel);
86
+ if (!t) {
87
+ const i = T.faixaPara(tl, 'video', 'new');
88
+ t = tl.tracks[i];
89
+ t.role = papel;
90
+ }
91
+ arrumar(tl);
92
+ return tl.tracks.indexOf(t);
93
+ }
94
+
95
+ /** Tira os clipes de um papel (de uma gravação), e a faixa se esvaziar. */
96
+ function limparPapel(tl, papel, assetId) {
97
+ for (const t of tl.tracks) {
98
+ if (t.role !== papel) continue;
99
+ t.clips = t.clips.filter((c) => assetId && c.base !== assetId);
100
+ }
101
+ tl.tracks = tl.tracks.filter((t) => t.role !== papel || t.clips.length);
102
+ }
103
+
104
+ /** Apaga os assets gerados que nenhum clipe usa mais. */
105
+ function limparGerados(tl) {
106
+ const usados = new Set(T.todosOsClipes(tl).map((x) => x.clip.asset));
107
+ for (const [id, a] of Object.entries(tl.assets)) if (a.gerado && !usados.has(id)) delete tl.assets[id];
108
+ }
109
+
110
+ /**
111
+ * Cria, na faixa `ti`, um clipe por clipe da gravação que cai em [de, ate)
112
+ * (segundos da origem), no MESMO lugar da timeline. `asset`: a mídia do
113
+ * clipe novo; `origemDe`: o segundo da gravação que corresponde ao 0 dela
114
+ * (a própria gravação: 0; uma camada gerada a partir de `de`: `de`).
115
+ */
116
+ function espelhar(tl, assetId, ti, { de = 0, ate = Infinity, asset = assetId, origemDe = 0, props = {}, papel } = {}) {
117
+ const criados = [];
118
+ for (const c of clipesBase(tl, assetId)) {
119
+ const j = T.janelaOrigem(tl, c);
120
+ const i0 = Math.max(j.de, de);
121
+ const i1 = Math.min(j.ate, ate);
122
+ if (i1 - i0 < 0.5 / tl.fps) continue;
123
+ const s = T.origemParaQuadro(tl, c, i0);
124
+ const e = Math.min(T.fim(c), T.origemParaQuadro(tl, c, i1));
125
+ if (s === null || e === null || e <= s) continue;
126
+ const o = s - c.startFrame;
127
+ const kf = {};
128
+ for (const [k, keys] of Object.entries(c.keyframes || {})) if (k !== 'volume') kf[k] = keys.map((x) => ({ ...x, frame: x.frame - o }));
129
+ const novo = T.clipe(tl, {
130
+ type: 'video', asset, startFrame: s, durationFrames: e - s,
131
+ trimStartFrame: Math.max(0, Math.round((i0 - origemDe) * tl.fps)), speed: c.speed,
132
+ transform: { ...c.transform }, crop: { ...c.crop }, fit: c.fit, volume: 0, ...props,
133
+ });
134
+ novo.keyframes = kf;
135
+ novo.base = assetId;
136
+ if (papel) novo.papel = papel;
137
+ tl.tracks[ti].clips.push(novo);
138
+ criados.push(novo);
139
+ }
140
+ T.ordenar(tl);
141
+ return criados;
142
+ }
143
+
144
+ // ── os momentos ──────────────────────────────────────────────────────────
145
+
146
+ const sem = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '');
147
+
148
+ /** A primeira palavra da transcrição que casa (sem acento, sem espaço: "chat gpt" casa "chatgpt"). */
149
+ function acharPalavra(palavras, alvo, { desde = 0 } = {}) {
150
+ const a = sem(alvo);
151
+ if (!a) return null;
152
+ for (let i = 0; i < palavras.length; i++) {
153
+ const p = palavras[i];
154
+ if (p.de < desde) continue;
155
+ const um = sem(p.texto);
156
+ const dois = um + sem((palavras[i + 1] || {}).texto);
157
+ if (um.startsWith(a) || (a.length > 4 && um.length >= 4 && a.startsWith(um) && dois.startsWith(a))) return p;
158
+ }
159
+ return null;
160
+ }
161
+
162
+ /** Cor por nome (português ou inglês) → hex. Hex passa direto. */
163
+ const CORES = {
164
+ azul: '#2F80FF', blue: '#2F80FF', 'azul claro': '#5AC8FA', ciano: '#22D3EE', cyan: '#22D3EE',
165
+ vermelho: '#EF4444', vermelha: '#EF4444', red: '#EF4444', verde: '#22C55E', green: '#22C55E',
166
+ amarelo: '#FFD400', amarela: '#FFD400', yellow: '#FFD400', laranja: '#FF8A00', orange: '#FF8A00',
167
+ rosa: '#FF4FA3', pink: '#FF4FA3', roxo: '#8B5CF6', roxa: '#8B5CF6', purple: '#8B5CF6', lilas: '#C084FC',
168
+ branco: '#FFFFFF', branca: '#FFFFFF', white: '#FFFFFF', preto: '#000000', preta: '#000000', black: '#000000',
169
+ dourado: '#F5C542', dourada: '#F5C542', gold: '#F5C542', cinza: '#9CA3AF', gray: '#9CA3AF',
170
+ };
171
+ function cor(nome) {
172
+ if (nome === undefined || nome === null) return undefined;
173
+ const s = String(nome).trim();
174
+ if (/^#?[0-9a-f]{6}$/i.test(s)) return s.startsWith('#') ? s.toUpperCase() : '#' + s.toUpperCase();
175
+ const k = s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
176
+ if (CORES[k]) return CORES[k];
177
+ throw T.erro(`cor "${nome}" (use #RRGGBB ou azul/vermelho/verde/amarelo/laranja/rosa/roxo/ciano/branco/preto/dourado/cinza)`);
178
+ }
179
+
180
+ /** Fundo pedido por palavra → termo de busca que acha foto boa (o acervo é em inglês). */
181
+ const FUNDOS = {
182
+ praia: 'tropical beach turquoise sea', beach: 'tropical beach turquoise sea', mar: 'ocean waves', espaco: 'outer space nebula stars',
183
+ space: 'outer space nebula stars', cidade: 'city skyline night', floresta: 'green forest', montanha: 'mountain landscape',
184
+ escritorio: 'modern office interior', deserto: 'desert dunes', neve: 'snowy mountains', estudio: 'dark studio backdrop',
185
+ paris: 'paris eiffel tower', 'nova york': 'new york skyline', por_do_sol: 'sunset sky', 'por do sol': 'sunset sky',
186
+ };
187
+ function termoDeFundo(pedido) {
188
+ const k = String(pedido || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim();
189
+ return FUNDOS[k] || FUNDOS[k.replace(/^(uma?|o|a|na|no|numa|num)\s+/, '')] || k;
190
+ }
191
+
192
+ /** Objeto pedido → modelo do fx3d/modelos.jsx. */
193
+ function modeloDe(nome) {
194
+ const k = sem(nome);
195
+ if (/^(chatgpt|gpt|openai|chat)/.test(k)) return 'chatgpt';
196
+ if (/^(claude|claud|anthropic)/.test(k)) return 'claude';
197
+ if (/^(brain|cerebro)/.test(k)) return 'cerebro';
198
+ if (/^(chrome|google ?chrome|navegador)/.test(k)) return 'chrome';
199
+ if (/^(drive|google ?drive)/.test(k)) return 'drive';
200
+ if (/^(foguete|rocket|nave)/.test(k)) return 'foguete';
201
+ return null;
202
+ }
203
+
204
+ module.exports = {
205
+ ORDEM, base, clipesBase, quadroParaOrigem, origemParaQuadro, clipeNoQuadro, arrumar, faixa, limparPapel, limparGerados,
206
+ espelhar, acharPalavra, sem, cor, CORES, termoDeFundo, FUNDOS, modeloDe,
207
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * cli.js — `primocode video <op> [--json '{...}'] [--project <id>]`.
3
+ *
4
+ * O editor entra pelo run_shell que o agente JÁ tem: as ferramentas do
5
+ * modelo são declaradas pelo servidor, e um editor que precisasse de tool
6
+ * nova no servidor só existiria depois de um deploy de lá. Um subcomando que
7
+ * se descreve (`help`) funciona hoje, com qualquer modelo.
8
+ *
9
+ * Contrato da saída: UMA linha de JSON no stdout (o que o modelo lê) e, no
10
+ * máximo, uma linha curta para gente no stderr. Código de saída 1 quando
11
+ * ok:false.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const T = require('./timeline.js');
17
+ const P = require('./projeto.js');
18
+ const { OPS, ALIAS, ajuda } = require('./ops.js');
19
+
20
+ function lerArgs(argv) {
21
+ let op = argv[0];
22
+ let json = null;
23
+ let projeto = null;
24
+ for (let i = 1; i < argv.length; i++) {
25
+ if (argv[i] === '--json' || argv[i] === '-j') json = argv[++i];
26
+ else if (argv[i] === '--project' || argv[i] === '-p') projeto = argv[++i];
27
+ else if (json === null && /^\s*\{/.test(argv[i])) json = argv[i];
28
+ }
29
+ let args = {};
30
+ if (json) {
31
+ try { args = JSON.parse(json); } catch (e) { throw T.erro(`--json inválido: ${e.message}`); }
32
+ }
33
+ if (projeto) args.project = projeto;
34
+ return { op, args };
35
+ }
36
+
37
+ function humano(res) {
38
+ if (!res || res.ok === false) return `erro: ${res && res.error}`;
39
+ const partes = [];
40
+ if (res.added) partes.push(`+${res.added.length}`);
41
+ if (res.removed) partes.push(`-${res.removed.length}`);
42
+ if (res.changed) partes.push(`~${res.changed.length}`);
43
+ if (res.shifted) partes.push(`${res.shifted.reduce((s, x) => s + x.ids.length, 0)} deslocados`);
44
+ if (res.dur !== undefined && res.fps === undefined) partes.push(`dur ${res.dur}q`);
45
+ if (res.file) partes.push(res.file);
46
+ if (res.png) partes.push(res.png);
47
+ return partes.join(' ') || 'ok';
48
+ }
49
+
50
+ async function main(argv, { out = process.stdout, err = process.stderr } = {}) {
51
+ let res;
52
+ try {
53
+ const { op, args } = lerArgs(argv);
54
+ if (!op || op === 'help' || op === '--help' || op === '-h') {
55
+ out.write(ajuda(argv[1]) + '\n');
56
+ return 0;
57
+ }
58
+ const nome = ALIAS[op] || op;
59
+ const def = OPS[nome];
60
+ if (!def) throw T.erro(`operação "${op}" não existe — rode: primocode video help`);
61
+ const aberto = nome === 'new' ? null : P.abrir(args.project);
62
+ if (args.project && !aberto && nome !== 'new') throw T.erro(`não há projeto "${args.project}"`);
63
+ const ctx = aberto ? { p: aberto.p, tl: aberto.tl } : { p: null, tl: null };
64
+ res = await def.run(ctx, args);
65
+ } catch (e) {
66
+ res = { ok: false, error: e.doEditor ? e.message : `${e.name}: ${e.message}` };
67
+ if (!e.doEditor && process.env.PRIMOCODE_DEBUG) err.write(e.stack + '\n');
68
+ }
69
+ out.write(JSON.stringify(res) + '\n');
70
+ err.write(` video: ${humano(res)}\n`);
71
+ return res && res.ok === false ? 1 : 0;
72
+ }
73
+
74
+ module.exports = { main, lerArgs, humano };