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,347 @@
1
+ /**
2
+ * ops.js — as operações do editor, uma por nome, com o texto de ajuda junto.
3
+ *
4
+ * O texto de ajuda mora AO LADO da operação de propósito: é ele que o modelo
5
+ * lê (`primocode video help`) para saber o que existe, e ajuda escrita
6
+ * noutro arquivo envelhece diferente do código. Cada linha é curta, com a
7
+ * unidade de cada número — quadro na linha do tempo, segundo na origem.
8
+ *
9
+ * Toda operação que MUDA o projeto passa por `mutar`: foto de antes, edição,
10
+ * validação, gravação, delta. Se qualquer passo falha, nada é gravado — um
11
+ * add_clips de dez clipes com um errado não deixa nove pela metade.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const T = require('./timeline.js');
17
+ const E = require('./edits.js');
18
+ const F = require('./filters.js');
19
+ const { diff } = require('./delta.js');
20
+ const P = require('./projeto.js');
21
+ const TR = require('./transcript.js');
22
+ const A = require('./analise.js');
23
+
24
+ const r = () => require('./render.js'); // só carrega o ffmpeg quando precisa
25
+
26
+ const lista = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
27
+
28
+ /** Os clipes que a operação pediu: ids, id, ou "all" (todos os visuais). */
29
+ function alvos(tl, args, { tipos = ['video', 'image'] } = {}) {
30
+ const refs = lista(args.ids !== undefined ? args.ids : args.id);
31
+ if (!refs.length) throw T.erro('faltou "ids" (ou "all")');
32
+ if (refs.length === 1 && (refs[0] === 'all' || refs[0] === '*')) {
33
+ return T.todosOsClipes(tl).filter((x) => tipos.includes(x.clip.type)).map((x) => x.clip.id);
34
+ }
35
+ return refs.map((ref) => T.achar(tl, ref).clip.id);
36
+ }
37
+
38
+ async function mutar(ctx, op, fn) {
39
+ const antes = T.clone(ctx.tl);
40
+ const extra = (await fn(ctx.tl)) || {};
41
+ T.ordenar(ctx.tl);
42
+ const probs = T.problemas(ctx.tl);
43
+ if (probs.length) throw T.erro(`edição recusada (nada mudou): ${probs.slice(0, 4).join('; ')}`);
44
+ P.fotografar(ctx.p, antes, op);
45
+ P.salvar(ctx.p, ctx.tl);
46
+ const d = diff(antes, ctx.tl, extra);
47
+ P.anotar(ctx.p, op, { dur: d.dur, added: (d.added || []).length, removed: (d.removed || []).length });
48
+ return d;
49
+ }
50
+
51
+ function precisaProjeto(ctx) {
52
+ if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."} (cria o projeto) ou new');
53
+ }
54
+
55
+ // ── as operações ─────────────────────────────────────────────────────────
56
+
57
+ const OPS = {
58
+ new: {
59
+ uso: 'new {name?,width?:1080,height?:1920,fps?:30} — cria projeto em ./video/<id>/ e o torna atual',
60
+ async run(ctx, a) {
61
+ const { p, tl } = P.criar({ nome: a.name, width: a.width, height: a.height, fps: a.fps });
62
+ return { ok: true, project: p.id, dir: p.dir, fps: tl.fps, size: `${tl.width}x${tl.height}` };
63
+ },
64
+ },
65
+ open: {
66
+ uso: 'open {id} | list {} — troca de projeto / lista os projetos desta pasta',
67
+ async run(ctx, a) {
68
+ const x = P.abrir(a.id);
69
+ if (!x) throw T.erro(`não há projeto "${a.id}"`);
70
+ P.usar(x.p.id);
71
+ return { ok: true, project: x.p.id, ...T.vista(x.tl) };
72
+ },
73
+ },
74
+ list: { oculto: true, async run() { return { ok: true, current: P.atual(), projects: P.listar() }; } },
75
+ import: {
76
+ uso: 'import {path|paths[],track?,at?,from?,to?,noClip?} — mídia vira asset + clipe no fim da faixa (sem projeto, cria um do tamanho do vídeo)',
77
+ async run(ctx, a) {
78
+ const caminhos = lista(a.paths || a.path);
79
+ if (!caminhos.length) throw T.erro('faltou "path"');
80
+ if (!ctx.tl) {
81
+ const probe = await r().medirAsset(T.novo(), caminhos[0]);
82
+ const vert = probe.width && probe.height;
83
+ const x = P.criar({ nome: a.name || probe.nome.replace(/\.[^.]+$/, ''),
84
+ width: a.width || (vert ? probe.width : 1080), height: a.height || (vert ? probe.height : 1920),
85
+ fps: a.fps || (probe.fps ? Math.min(60, Math.round(probe.fps)) : 30) });
86
+ Object.assign(ctx, x);
87
+ }
88
+ const medidos = [];
89
+ for (const c of caminhos) medidos.push(await r().medirAsset(ctx.tl, c));
90
+ return mutar(ctx, 'import', (tl) => {
91
+ for (const m of medidos) {
92
+ m.id = T.novoAssetId(tl);
93
+ tl.assets[m.id] = m;
94
+ if (!a.noClip) E.adicionar(tl, { asset: m.id, track: a.track, at: a.at, from: a.from, to: a.to });
95
+ }
96
+ return { project: ctx.p.id };
97
+ });
98
+ },
99
+ },
100
+ get_timeline: {
101
+ uso: 'get_timeline {from?,to?} — o projeto compacto (só o que não é padrão). src=[s,s] segundos da origem',
102
+ async run(ctx, a) { precisaProjeto(ctx); return { ok: true, project: ctx.p.id, ...T.vista(ctx.tl, a) }; },
103
+ },
104
+ inspect: {
105
+ uso: 'inspect {frames:[f..]|at:f} — renderiza os quadros com grade 0–1 + nº do quadro numa folha PNG (abra e OLHE)',
106
+ async run(ctx, a) {
107
+ precisaProjeto(ctx);
108
+ const d = T.duracao(ctx.tl);
109
+ const frames = a.frames ? lista(a.frames) : a.at !== undefined ? [a.at] : [0, Math.floor(d / 2), d - 1];
110
+ return r().inspecionar(ctx.p.dir, ctx.tl, { frames, escala: a.scale || 0.5, out: a.out });
111
+ },
112
+ },
113
+ get_transcript: {
114
+ uso: 'get_transcript {asset?,from?,to?,force?} — palavras [i,texto,f,e] em QUADROS da timeline atual; i é estável entre edições',
115
+ async run(ctx, a) {
116
+ precisaProjeto(ctx);
117
+ const asset = T.acharAsset(ctx.tl, a.asset);
118
+ const t = await TR.obter(ctx.p.dir, asset, { force: a.force, idioma: a.lang || 'pt' });
119
+ if (!t.ok) return t;
120
+ const mapa = TR.mapear(ctx.tl, asset.id, t.palavras)
121
+ .filter((w) => w.e > (a.from || 0) && w.f < (a.to === undefined ? Infinity : a.to));
122
+ return { ok: true, asset: asset.id, fonte: t.fonte, total: t.palavras.length, onTimeline: mapa.length, words: mapa.map((w) => [w.i, w.texto, w.f, w.e]) };
123
+ },
124
+ },
125
+ add_clips: {
126
+ uso: 'add_clips {clips:[{asset,track?,at?,from?,to?|dur?,...props}]} — lote atômico (tudo ou nada)',
127
+ async run(ctx, a) {
128
+ precisaProjeto(ctx);
129
+ return mutar(ctx, 'add_clips', (tl) => { lista(a.clips).forEach((c) => E.adicionar(tl, c)); });
130
+ },
131
+ },
132
+ insert: {
133
+ uso: 'insert {asset|type:"text",at,from?,to?|dur?,track?,rippleTracks?} — insere empurrando tudo depois de at (ripple)',
134
+ async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'insert', (tl) => { E.inserir(tl, a); }); },
135
+ },
136
+ move: {
137
+ uso: 'move {id,to?(quadro),track?(índice|"new")} — move sem ripple (sobrepor na mesma faixa é recusado)',
138
+ async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'move', (tl) => { E.mover(tl, a.id, a); }); },
139
+ },
140
+ remove: {
141
+ uso: 'remove {ids,ripple?:false} — apaga clipes; ripple fecha o buraco se nenhuma faixa usa o trecho',
142
+ async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'remove', (tl) => { E.remover(tl, lista(a.ids || a.id), { ripple: a.ripple }); }); },
143
+ },
144
+ split: {
145
+ uso: 'split {id,at(quadro da timeline)} — divide em dois',
146
+ async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'split', (tl) => { E.dividir(tl, a.id, a.at); }); },
147
+ },
148
+ ripple_delete_ranges: {
149
+ uso: 'ripple_delete_ranges {ranges:[[de,ate],..](quadros, ate exclusivo),tracks?} — apaga trechos em todas as faixas e fecha',
150
+ async run(ctx, a) {
151
+ precisaProjeto(ctx);
152
+ return mutar(ctx, 'ripple_delete_ranges', (tl) => E.apagarTrechos(tl, lista(a.ranges), { tracks: a.tracks }));
153
+ },
154
+ },
155
+ remove_silence: {
156
+ uso: 'remove_silence {asset?,threshold?:-35(dB),minSilence?:0.5(s),padding?:0.15(s),dryRun?} — silencedetect → ripple',
157
+ async run(ctx, a) {
158
+ precisaProjeto(ctx);
159
+ const asset = T.acharAsset(ctx.tl, a.asset);
160
+ const sil = await r().silenciosDe(ctx.p.dir, asset, { threshold: a.threshold ?? -35, minSilence: a.minSilence ?? 0.5 });
161
+ const clipes = T.todosOsClipes(ctx.tl).map((x) => x.clip).filter((c) => c.asset === asset.id);
162
+ const ranges = A.trechosDeSilencio(ctx.tl, clipes, { [asset.id]: sil }, { padding: a.padding ?? 0.15 });
163
+ if (a.dryRun) return { ok: true, silences: sil.length, ranges: E.juntar(ranges) };
164
+ return mutar(ctx, 'remove_silence', (tl) => ({ silences: sil.length, ...E.apagarTrechos(tl, ranges) }));
165
+ },
166
+ },
167
+ remove_words: {
168
+ uso: 'remove_words {words:[i..],asset?} — apaga as palavras (índices do get_transcript) com ripple',
169
+ async run(ctx, a) {
170
+ precisaProjeto(ctx);
171
+ const asset = T.acharAsset(ctx.tl, a.asset);
172
+ const t = await TR.obter(ctx.p.dir, asset);
173
+ if (!t.ok) return t;
174
+ const { ranges, faltou } = E.trechosDasPalavras(TR.mapear(ctx.tl, asset.id, t.palavras), lista(a.words));
175
+ if (!ranges.length) throw T.erro('nenhuma dessas palavras está na timeline');
176
+ return mutar(ctx, 'remove_words', (tl) => ({ ...E.apagarTrechos(tl, ranges), ...(faltou.length ? { notOnTimeline: faltou } : {}) }));
177
+ },
178
+ },
179
+ set_clip_properties: {
180
+ uso: 'set_clip_properties {ids|"all",props:{volume,opacity,speed,fadeIn,fadeOut(quadros),dur,cx,cy,w,h,rotation,crop{l,t,r,b},fit(cover|contain|stretch),hidden,content,style,animation}}',
181
+ async run(ctx, a) {
182
+ precisaProjeto(ctx);
183
+ return mutar(ctx, 'set_clip_properties', (tl) => { E.propriedades(tl, alvos(tl, a, { tipos: ['video', 'image', 'audio', 'text'] }), a.props || {}); });
184
+ },
185
+ },
186
+ set_keyframes: {
187
+ uso: 'set_keyframes {id,prop(zoom|cx|cy|rotation|opacity|volume),keys:[{frame(timeline),value,interp?:ease|linear|hold}]} — keys:[] limpa',
188
+ async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'set_keyframes', (tl) => { E.keyframes(tl, a.id, a.prop, a.keys); }); },
189
+ },
190
+ apply_layout: {
191
+ uso: `apply_layout {layout(${E.LAYOUTS.join('|')}),ids:[principal,..],size?:0.34} — posiciona (transform 0–1)`,
192
+ async run(ctx, a) {
193
+ precisaProjeto(ctx);
194
+ return mutar(ctx, 'apply_layout', (tl) => { const av = E.layout(tl, a.layout, lista(a.ids), a); return av.length ? { avisos: av } : {}; });
195
+ },
196
+ },
197
+ add_texts: {
198
+ uso: 'add_texts {texts:[{content,at,dur(quadros),cx?,cy?,w?,animation?,style?}]} — títulos/letreiros',
199
+ async run(ctx, a) { precisaProjeto(ctx); return mutar(ctx, 'add_texts', (tl) => { E.textos(tl, lista(a.texts)); }); },
200
+ },
201
+ add_captions: {
202
+ uso: 'add_captions {asset?,maxWords?:3,animation?:highlightPop,cy?:0.72,style?} — legenda da transcrição, já nos cortes atuais (refazer substitui)',
203
+ async run(ctx, a) {
204
+ precisaProjeto(ctx);
205
+ const asset = T.acharAsset(ctx.tl, a.asset);
206
+ const t = await TR.obter(ctx.p.dir, asset);
207
+ if (!t.ok) return t;
208
+ return mutar(ctx, 'add_captions', (tl) => ({ captions: E.legendas(tl, TR.mapear(tl, asset.id, t.palavras), a).length }));
209
+ },
210
+ },
211
+ apply_color: {
212
+ uso: `apply_color {ids|"all",preset?(${Object.keys(F.PRESETS_COR).join('|')}),exposure,contrast,saturation,temperature,tint(-1..1),gamma}`,
213
+ async run(ctx, a) {
214
+ precisaProjeto(ctx);
215
+ const ef = F.efeitoDeCor(a);
216
+ return mutar(ctx, 'apply_color', (tl) => {
217
+ for (const id of alvos(tl, a)) { const { clip } = T.achar(tl, id); clip.effects = clip.effects.filter((e) => e.type !== 'color.adjust').concat(ef); }
218
+ });
219
+ },
220
+ },
221
+ apply_effect: {
222
+ uso: `apply_effect {ids|"all",type(${Object.keys(F.EFEITOS).filter((k) => k !== 'color.adjust').join('|')}),params?,remove?:true}`,
223
+ async run(ctx, a) {
224
+ precisaProjeto(ctx);
225
+ const ef = { type: a.type, params: a.params || {} };
226
+ if (!a.remove) F.filtrosDoEfeito(ef);
227
+ return mutar(ctx, 'apply_effect', (tl) => {
228
+ for (const id of alvos(tl, a)) {
229
+ const { clip } = T.achar(tl, id);
230
+ clip.effects = clip.effects.filter((e) => e.type !== a.type);
231
+ if (!a.remove) clip.effects.push(ef);
232
+ }
233
+ });
234
+ },
235
+ },
236
+ detect_beats: {
237
+ uso: 'detect_beats {asset,sensitivity?:1.4,markers?:false} — batidas em quadros da timeline (+bpm); markers:true grava marcadores',
238
+ async run(ctx, a) {
239
+ precisaProjeto(ctx);
240
+ const asset = T.acharAsset(ctx.tl, a.asset);
241
+ const taxa = 22050;
242
+ const b = A.batidas(await r().amostras(asset.path, taxa), taxa, { sensibilidade: a.sensitivity || 1.4 });
243
+ const clipes = T.todosOsClipes(ctx.tl).map((x) => x.clip).filter((c) => c.asset === asset.id);
244
+ const quadros = clipes.length
245
+ ? [...new Set(clipes.flatMap((c) => b.beats.map((s) => T.origemParaQuadro(ctx.tl, c, s)).filter((f) => f !== null)))].sort((x, y) => x - y)
246
+ : b.beats.map((s) => T.quadros(ctx.tl, s));
247
+ const res = { bpm: b.bpm, count: quadros.length, beats: quadros.slice(0, 300) };
248
+ if (!a.markers) return { ok: true, ...res };
249
+ return mutar(ctx, 'detect_beats', (tl) => {
250
+ tl.markers = tl.markers.filter((m) => m.label !== 'beat').concat(quadros.map((f) => ({ frame: f, label: 'beat' })));
251
+ return res;
252
+ });
253
+ },
254
+ },
255
+ undo: {
256
+ uso: 'undo {steps?:1} — volta edições (cada edição guarda foto; edite sem pedir licença)',
257
+ async run(ctx, a) {
258
+ precisaProjeto(ctx);
259
+ const antes = ctx.tl;
260
+ const u = P.desfazer(ctx.p, a.steps || 1);
261
+ P.salvar(ctx.p, u.tl);
262
+ ctx.tl = u.tl;
263
+ return diff(antes, u.tl, { undone: u.desfeitas, left: u.restam });
264
+ },
265
+ },
266
+ export: {
267
+ uso: 'export {out?,from?,to?(quadros),preview?:false(meia resolução),crf?:20} — renderiza o mp4',
268
+ async run(ctx, a) { precisaProjeto(ctx); return r().exportar(ctx.p.dir, ctx.tl, a); },
269
+ },
270
+ };
271
+
272
+ // Os efeitos que seguem a pessoa (rastro, camadas, objetos 3D, legenda na cabeça) e o pedido em português.
273
+ const FXO = require('./ops-fx.js');
274
+ const PED = require('./pedido.js');
275
+ const AUTO = require('./auto.js');
276
+ Object.assign(OPS, FXO.OPS, PED.OPS, AUTO.OPS);
277
+
278
+ const ALIAS = { timeline: 'get_timeline', transcript: 'get_transcript', captions: 'add_captions', texts: 'add_texts', render: 'export', pedido: 'do', analyze: 'track' };
279
+
280
+ // Um exemplo por operação: o modelo copia a forma, e não inventa a chave.
281
+ const EX = {
282
+ new: '{"name":"aula","width":1920,"height":1080,"fps":30}',
283
+ open: '{"id":"2026-09-29-aula"}',
284
+ import: '{"path":"gravacao.mp4"}',
285
+ get_timeline: '{"from":0,"to":300}',
286
+ inspect: '{"frames":[0,150,299]}',
287
+ get_transcript: '{"from":0,"to":600}',
288
+ add_clips: '{"clips":[{"asset":"a2","track":"new","at":90,"from":2.5,"to":6}]}',
289
+ insert: '{"asset":"a2","at":120,"from":0,"to":3}',
290
+ move: '{"id":"3f2a","to":240,"track":"new"}',
291
+ remove: '{"ids":["3f2a"],"ripple":true}',
292
+ split: '{"id":"3f2a","at":180}',
293
+ ripple_delete_ranges: '{"ranges":[[120,150],[400,410]]}',
294
+ remove_silence: '{"padding":0.12,"minSilence":0.4}',
295
+ remove_words: '{"words":[12,13,14]}',
296
+ set_clip_properties: '{"ids":["3f2a"],"props":{"volume":0.3,"fadeIn":15,"opacity":0.9}}',
297
+ set_keyframes: '{"id":"3f2a","prop":"zoom","keys":[{"frame":60,"value":1},{"frame":64,"value":1.2}]}',
298
+ apply_layout: '{"layout":"pip_br","ids":["<fala>","<broll>"]}',
299
+ add_texts: '{"texts":[{"content":"3 DICAS","at":0,"dur":60,"cy":0.2,"animation":"popIn","style":{"bg":"#E11D48"}}]}',
300
+ add_captions: '{"maxWords":3,"animation":"highlightPop","style":{"upper":true}}',
301
+ apply_color: '{"ids":"all","preset":"warm","contrast":0.1}',
302
+ apply_effect: '{"ids":["3f2a"],"type":"stylize.vignette","params":{"strength":0.6}}',
303
+ detect_beats: '{"asset":"a3","markers":true}',
304
+ undo: '{"steps":2}',
305
+ export: '{"out":"final.mp4"}',
306
+ };
307
+
308
+ const EFEITOS = new Set([...Object.keys(FXO.OPS), ...Object.keys(PED.OPS), ...Object.keys(AUTO.OPS)]);
309
+
310
+ /* A ajuda principal fica curta (o modelo a lê inteira a cada tarefa): os
311
+ efeitos entram num resumo de três linhas, e o detalhe em `help efeitos`. */
312
+ function ajuda(secao) {
313
+ Object.assign(EX, FXO.EX, PED.EX, AUTO.EX);
314
+ const linha = (nome, o, linhas) => {
315
+ linhas.push(' ' + o.uso);
316
+ if (EX[nome]) linhas.push(` ex: primocode video ${nome} --json '${EX[nome]}'`);
317
+ };
318
+ if (secao === 'efeitos' || secao === 'fx') {
319
+ const linhas = ['EFEITOS QUE SEGUEM A PESSOA — o rastro (MediaPipe) roda 1x por mídia, em paralelo; o 3D renderiza no export (Remotion).',
320
+ 'Momentos: um quadro, "snap" (o estalo do áudio), "junta"/"abre" (as mãos) ou "palavra:<texto>" (quando é dito).',
321
+ 'Receitas em segundos da ORIGEM: cortar silêncio depois não desalinha nada. hand left/right = lado da TELA.'];
322
+ for (const nome of EFEITOS) linha(nome, OPS[nome], linhas);
323
+ return linhas.join('\n');
324
+ }
325
+ const linhas = [
326
+ "primocode video <op> [--json '{...}'] [--project <id>] — editor de vídeo por linha do tempo (ffmpeg)",
327
+ 'UNIDADES: posição/duração na timeline = QUADROS inteiros (fps do projeto); from/to da mídia = SEGUNDOS da origem.',
328
+ 'Faixa 0 = a de cima. cx,cy,w,h em 0–1 do quadro (cx,cy = centro). Clipe por id curto ou prefixo único. Mídia: a1, a2…',
329
+ 'Toda edição devolve um DELTA (added/removed/changed/shifted[{by,ids}]/dur) e é desfeita com undo — não peça licença.',
330
+ 'Saída: 1 linha JSON no stdout (ok:false + error se falhou). Depois de editar, use inspect e OLHE o png.',
331
+ ];
332
+ for (const [nome, o] of Object.entries(OPS)) {
333
+ if (o.oculto || EFEITOS.has(nome)) continue;
334
+ linha(nome, o, linhas);
335
+ }
336
+ linhas.push(
337
+ 'Texto: animation none|fade|popIn|typewriter|wordReveal|highlightPop; style {size(0–1 da altura),color,stroke,',
338
+ ' strokeWidth,highlight,bg,upper,font(bold|black|regular|caminho .ttf),align}.',
339
+ 'EFEITOS (recorte, fundo no estalo, logo 3D na mão, cérebro, legenda na cabeça) — detalhe: primocode video help efeitos',
340
+ ' track · separate_layers{show3d} · replace_background{with,at:"snap"} · attach_3d{object,hand} · merge_on_join · cut_silence_at · style_captions{color,position:"above_head"}',
341
+ ` do {pedido:"<português>"} — ex.: primocode video do --json '{"pedido":"quando eu estalar o dedo me coloca numa praia"}'`,
342
+ ` auto {} — O VÍDEO É O PEDIDO: lê o que a pessoa fala na gravação e aplica cada efeito no momento em que ela diz. "Edite esse vídeo" sem dizer o quê: import e auto`,
343
+ );
344
+ return linhas.join('\n');
345
+ }
346
+
347
+ module.exports = { OPS, ALIAS, ajuda, mutar, alvos };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * paralelo.js — repartir o trabalho do vídeo entre vários processos.
3
+ *
4
+ * "Um sistema que delega as tarefas para vários trabalhadores, para o
5
+ * vídeo sair mais rápido." (30/09/2026)
6
+ *
7
+ * Duas regras, e é delas que vem a CORREÇÃO:
8
+ *
9
+ * · as emendas são FIXAS (de `tamanho` em `tamanho` quadros), e não "o
10
+ * vídeo dividido pelo número de núcleos". O número de processos só muda
11
+ * quantas partes rodam ao mesmo tempo — o resultado é o mesmo numa
12
+ * máquina de 4 núcleos e numa de 16;
13
+ * · o resultado volta NA ORDEM das tarefas, seja qual for a ordem em que
14
+ * terminaram.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const os = require('os');
20
+
21
+ /** Quantos trabalhadores: os núcleos menos um (o sistema e o ffmpeg respiram). */
22
+ function quantos(max = Infinity) {
23
+ const env = Number(process.env.PRIMOCODE_TRABALHADORES);
24
+ const n = env > 0 ? env : Math.max(1, (os.cpus() || []).length - 1);
25
+ return Math.max(1, Math.min(max, n));
26
+ }
27
+
28
+ /** As partes [de, ate) de `total` quadros, de `tamanho` em `tamanho`. A última vai até o fim (ate: null). */
29
+ function partes(total, tamanho = 96) {
30
+ const out = [];
31
+ for (let de = 0; de < total; de += tamanho) out.push({ de, ate: de + tamanho >= total ? null : de + tamanho });
32
+ if (!out.length) out.push({ de: 0, ate: null });
33
+ return out;
34
+ }
35
+
36
+ /** Roda `tarefas` (funções que devolvem promessa) com no máximo `n` ao mesmo tempo. Resultado na ordem. */
37
+ async function emParalelo(tarefas, n = quantos()) {
38
+ const res = new Array(tarefas.length);
39
+ let prox = 0;
40
+ const trabalhador = async () => {
41
+ while (prox < tarefas.length) {
42
+ const i = prox++;
43
+ res[i] = await tarefas[i]();
44
+ }
45
+ };
46
+ await Promise.all(Array.from({ length: Math.min(n, tarefas.length) }, trabalhador));
47
+ return res;
48
+ }
49
+
50
+ module.exports = { quantos, partes, emParalelo };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * pedido.js — `primocode video do {"pedido":"..."}`: o pedido em português
3
+ * vira operações do editor, por REGRA (palavras-chave), sem modelo.
4
+ *
5
+ * "Tira o fundo e separa em camadas. Quando eu estalar o dedo me coloca
6
+ * numa praia. Põe o logo do ChatGPT na minha mão, e o do Claude nessa.
7
+ * Quando eu juntar as mãos vira um cérebro. Corta o silêncio depois do
8
+ * estalo. A legenda eu quero azul, em cima da minha cabeça."
9
+ *
10
+ * Por que regra e não o modelo: o modelo que dirige o editor pode ser um
11
+ * modelo grátis e pequeno. O pedido comum ("legenda azul") tem que dar
12
+ * certo sempre, igual, em milissegundos — e o que a regra não entende volta
13
+ * como "não entendi" com os exemplos, para o modelo usar as operações à mão.
14
+ *
15
+ * `interpretar` é pura (o teste roda cada frase); `do` executa o plano,
16
+ * adiantando EM PARALELO o que é lento e independente (rastreio e
17
+ * transcrição) antes do primeiro passo.
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const C = require('./camadas.js');
23
+
24
+ const tira = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
25
+
26
+ const LUGARES = ['praia', 'espaco', 'cidade', 'floresta', 'montanha', 'escritorio', 'deserto', 'neve', 'por do sol', 'paris', 'nova york', 'mar'];
27
+ const NOMES_COR = Object.keys(C.CORES).filter((k) => !/[^a-z ]/.test(k)).sort((a, b) => b.length - a.length);
28
+
29
+ /** A cor PEDIDA: ignora a que só descreve o estado atual ("ela está amarela"). */
30
+ function corPedida(t) {
31
+ const limpo = t.replace(/\b(esta|ta|e|era|estava|tava|ficou)\s+(\w+)/g, (m, v, w) => (NOMES_COR.includes(w) ? ' ' : m));
32
+ let melhor = null;
33
+ for (const nome of NOMES_COR) {
34
+ const m = new RegExp(`\\b${nome}\\b`).exec(limpo);
35
+ if (m && (melhor === null || m.index < melhor.i)) melhor = { i: m.index, nome };
36
+ }
37
+ return melhor && melhor.nome;
38
+ }
39
+
40
+ function lugar(t) {
41
+ for (const l of LUGARES) if (new RegExp(`\\b${l}\\b`).test(t)) return l;
42
+ const m = t.match(/\bfundo\s+(?:de|com|do|da|num|numa)\s+(?:um|uma\s+)?([a-z][a-z ]{2,30}?)(?=[,.;!?]|\s+e\s|\s+quando|$)/);
43
+ return m ? m[1].trim() : null;
44
+ }
45
+
46
+ /** Onde cada objeto é citado, em ordem, com a mão dita logo depois. */
47
+ function objetosCitados(t) {
48
+ const out = [];
49
+ const re = /\b(chat ?gpt|gpt|openai|claude|anthropic|chrome|drive)\b|\blogo\s+d[aoe]s?\s+([a-z0-9]+)/g;
50
+ let m;
51
+ const todas = [];
52
+ while ((m = re.exec(t))) todas.push(m);
53
+ for (let k = 0; k < todas.length; k++) {
54
+ m = todas[k];
55
+ const nome = m[1] || m[2];
56
+ const modelo = C.modeloDe(nome);
57
+ if (!modelo && m[2] && /^(chat|gpt|claude)/.test(m[2])) continue;
58
+ if (out.some((o) => o.pos === m.index)) continue;
59
+ // A mão dita vale até a próxima citação: "ChatGPT na mão, e o do Claude nessa" — o "nessa" é do Claude.
60
+ const prox = todas.slice(k + 1).find((x) => x.index > m.index + m[0].length);
61
+ const depois = t.slice(m.index, Math.min(m.index + 70, prox ? prox.index : Infinity));
62
+ const mao = /\besquerd/.test(depois) ? 'left' : /\bdireit/.test(depois) ? 'right'
63
+ : /\b(nessa|nesta|outra|dessa|desta|nessa aqui|na outra)\b/.test(depois) ? 'other' : null;
64
+ if (modelo === 'cerebro') continue;
65
+ const item = { object: modelo || nome, pos: m.index, mao };
66
+ // "o logo do ChatGPT": a mesma citação casou duas vezes.
67
+ if (out.length && out[out.length - 1].object === item.object && m.index - out[out.length - 1].pos < 14) continue;
68
+ out.push(item);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * pedido → {passos:[{op,args}], avisos}. `ctx`: {temLegenda, objetos:[{modelo,mao}]}.
75
+ */
76
+ function interpretar(pedido, ctx = {}) {
77
+ const t = tira(pedido).replace(/\s+/g, ' ');
78
+ const passos = [];
79
+ const avisos = [];
80
+ const add = (op, args = {}) => passos.push({ op, args });
81
+
82
+ const camadas = /\bcamadas?\b|\b3d\b|tres dimens/.test(t);
83
+ const tiraFundo = /\b(tir\w*|remov\w*|apag\w*|recort\w*)\s+(?:o\s+|do\s+|meu\s+)?fundo\b|\brecort\w*\s+(?:eu|me|a pessoa)\b|\bme\s+recort/.test(t);
84
+ const estalo = /\bestal\w*|\bsnap\b|\bdedo\b/.test(t);
85
+ const onde = lugar(t);
86
+ const trocaFundo = !!onde && /\b(coloc\w*|poe|pon\w*|bot\w*|lev\w*|transport\w*|troc\w*|mud\w*|fundo|joga\w*)\b/.test(t);
87
+
88
+ if (camadas && /\bsepar\w*|camadas\b/.test(t)) add('separate_layers', { show3d: true });
89
+ else if (tiraFundo && !trocaFundo) add('separate_layers', {});
90
+
91
+ if (trocaFundo) add('replace_background', { with: onde, ...(estalo ? { at: 'snap' } : {}) });
92
+
93
+ const objs = objetosCitados(t);
94
+ let anteriores = (ctx.objetos || []).length;
95
+ for (const o of objs) {
96
+ const hand = o.mao || (anteriores > 0 ? 'other' : 'auto');
97
+ add('attach_3d', { object: o.object, hand });
98
+ anteriores++;
99
+ }
100
+
101
+ const cerebro = /\bcerebro\b|\bbrain\b/.test(t);
102
+ const juntar = /\bjunt\w*\s+(?:as\s+|minhas\s+)?maos\b|\bmaos\s+juntas\b|\bfund\w*\s+(?:os|as)\b|\bjunt\w*\s+(?:os|as)\s+(?:dois|duas|logos)\b/.test(t);
103
+ if (juntar || (cerebro && objs.length + (ctx.objetos || []).length)) add('merge_on_join', { then: cerebro ? 'brain' : 'none' });
104
+ else if (cerebro) add('attach_3d', { object: 'brain', hand: 'auto' });
105
+
106
+ if (/\b(cort\w*|tir\w*|remov\w*|apag\w*)\s+(?:o\s+|os\s+|esse\s+|aquele\s+)?silencios?\b/.test(t)) {
107
+ if (estalo) add('cut_silence_at', { event: 'snap' });
108
+ else add('remove_silence', {});
109
+ }
110
+
111
+ if (/\blegenda/.test(t)) {
112
+ const cor = corPedida(t);
113
+ let position = null;
114
+ if (/\b(em cima|acima|sobre|por cima|encima)\s+da\s+(?:minha\s+)?cabeca\b/.test(t)) position = 'above_head';
115
+ else if (/\b(embaixo|em baixo|rodape|parte de baixo|la embaixo)\b/.test(t)) position = 'bottom';
116
+ else if (/\b(no topo|la em cima|parte de cima)\b/.test(t)) position = 'top';
117
+ else if (/\b(no meio|no centro|centralizad)\w*/.test(t)) position = 'center';
118
+ const estilo = {};
119
+ if (cor) estilo.color = cor;
120
+ if (position) estilo.position = position;
121
+ if (!ctx.temLegenda) add('add_captions', {});
122
+ if (Object.keys(estilo).length) add('style_captions', estilo);
123
+ else if (ctx.temLegenda) avisos.push('a legenda já existe: diga a cor ou a posição (ex.: "legenda azul", "em cima da minha cabeça")');
124
+ }
125
+
126
+ return { passos, avisos };
127
+ }
128
+
129
+ const EXEMPLOS = [
130
+ 'tira o fundo e separa em camadas', 'quando eu estalar o dedo me coloca numa praia', 'põe o logo do ChatGPT na minha mão',
131
+ 'e o do Claude nessa', 'quando eu juntar as mãos vira um cérebro', 'corta o silêncio depois do estalo',
132
+ 'essa legenda eu quero azul', 'quero a legenda em cima da minha cabeça',
133
+ ];
134
+
135
+ const OPS = {
136
+ do: {
137
+ uso: 'do {pedido:"<português>",dryRun?} — ex.: "tira o fundo e separa em camadas", "quando eu estalar o dedo me coloca numa praia", "põe o logo do ChatGPT na minha mão", "o do Claude nessa", "quando eu juntar as mãos vira um cérebro", "corta o silêncio depois do estalo", "legenda azul em cima da minha cabeça"',
138
+ async run(ctx, a) {
139
+ const T = require('./timeline.js');
140
+ const pedido = a.pedido || a.request || a.texto;
141
+ if (!pedido) throw T.erro('faltou "pedido"');
142
+ const tl = ctx.tl;
143
+ const info = {
144
+ temLegenda: !!tl && T.todosOsClipes(tl).some((x) => x.clip.caption),
145
+ objetos: tl && tl.fx ? Object.values(tl.fx).flatMap((v) => v.objetos || []) : [],
146
+ };
147
+ const plano = interpretar(pedido, info);
148
+ if (!plano.passos.length) {
149
+ return { ok: false, error: 'não entendi o pedido (nenhuma regra casou)', exemplos: EXEMPLOS,
150
+ dica: 'use as operações direto: primocode video help' };
151
+ }
152
+ const resumo = plano.passos.map((p) => ({ op: p.op, args: p.args }));
153
+ if (a.dryRun) return { ok: true, plano: resumo, ...(plano.avisos.length ? { avisos: plano.avisos } : {}) };
154
+ if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
155
+ const { OPS: TODAS } = require('./ops.js');
156
+ // O que é lento e não depende de nada roda JUNTO antes do primeiro passo.
157
+ const C2 = require('./camadas.js');
158
+ const asset = C2.base(tl);
159
+ const precisaRastro = plano.passos.some((p) => p.op !== 'add_captions' && p.op !== 'remove_silence');
160
+ const precisaFala = plano.passos.some((p) => ['add_captions', 'attach_3d', 'separate_layers'].includes(p.op));
161
+ const inicio = Date.now();
162
+ const [rs] = await Promise.all([
163
+ precisaRastro ? require('./rastrear.js').garantir(ctx.p.dir, asset) : null,
164
+ precisaFala && asset.hasAudio ? require('./transcript.js').obter(ctx.p.dir, asset).catch(() => null) : null,
165
+ ]);
166
+ if (rs && !rs.ok) return rs;
167
+ const feitos = [];
168
+ for (const p of plano.passos) {
169
+ const res = await TODAS[p.op].run(ctx, p.args);
170
+ feitos.push({ op: p.op, ...compacto(res) });
171
+ if (!res || res.ok === false) {
172
+ return { ok: false, error: `o passo ${p.op} falhou: ${res && res.error}`, feitos, desfazer: feitos.length > 1 ? `undo {"steps":${feitos.length - 1}}` : null };
173
+ }
174
+ }
175
+ return { ok: true, plano: resumo, feitos, tookS: Math.round((Date.now() - inicio) / 100) / 10, ...(plano.avisos.length ? { avisos: plano.avisos } : {}),
176
+ depois: 'inspect para OLHAR; export renderiza o 3D (Remotion) na primeira vez' };
177
+ },
178
+ },
179
+ };
180
+
181
+ /** O que importa de cada passo, numa linha (o delta inteiro de cada um encheria o contexto). */
182
+ function compacto(res) {
183
+ if (!res) return { ok: false };
184
+ const o = { ok: res.ok !== false };
185
+ for (const k of ['layers', 'show3d', 'background', 'object', 'merge', 'silence', 'removedFrames', 'captions', 'color', 'follow', 'dur', 'error']) if (res[k] !== undefined) o[k] = res[k];
186
+ return o;
187
+ }
188
+
189
+ const EX = { do: '{"pedido":"quando eu estalar o dedo me coloca numa praia"}' };
190
+
191
+ module.exports = { interpretar, corPedida, objetosCitados, lugar, OPS, EX, EXEMPLOS };