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,453 @@
1
+ /**
2
+ * ops-fx.js — as operações "de efeito" do editor: rastrear, separar em
3
+ * camadas, trocar o fundo no estalo, objeto 3D na mão, fusão ao juntar as
4
+ * mãos, cortar o silêncio do estalo e a legenda que segue a cabeça.
5
+ *
6
+ * Toda operação aqui escreve RECEITA na timeline (segundos da origem) e
7
+ * passa pelo mesmo `mutar` das outras: foto de antes, validação, delta,
8
+ * undo. O que é pesado — rastrear (uma vez por mídia, em paralelo) e
9
+ * renderizar o 3D (no export, pelo hash) — fica fora do caminho da edição.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const T = require('./timeline.js');
17
+ const E = require('./edits.js');
18
+ const C = require('./camadas.js');
19
+ const R = require('./rastro.js');
20
+ const RT = require('./rastrear.js');
21
+ const TR = require('./transcript.js');
22
+ const FX = require('./fx3d.js');
23
+ const video = require('../estudio/video.js');
24
+
25
+ const r = () => require('./render.js');
26
+ const lista = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
27
+ const r2 = T.r2;
28
+
29
+ // ── medidas (com cache) ──────────────────────────────────────────────────
30
+
31
+ async function rastro(ctx, asset) {
32
+ const rs = await RT.garantir(ctx.p.dir, asset);
33
+ if (!rs.ok) throw Object.assign(T.erro(rs.error), { extra: rs });
34
+ return rs;
35
+ }
36
+
37
+ /** As palavras da gravação (a transcrição em cache; transcreve se faltar). */
38
+ async function palavras(ctx, asset, { obrigatorio = false } = {}) {
39
+ const t = await TR.obter(ctx.p.dir, asset);
40
+ if (!t.ok) { if (obrigatorio) throw T.erro(t.error); return []; }
41
+ return t.palavras;
42
+ }
43
+
44
+ /** Os estalos da gravação (segundos), melhor primeiro. */
45
+ async function estalosDe(ctx, asset) {
46
+ const cache = path.join(ctx.p.dir, 'midia', `estalos-${asset.id}.json`);
47
+ let cands;
48
+ if (fs.existsSync(cache)) cands = JSON.parse(fs.readFileSync(cache, 'utf8'));
49
+ else {
50
+ const taxa = 22050;
51
+ cands = R.estalos(await r().amostras(asset.path, taxa), taxa);
52
+ fs.mkdirSync(path.dirname(cache), { recursive: true });
53
+ fs.writeFileSync(cache, JSON.stringify(cands));
54
+ }
55
+ const arq = RT.arquivos(ctx.p.dir, asset).rastro;
56
+ const trilhas = fs.existsSync(arq) ? R.trilhasDasMaos(RT.lerRastro(arq)) : null;
57
+ return R.escolherEstalos(cands, trilhas, trilhas ? RT.lerRastro(arq).fps : 30);
58
+ }
59
+
60
+ /**
61
+ * "snap" | "snap:2" | "junta" | "abre" | "palavra:camadas" | quadro (número)
62
+ * → segundo da ORIGEM da gravação.
63
+ */
64
+ async function momento(ctx, asset, valor) {
65
+ if (valor === undefined || valor === null) return null;
66
+ if (typeof valor === 'number' || /^\d+$/.test(String(valor))) {
67
+ const s = C.quadroParaOrigem(ctx.tl, asset.id, Number(valor));
68
+ if (s === null) throw T.erro(`o quadro ${valor} não cai na gravação ${asset.id}`);
69
+ return s;
70
+ }
71
+ const v = String(valor).trim().toLowerCase();
72
+ let m = v.match(/^(snap|estalo|palma|clap)(?::(\d+))?$/);
73
+ if (m) {
74
+ const e = await estalosDe(ctx, asset);
75
+ if (!e.length) throw T.erro('não achei estalo no áudio (um "tec" curto e agudo). Diga o quadro: {"at":<quadro>}');
76
+ const n = Number(m[2] || 0);
77
+ const porTempo = [...e].filter((x) => x.total >= e[0].total - 12).sort((a, b) => a.t - b.t);
78
+ return n ? (porTempo[n - 1] || porTempo[porTempo.length - 1]).t : e[0].t;
79
+ }
80
+ m = v.match(/^(junta|join|abre|open)(?::(\d+))?$/);
81
+ if (m) {
82
+ const rs = await rastro(ctx, asset);
83
+ const ev = R.juntaEAbre(R.trilhasDasMaos(rs.rastro), { rastro: rs.rastro });
84
+ const k = ev[Number(m[2] || 1) - 1];
85
+ const f = k && (/junta|join/.test(m[1]) ? k.junta : k.abre);
86
+ if (f === null || f === undefined) throw T.erro('não vi as mãos se juntarem e abrirem; diga os quadros: {"junta":<quadro>,"abre":<quadro>}');
87
+ return f / rs.rastro.fps;
88
+ }
89
+ m = v.match(/^(?:palavra|word):(.+)$/);
90
+ if (m) {
91
+ const p = C.acharPalavra(await palavras(ctx, asset, { obrigatorio: true }), m[1]);
92
+ if (!p) throw T.erro(`a palavra "${m[1]}" não aparece na fala`);
93
+ return p.de;
94
+ }
95
+ throw T.erro(`momento "${valor}" (use um quadro, "snap", "junta", "abre" ou "palavra:<texto>")`);
96
+ }
97
+
98
+ const quadro = (ctx, asset, seg) => (seg === null ? null : C.origemParaQuadro(ctx.tl, asset.id, seg));
99
+
100
+ /** A cor média de uma imagem/vídeo (0..1 por canal), pelo ffmpeg reduzindo a 1 pixel. */
101
+ async function corMedia(arq, seg = 0) {
102
+ const out = await new Promise((resolve) => {
103
+ const { spawn } = require('child_process');
104
+ const p = spawn(video.ffmpeg(), ['-hide_banner', '-loglevel', 'error', ...(seg ? ['-ss', String(seg)] : []), '-i', arq, '-frames:v', '1', '-vf', 'scale=1:1:flags=area', '-f', 'rawvideo', '-pix_fmt', 'rgb24', '-']);
105
+ const partes = [];
106
+ p.stdout.on('data', (d) => partes.push(d));
107
+ p.on('close', () => resolve(Buffer.concat(partes)));
108
+ p.on('error', () => resolve(Buffer.alloc(0)));
109
+ });
110
+ return out.length >= 3 ? [out[0] / 255, out[1] / 255, out[2] / 255] : null;
111
+ }
112
+
113
+ /** O ganho por canal que aproxima a cor da pessoa da luz do fundo novo (de leve). */
114
+ function ganhoDeCor(pessoa, fundo, forca = 0.18) {
115
+ if (!pessoa || !fundo) return null;
116
+ const norm = (c) => { const m = (c[0] + c[1] + c[2]) / 3 || 1; return c.map((x) => x / m); };
117
+ const [p, f] = [norm(pessoa), norm(fundo)];
118
+ const g = p.map((x, i) => Math.max(0.92, Math.min(1.08, 1 + forca * (f[i] - x))));
119
+ const m = (x) => Math.round(x * 1000) / 1000;
120
+ return { r: m(g[0]), g: m(g[1]), b: m(g[2]) };
121
+ }
122
+
123
+ // ── as receitas na timeline ──────────────────────────────────────────────
124
+
125
+ function fxDe(tl, assetId) {
126
+ tl.fx = tl.fx || {};
127
+ tl.fx[assetId] = tl.fx[assetId] || { objetos: [] };
128
+ return tl.fx[assetId];
129
+ }
130
+
131
+ /** Refaz a camada dos objetos a partir da receita (troca o asset gerado e os clipes). */
132
+ function regerarObjetos(ctx, asset, rs) {
133
+ const tl = ctx.tl;
134
+ const fx = fxDe(tl, asset.id);
135
+ C.limparPapel(tl, 'objetos', asset.id);
136
+ C.limparGerados(tl);
137
+ if (!fx.objetos.length) return null;
138
+ let de = Math.min(...fx.objetos.map((o) => o.de));
139
+ let ate = Math.max(...fx.objetos.map((o) => o.ate));
140
+ if (fx.fusao && fx.fusao.depoisAte) ate = Math.max(ate, fx.fusao.depoisAte);
141
+ de = Math.max(0, de);
142
+ ate = Math.min(asset.seconds, ate);
143
+ const receita = { tipo: 'objetos', base: asset.id, de: r2(de * 1000) / 1000, ate: r2(ate * 1000) / 1000, objetos: fx.objetos, fusao: fx.fusao || null };
144
+ const h = FX.hash({ receita, rastro: [rs.rastro.n, rs.rastro.versao, rs.rastro.segmentador], fps: tl.fps, w: tl.width, h: tl.height, v: FX.versao() });
145
+ const id = T.novoAssetId(tl);
146
+ tl.assets[id] = { id, kind: 'video', nome: `objetos-3d-${asset.id}`, path: path.join(ctx.p.dir, 'midia', 'gerado', `objetos-${h}.mov`),
147
+ seconds: r2(ate - de), width: tl.width, height: tl.height, fps: tl.fps, hasAudio: false, gerado: receita };
148
+ const ti = C.faixa(tl, 'objetos');
149
+ return { asset: id, clips: C.espelhar(tl, asset.id, ti, { de, ate, asset: id, origemDe: de, papel: 'objetos' }).map((c) => c.id) };
150
+ }
151
+
152
+ /** A pessoa recortada sobre a gravação (cria se não houver). */
153
+ function camadaDaPessoa(tl, asset, rs, { feather = 1.2, choke = 1 } = {}) {
154
+ C.limparPapel(tl, 'pessoa', asset.id);
155
+ const ti = C.faixa(tl, 'pessoa');
156
+ return C.espelhar(tl, asset.id, ti, { papel: 'pessoa', props: { } }).map((c) => {
157
+ c.effects = [{ type: 'key.matte', params: { matte: rs.arquivos.matte, feather, choke } }];
158
+ return c;
159
+ });
160
+ }
161
+
162
+ function temPessoa(tl, assetId) { return T.todosOsClipes(tl).some((x) => x.clip.papel === 'pessoa' && x.clip.base === assetId); }
163
+
164
+ /** O trecho [de, ate) em que uma mão está na tela (juntando buracos de até `folga` s), a partir de `desde`. */
165
+ function trechoDaMao(trilha, fps, desde, { folga = 1.5 } = {}) {
166
+ const runs = R.trechosVisiveis(trilha, Math.round(fps * 0.3));
167
+ const juntos = [];
168
+ for (const [a, b] of runs) {
169
+ const u = juntos[juntos.length - 1];
170
+ if (u && a - u[1] <= folga * fps) u[1] = b;
171
+ else juntos.push([a, b]);
172
+ }
173
+ const f = Math.round(desde * fps);
174
+ const k = juntos.find(([a, b]) => b > f + fps * 0.3);
175
+ return k ? [Math.max(k[0], f) / fps, k[1] / fps] : null;
176
+ }
177
+
178
+ // ── as operações ─────────────────────────────────────────────────────────
179
+
180
+ const OPS = {
181
+ track: {
182
+ uso: 'track {asset?,force?} — rastreia pessoa/rosto/mãos (MediaPipe, em paralelo, 1 vez por mídia) e acha estalos; devolve trechos e eventos em QUADROS',
183
+ async run(ctx, a) {
184
+ const asset = C.base(ctx.tl, a.asset);
185
+ const rs = await RT.garantir(ctx.p.dir, asset, { force: a.force });
186
+ if (!rs.ok) return rs;
187
+ const tr = R.trilhasDasMaos(rs.rastro);
188
+ const fps = rs.rastro.fps;
189
+ const q = (fr) => quadro(ctx, asset, fr / fps);
190
+ const trechos = (t) => R.trechosVisiveis(t, Math.round(fps * 0.3)).map(([x, y]) => [q(x), q(Math.max(x, y - 1))]).filter(([x, y]) => x !== null && y !== null);
191
+ const est = await estalosDe(ctx, asset);
192
+ return {
193
+ ok: true, asset: asset.id, cache: !!rs.cache, ...(rs.tookS !== undefined ? { tookS: rs.tookS, partes: rs.partes, trabalhadores: rs.trabalhadores } : {}),
194
+ segmentador: rs.rastro.segmentador, rosto: r2(rs.rastro.quadros.filter((x) => x.f).length / rs.rastro.n),
195
+ maos: { left: trechos(tr.left), right: trechos(tr.right) },
196
+ snaps: est.slice(0, 4).map((e) => ({ at: quadro(ctx, asset, e.t), s: e.t, score: e.score, mao: e.mao })),
197
+ juntaAbre: R.juntaEAbre(tr, { rastro: rs.rastro }).map((e) => ({ junta: q(e.junta), abre: e.abre === null ? null : q(e.abre) })),
198
+ };
199
+ },
200
+ },
201
+ separate_layers: {
202
+ uso: 'separate_layers {asset?,show3d?:false,from?,to?(quadros)|at?("palavra:camadas"),labels?:true} — pessoa recortada (alfa) numa faixa sobre o fundo; show3d = momento 3D das camadas se afastando',
203
+ async run(ctx, a) {
204
+ const asset = C.base(ctx.tl, a.asset);
205
+ const rs = await rastro(ctx, asset);
206
+ let de = null;
207
+ let ate = null;
208
+ if (a.show3d) {
209
+ if (a.from !== undefined) de = await momento(ctx, asset, a.from);
210
+ else {
211
+ const alvo = a.at || 'palavra:camada';
212
+ try { de = Math.max(0, (await momento(ctx, asset, alvo)) - 0.3); } catch { de = Math.min(1, asset.seconds / 4); }
213
+ }
214
+ ate = a.to !== undefined ? await momento(ctx, asset, a.to) : de + 2.8;
215
+ ate = Math.min(asset.seconds, ate);
216
+ if (!(ate > de + 0.5)) throw T.erro('o momento 3D precisa de pelo menos meio segundo');
217
+ }
218
+ return require('./ops.js').mutar(ctx, 'separate_layers', (tl) => {
219
+ const pessoa = camadaDaPessoa(tl, asset, rs);
220
+ const out = { layers: { pessoa: pessoa.length, fundo: 'a gravação embaixo' } };
221
+ if (a.show3d !== undefined) { C.limparPapel(tl, 'camadas3d', asset.id); C.limparGerados(tl); }
222
+ if (a.show3d) {
223
+ const rotulos = a.labels === false ? null : { fundo: 'FUNDO', pessoa: 'VOCÊ' };
224
+ const receita = { tipo: 'camadas', base: asset.id, de: r2(de * 1000) / 1000, ate: r2(ate * 1000) / 1000, rotulos };
225
+ const h = FX.hash({ receita, w: tl.width, h: tl.height, fps: tl.fps, n: rs.rastro.n, v: FX.versao() });
226
+ const id = T.novoAssetId(tl);
227
+ tl.assets[id] = { id, kind: 'video', nome: `camadas-3d-${asset.id}`, path: path.join(ctx.p.dir, 'midia', 'gerado', `camadas-${h}.mp4`),
228
+ seconds: r2(ate - de), width: tl.width, height: tl.height, fps: tl.fps, hasAudio: false, gerado: receita };
229
+ const ti = C.faixa(tl, 'camadas3d');
230
+ const cl = C.espelhar(tl, asset.id, ti, { de, ate, asset: id, origemDe: de, papel: 'camadas3d' });
231
+ out.show3d = { from: quadro(ctx, asset, de), to: quadro(ctx, asset, ate), clips: cl.map((c) => c.id), render: 'no export (Remotion)' };
232
+ }
233
+ C.arrumar(tl);
234
+ return out;
235
+ });
236
+ },
237
+ },
238
+ replace_background: {
239
+ uso: 'replace_background {with:"praia"|"beach"|termo|arquivo,at?:"snap"|quadro|"palavra:x",until?(quadro),asset?} — fundo novo atrás da pessoa a partir do momento (busca a foto se for termo)',
240
+ async run(ctx, a) {
241
+ const asset = C.base(ctx.tl, a.asset);
242
+ const rs = await rastro(ctx, asset);
243
+ const pedido = a.with || a.image || a.bg;
244
+ if (!pedido) throw T.erro('faltou "with" (ex.: "praia" ou um arquivo)');
245
+ let arquivo = null;
246
+ let creditos = null;
247
+ if (fs.existsSync(path.resolve(String(pedido)))) arquivo = path.resolve(String(pedido));
248
+ else {
249
+ const ass = require('../estudio/assets.js');
250
+ const termo = C.termoDeFundo(pedido);
251
+ const vert = ctx.tl.height > ctx.tl.width;
252
+ const busca = await ass.procurarImagens(termo, { quantas: 6, orientacao: vert ? 'portrait' : 'landscape' });
253
+ const boas = (busca.imagens || []).filter((x) => !x.largura || (vert ? x.altura >= x.largura : x.largura >= x.altura));
254
+ const item = boas[0] || (busca.imagens || [])[0];
255
+ if (!item) throw T.erro(`não achei foto de fundo para "${termo}" (sem rede?). Passe um arquivo: {"with":"fundo.jpg"}`);
256
+ const b = await ass.baixar(item, { pasta: ctx.p.dir, nome: `fundo-${C.sem(pedido)}`, sub: path.join('midia', 'fundos') });
257
+ if (!b.ok) throw T.erro(b.error);
258
+ arquivo = b.arquivo;
259
+ creditos = b.creditos;
260
+ }
261
+ const seg = a.at === undefined ? await momento(ctx, asset, 'snap').catch(() => 0) : await momento(ctx, asset, a.at);
262
+ const medido = await r().medirAsset(ctx.tl, arquivo);
263
+ const ganho = ganhoDeCor(await corMedia(rs.arquivos.fundo), await corMedia(arquivo));
264
+ return require('./ops.js').mutar(ctx, 'replace_background', (tl) => {
265
+ if (!temPessoa(tl, asset.id)) camadaDaPessoa(tl, asset, rs);
266
+ const F = quadro({ tl }, asset, seg) ?? 0;
267
+ const fim = a.until !== undefined ? Math.round(Number(a.until)) : T.duracao(tl);
268
+ if (!(fim > F)) throw T.erro(`o fundo começaria em ${F} e terminaria em ${fim}`);
269
+ medido.id = T.novoAssetId(tl);
270
+ tl.assets[medido.id] = medido;
271
+ const ti = C.faixa(tl, 'fundo');
272
+ // Trocar de novo no mesmo trecho substitui (não empilha), sem ripple.
273
+ tl.tracks[ti].clips = tl.tracks[ti].clips.flatMap((c) => {
274
+ if (T.fim(c) <= F || c.startFrame >= fim) return [c];
275
+ if (c.startFrame < F) { c.durationFrames = F - c.startFrame; return [c]; }
276
+ if (T.fim(c) > fim) { const corte = fim - c.startFrame; c.trimStartFrame += c.type === 'image' ? 0 : corte; c.startFrame = fim; c.durationFrames -= corte; return [c]; }
277
+ return [];
278
+ });
279
+ const tiAgora = ti;
280
+ const durMax = medido.kind === 'video' ? Math.floor(medido.seconds * tl.fps) : Infinity;
281
+ const c = T.clipe(tl, { type: medido.kind === 'video' ? 'video' : 'image', asset: medido.id, startFrame: F, durationFrames: Math.min(fim - F, durMax), volume: 0 });
282
+ // Um desfoque leve, de lente: fundo nítido demais atrás de quem fala é o que denuncia a montagem.
283
+ if (a.blur !== 0) c.effects.push({ type: 'blur.gaussian', params: { sigma: a.blur ?? 2.5 } });
284
+ c.papel = 'fundo';
285
+ c.base = asset.id;
286
+ tl.tracks[tiAgora].clips.push(c);
287
+ // A pessoa ganha a cor do fundo novo só dali em diante.
288
+ const pessoas = () => T.todosOsClipes(tl).filter((x) => x.clip.papel === 'pessoa' && x.clip.base === asset.id).map((x) => x.clip);
289
+ for (const p of pessoas()) if (F > p.startFrame && F < T.fim(p)) E.dividir(tl, p.id, F);
290
+ if (fim < T.duracao(tl)) for (const p of pessoas()) if (fim > p.startFrame && fim < T.fim(p)) E.dividir(tl, p.id, fim);
291
+ if (ganho) for (const p of pessoas()) {
292
+ if (p.startFrame >= F && T.fim(p) <= fim) p.effects = p.effects.filter((e) => e.type !== 'color.match').concat({ type: 'color.match', params: ganho });
293
+ }
294
+ T.ordenar(tl);
295
+ C.arrumar(tl);
296
+ return { background: { asset: medido.id, at: F, until: fim, arquivo: path.basename(arquivo), ...(creditos ? { creditos } : {}), ...(ganho ? { corDaPessoa: ganho } : {}) } };
297
+ });
298
+ },
299
+ },
300
+ attach_3d: {
301
+ uso: 'attach_3d {object:"chatgpt"|"claude"|"brain"|texto,hand?:"auto"|"left"|"right"|"other"(lado da TELA),from?,to?(quadros),color?,asset?} — objeto 3D flutuando na palma, seguindo a mão; sem from: quando o nome é dito, senão quando a mão aparece',
302
+ async run(ctx, a) {
303
+ const asset = C.base(ctx.tl, a.asset);
304
+ const rs = await rastro(ctx, asset);
305
+ const fps = rs.rastro.fps;
306
+ const tr = R.trilhasDasMaos(rs.rastro);
307
+ const fx = (ctx.tl.fx && ctx.tl.fx[asset.id]) || { objetos: [] }; // só leitura: mudar fora do mutar escaparia do undo
308
+ const nome = String(a.object || a.objeto || '').trim();
309
+ if (!nome) throw T.erro('faltou "object" (chatgpt, claude, brain ou um texto curto)');
310
+ const modelo = C.modeloDe(nome) || 'texto';
311
+ const anterior = fx.objetos[fx.objetos.length - 1];
312
+ let de;
313
+ if (a.from !== undefined) de = await momento(ctx, asset, a.from);
314
+ else {
315
+ const dito = C.acharPalavra(await palavras(ctx, asset), modelo === 'texto' ? nome : ({ chatgpt: 'chatgpt', claude: 'claude', cerebro: 'cerebro' })[modelo] || nome, { desde: anterior ? anterior.de : 0 });
316
+ de = dito ? dito.de : null;
317
+ }
318
+ let mao = String(a.hand || 'auto').toLowerCase();
319
+ if (/^(other|outra)$/.test(mao)) mao = anterior ? (anterior.mao === 'left' ? 'right' : 'left') : 'auto';
320
+ if (/^(esquerda|left)$/.test(mao)) mao = 'left';
321
+ if (/^(direita|right)$/.test(mao)) mao = 'right';
322
+ if (mao === 'auto') {
323
+ const ocupadas = new Set(fx.objetos.filter((o) => de === null || (o.de <= de && o.ate > de)).map((o) => o.mao));
324
+ const opcoes = ['left', 'right'].filter((m) => !ocupadas.has(m));
325
+ const desde = de === null ? (anterior ? anterior.de : 0) : de;
326
+ const cand = (opcoes.length ? opcoes : ['left', 'right']).map((m) => ({ m, t: trechoDaMao(tr[m], fps, desde) })).filter((x) => x.t);
327
+ cand.sort((x, y) => Math.max(x.t[0], desde) - Math.max(y.t[0], desde));
328
+ if (!cand.length) throw T.erro('não vi nenhuma mão livre na tela depois desse ponto (rode track para ver os trechos)');
329
+ mao = cand[0].m;
330
+ }
331
+ if (!['left', 'right'].includes(mao)) throw T.erro(`hand "${a.hand}" (use auto, left, right ou other)`);
332
+ const t = trechoDaMao(tr[mao], fps, de === null ? (anterior ? anterior.de : 0) : de);
333
+ if (!t) throw T.erro(`a mão ${mao} não aparece depois desse ponto`);
334
+ if (de === null) de = t[0];
335
+ const ate = a.to !== undefined ? await momento(ctx, asset, a.to) : Math.min(asset.seconds, Math.max(t[1] + 0.3, de + 1.5));
336
+ if (!(ate > de)) throw T.erro('"to" antes de "from"');
337
+ return require('./ops.js').mutar(ctx, 'attach_3d', (tl) => {
338
+ const f = fxDe(tl, asset.id);
339
+ const o = { id: `o${f.objetos.length + 1}`, modelo, mao, de: r2(de * 1000) / 1000, ate: r2(ate * 1000) / 1000 };
340
+ if (a.color) o.cor = C.cor(a.color);
341
+ if (modelo === 'texto') o.texto = nome.slice(0, 14);
342
+ f.objetos.push(o);
343
+ const g = regerarObjetos({ tl, p: ctx.p }, asset, rs);
344
+ C.arrumar(tl);
345
+ return { object: { id: o.id, modelo, hand: mao, from: quadro({ tl }, asset, o.de), to: quadro({ tl }, asset, o.ate) }, layer: g && g.asset, render: 'no export (Remotion)' };
346
+ });
347
+ },
348
+ },
349
+ merge_on_join: {
350
+ uso: 'merge_on_join {junta?,abre?(quadros; sem eles, os do rastro),then?:"brain"|"none",until?(quadro|"palavra:x"; padrão: 3 s depois de abrir),asset?} — ao juntar as mãos os objetos voam e se fundem num clarão; ao abrir nasce o cérebro',
351
+ async run(ctx, a) {
352
+ const asset = C.base(ctx.tl, a.asset);
353
+ const rs = await rastro(ctx, asset);
354
+ const fps = rs.rastro.fps;
355
+ const fx = (ctx.tl.fx && ctx.tl.fx[asset.id]) || { objetos: [] }; // só leitura: mudar fora do mutar escaparia do undo
356
+ if (!fx.objetos.length) throw T.erro('não há objeto na mão para fundir: rode attach_3d antes');
357
+ let junta = a.junta !== undefined ? await momento(ctx, asset, a.junta) : null;
358
+ let abre = a.abre !== undefined ? await momento(ctx, asset, a.abre) : null;
359
+ if (junta === null) {
360
+ const desde = Math.max(...fx.objetos.map((o) => o.de));
361
+ const ev = R.juntaEAbre(R.trilhasDasMaos(rs.rastro), { rastro: rs.rastro, desde: Math.round(desde * fps) });
362
+ if (!ev.length) throw T.erro('não vi as mãos se juntarem depois dos objetos; diga os quadros: {"junta":<quadro>,"abre":<quadro>}');
363
+ junta = ev[0].junta / fps;
364
+ if (abre === null && ev[0].abre !== null) abre = ev[0].abre / fps;
365
+ }
366
+ const ateFusao = a.until !== undefined ? await momento(ctx, asset, a.until) : null;
367
+ const depois = a.then === 'none' || a.then === false ? null : C.modeloDe(a.then || 'brain') || 'cerebro';
368
+ return require('./ops.js').mutar(ctx, 'merge_on_join', (tl) => {
369
+ const f = fxDe(tl, asset.id);
370
+ // Os objetos vivos na junção ficam até ela (e se fundem).
371
+ for (const o of f.objetos) if (o.de < junta && o.ate < junta + 0.5) o.ate = r2((junta + 0.6) * 1000) / 1000;
372
+ // O cérebro é o fecho do gesto, não o resto do vídeo: 3 s, ou até o `until`.
373
+ const nasce = abre === null ? junta + 0.7 : abre;
374
+ const depoisAte = Math.min(asset.seconds, ateFusao !== null ? ateFusao : nasce + 3);
375
+ f.fusao = { junta: r2(junta * 1000) / 1000, abre: abre === null ? null : r2(abre * 1000) / 1000, depois, depoisAte: r2(depoisAte * 1000) / 1000 };
376
+ const g = regerarObjetos({ tl, p: ctx.p }, asset, rs);
377
+ C.arrumar(tl);
378
+ return { merge: { junta: quadro({ tl }, asset, junta), abre: abre === null ? null : quadro({ tl }, asset, abre), then: depois }, layer: g && g.asset };
379
+ });
380
+ },
381
+ },
382
+ cut_silence_at: {
383
+ uso: 'cut_silence_at {event?:"snap"|quadro|"palavra:x",asset?,threshold?:-35,minSilence?:0.4,padding?:0.1,window?:1(s)} — ripple no silêncio que vem logo depois do momento',
384
+ async run(ctx, a) {
385
+ const asset = C.base(ctx.tl, a.asset);
386
+ const seg = await momento(ctx, asset, a.event === undefined ? 'snap' : a.event);
387
+ const sil = await r().silenciosDe(ctx.p.dir, asset, { threshold: a.threshold ?? -35, minSilence: a.minSilence ?? 0.4 });
388
+ const janela = a.window ?? 1;
389
+ const pad = a.padding ?? 0.1;
390
+ const s = sil.find((x) => x.ate > seg + 0.2 && x.de >= seg - 0.6 && x.de <= seg + janela);
391
+ if (!s) return { ok: false, error: `não há silêncio logo depois de ${r2(seg)} s (silêncios: ${sil.slice(0, 6).map((x) => `${r2(x.de)}-${r2(x.ate)}`).join(', ')})` };
392
+ const de = Math.max(seg + 0.12, s.de + pad);
393
+ const ate = s.ate >= asset.seconds - 0.05 ? s.ate : s.ate - pad;
394
+ if (!(ate > de)) return { ok: false, error: 'o silêncio depois do momento é curto demais para cortar' };
395
+ const ranges = [];
396
+ for (const c of C.clipesBase(ctx.tl, asset.id)) {
397
+ const j = T.janelaOrigem(ctx.tl, c);
398
+ const i0 = Math.max(j.de, de);
399
+ const i1 = Math.min(j.ate, ate);
400
+ if (i1 > i0) ranges.push([T.origemParaQuadro(ctx.tl, c, i0), T.origemParaQuadro(ctx.tl, c, i1)]);
401
+ }
402
+ if (!ranges.length) return { ok: false, error: 'esse silêncio já foi cortado' };
403
+ return require('./ops.js').mutar(ctx, 'cut_silence_at', (tl) => ({ silence: { de: r2(de), ate: r2(ate) }, ...E.apagarTrechos(tl, ranges) }));
404
+ },
405
+ },
406
+ style_captions: {
407
+ uso: 'style_captions {ids?:"all"|[..],at?(quadro: a legenda ali),color?,textColor?,size?,position?:"above_head"|"bottom"|"top"|"center",follow?} — color = o que está colorido (o destaque; sem destaque, o texto)',
408
+ async run(ctx, a) {
409
+ const tl = ctx.tl;
410
+ const leg = T.todosOsClipes(tl).filter((x) => x.clip.type === 'text').map((x) => x.clip);
411
+ let alvo;
412
+ if (a.at !== undefined) alvo = leg.filter((c) => a.at >= c.startFrame && a.at < T.fim(c));
413
+ else if (a.ids === undefined || a.ids === 'all' || a.ids === 'current') alvo = leg.filter((c) => c.caption).length ? leg.filter((c) => c.caption) : leg;
414
+ else alvo = lista(a.ids).map((id) => T.achar(tl, id).clip);
415
+ if (!alvo.length) throw T.erro('não há legenda para mudar: rode add_captions antes');
416
+ const pos = a.position ? String(a.position).toLowerCase() : null;
417
+ const seguir = pos === 'above_head' || pos === 'acima_da_cabeca' || a.follow === true;
418
+ let asset = null;
419
+ if (seguir) {
420
+ asset = C.base(tl, a.asset);
421
+ await rastro(ctx, asset);
422
+ }
423
+ const CY = { bottom: 0.78, baixo: 0.78, top: 0.14, topo: 0.14, center: 0.5, centro: 0.5, middle: 0.5 };
424
+ if (pos && !seguir && CY[pos] === undefined) throw T.erro(`position "${a.position}" (use above_head, bottom, top ou center)`);
425
+ const corPedida = a.color !== undefined ? C.cor(a.color) : undefined;
426
+ return require('./ops.js').mutar(ctx, 'style_captions', (t) => {
427
+ for (const ref of alvo) {
428
+ const c = T.achar(t, ref.id).clip;
429
+ const st = { ...(c.text.style || {}) };
430
+ if (corPedida) { if (c.text.animation === 'highlightPop') st.highlight = corPedida; else st.color = corPedida; }
431
+ if (a.textColor !== undefined) st.color = C.cor(a.textColor);
432
+ if (a.size !== undefined) st.size = Number(a.size);
433
+ c.text.style = st;
434
+ if (seguir) c.text.follow = { asset: asset.id, alvo: 'cabeca' };
435
+ else if (pos) { delete c.text.follow; c.transform.cy = CY[pos]; }
436
+ }
437
+ return { captions: alvo.length, ...(corPedida ? { color: corPedida } : {}), ...(seguir ? { follow: 'cabeça' } : pos ? { cy: CY[pos] } : {}) };
438
+ });
439
+ },
440
+ },
441
+ };
442
+
443
+ const EX = {
444
+ track: '{}',
445
+ separate_layers: '{"show3d":true}',
446
+ replace_background: '{"with":"praia","at":"snap"}',
447
+ attach_3d: '{"object":"chatgpt","hand":"auto"}',
448
+ merge_on_join: '{"then":"brain"}',
449
+ cut_silence_at: '{"event":"snap"}',
450
+ style_captions: '{"color":"azul","position":"above_head"}',
451
+ };
452
+
453
+ module.exports = { OPS, EX, momento, estalosDe, ganhoDeCor, trechoDaMao, regerarObjetos, palavras };