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.
- package/README.md +30 -6
- package/bin/primocode.js +169 -342
- package/lib/act.js +360 -39
- package/lib/browser.js +105 -4
- package/lib/casar.js +108 -0
- package/lib/catalogo.js +6 -2
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -3
- package/lib/cursor.js +10 -1
- package/lib/entrada.js +77 -11
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +21 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +66 -0
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/glifos.js +215 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +284 -53
- package/lib/estudio/kit/base.jsx +285 -0
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +13 -0
- package/lib/estudio/kit/marca.jsx +741 -0
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit/vox.jsx +357 -0
- package/lib/estudio/kit.js +78 -0
- package/lib/estudio/lancamento.js +441 -0
- package/lib/estudio/motion.js +2 -5
- package/lib/estudio/plano.js +28 -3
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +139 -51
- package/lib/estudio/ritmo.js +77 -0
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +414 -0
- package/lib/fala.js +1 -94
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/pipeline.js +55 -56
- package/lib/repeticao.js +3 -1
- package/lib/skills.js +44 -2
- package/lib/snapshot.js +111 -0
- package/lib/tools.js +100 -22
- package/lib/ui.js +50 -8
- package/lib/video/analise.js +89 -0
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +74 -0
- package/lib/video/delta.js +90 -0
- package/lib/video/edits.js +379 -0
- package/lib/video/filters.js +280 -0
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +347 -0
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/projeto.js +133 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +274 -0
- package/lib/video/texto.js +144 -0
- package/lib/video/timeline.js +318 -0
- package/lib/video/transcript.js +124 -0
- package/lib/voz.js +75 -223
- package/package.json +2 -3
- package/skills/motion-de-primeira/SKILL.md +5 -5
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/texto.py +208 -0
- package/voz/primo_voz/__main__.py +96 -236
- package/voz/primo_voz/transcrever.py +31 -1
- package/app/ansi.js +0 -55
- package/app/app.css +0 -500
- package/app/app.js +0 -1017
- package/app/icone.svg +0 -6
- package/app/index.html +0 -220
- package/app/primo-marca.js +0 -192
- package/lib/app.js +0 -615
- package/lib/icone.js +0 -263
- package/lib/instalar-app.js +0 -231
- package/lib/painel.js +0 -291
- package/voz/primo_voz/desperta.py +0 -195
- package/voz/primo_voz/orbe.py +0 -406
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delta.js — toda edição devolve O QUE MUDOU, não o projeto inteiro.
|
|
3
|
+
*
|
|
4
|
+
* Um corte de silêncio num vídeo de três minutos mexe em quarenta clipes.
|
|
5
|
+
* Devolver a linha do tempo inteira a cada passo enche o contexto do modelo
|
|
6
|
+
* em cinco edições; devolver "sumiram estes, estes andaram 45 quadros para a
|
|
7
|
+
* esquerda" cabe numa linha.
|
|
8
|
+
*
|
|
9
|
+
* O delta sai da COMPARAÇÃO do antes com o depois, e não de cada operação
|
|
10
|
+
* anotando o que fez. É de propósito: uma operação que esquece de anotar um
|
|
11
|
+
* clipe deslocado mentiria para o modelo, e ele planejaria o passo seguinte
|
|
12
|
+
* em cima de um quadro errado. Comparando, o delta é certo por construção.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const T = require('./timeline.js');
|
|
18
|
+
|
|
19
|
+
const MAX_LISTA = 12;
|
|
20
|
+
|
|
21
|
+
function indexar(tl) {
|
|
22
|
+
const m = new Map();
|
|
23
|
+
tl.tracks.forEach((t, i) => t.clips.forEach((c) => m.set(c.id, { c, t: i, tid: t.id || i })));
|
|
24
|
+
return m;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// O clipe sem posição: se só a posição mudou, é deslocamento, não mudança.
|
|
28
|
+
function semPosicao(c) {
|
|
29
|
+
const { startFrame, ...resto } = c;
|
|
30
|
+
return JSON.stringify(resto);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function diff(antes, depois, extra = {}) {
|
|
34
|
+
const a = indexar(antes);
|
|
35
|
+
const d = indexar(depois);
|
|
36
|
+
const added = [];
|
|
37
|
+
const removed = [];
|
|
38
|
+
const changed = [];
|
|
39
|
+
const deslocados = new Map(); // por quanto andou → ids
|
|
40
|
+
|
|
41
|
+
for (const [id, x] of d) {
|
|
42
|
+
const y = a.get(id);
|
|
43
|
+
if (!y) { added.push(T.compacto(depois, x.c, x.t)); continue; }
|
|
44
|
+
const mesmaCoisa = semPosicao(y.c) === semPosicao(x.c);
|
|
45
|
+
if (mesmaCoisa && y.tid === x.tid && y.c.startFrame === x.c.startFrame) continue;
|
|
46
|
+
if (mesmaCoisa && y.tid === x.tid) {
|
|
47
|
+
const by = x.c.startFrame - y.c.startFrame;
|
|
48
|
+
if (!deslocados.has(by)) deslocados.set(by, []);
|
|
49
|
+
deslocados.get(by).push(id);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
changed.push(camposMudados(antes, depois, y, x));
|
|
53
|
+
}
|
|
54
|
+
for (const id of a.keys()) if (!d.has(id)) removed.push(id);
|
|
55
|
+
|
|
56
|
+
const out = { ok: true };
|
|
57
|
+
// Cinquenta legendas novas não cabem num delta: as primeiras e a conta.
|
|
58
|
+
if (added.length) out.added = added.slice(0, MAX_LISTA);
|
|
59
|
+
if (added.length > MAX_LISTA) out.addedMore = added.length - MAX_LISTA;
|
|
60
|
+
if (removed.length) out.removed = removed;
|
|
61
|
+
if (changed.length) out.changed = changed.slice(0, MAX_LISTA);
|
|
62
|
+
if (changed.length > MAX_LISTA) out.changedMore = changed.length - MAX_LISTA;
|
|
63
|
+
if (deslocados.size) out.shifted = [...deslocados].map(([by, ids]) => ({ by, ids }));
|
|
64
|
+
if (antes.tracks.map((t) => t.id).join() !== depois.tracks.map((t) => t.id).join()) out.tracks = depois.tracks.map((t) => t.type);
|
|
65
|
+
const d0 = T.duracao(antes);
|
|
66
|
+
const d1 = T.duracao(depois);
|
|
67
|
+
out.dur = d1;
|
|
68
|
+
if (d0 !== d1) out.durWas = d0;
|
|
69
|
+
if (antes.markers.length !== depois.markers.length) out.markers = depois.markers.length;
|
|
70
|
+
const assetsNovos = Object.keys(depois.assets).filter((k) => !antes.assets[k]);
|
|
71
|
+
if (assetsNovos.length) out.assets = assetsNovos.map((k) => ({ id: k, nome: depois.assets[k].nome, s: T.r2(depois.assets[k].seconds || 0) }));
|
|
72
|
+
if (!added.length && !removed.length && !changed.length && !deslocados.size && !assetsNovos.length && out.markers === undefined) out.nada = true;
|
|
73
|
+
return { ...out, ...extra };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Só as chaves da vista compacta que mudaram (e o id). */
|
|
77
|
+
function camposMudados(antes, depois, y, x) {
|
|
78
|
+
const va = T.compacto(antes, y.c, y.t);
|
|
79
|
+
const vd = T.compacto(depois, x.c, x.t);
|
|
80
|
+
const o = { id: x.c.id };
|
|
81
|
+
for (const k of new Set([...Object.keys(va), ...Object.keys(vd)])) {
|
|
82
|
+
if (k === 'id') continue;
|
|
83
|
+
if (JSON.stringify(va[k]) !== JSON.stringify(vd[k])) o[k] = vd[k] === undefined ? null : vd[k];
|
|
84
|
+
}
|
|
85
|
+
// Mudou algo que a vista compacta não mostra (estilo do texto, keyframe…).
|
|
86
|
+
if (Object.keys(o).length === 1) o.props = true;
|
|
87
|
+
return o;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { diff };
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* edits.js — as edições, como funções puras sobre o json da linha do tempo.
|
|
3
|
+
*
|
|
4
|
+
* Nada aqui toca disco nem ffmpeg: entra timeline, sai timeline mexida (ou
|
|
5
|
+
* um erro dizendo por quê). É isso que deixa o ripple ser testado em
|
|
6
|
+
* milissegundos, e é isso que deixa a edição ser atômica — quem salva é o
|
|
7
|
+
* ops.js, e só se a função inteira terminou sem erro.
|
|
8
|
+
*
|
|
9
|
+
* ── O RIPPLE ─────────────────────────────────────────────────────────────
|
|
10
|
+
* Apagar um trecho [a, b) da linha do tempo faz, em cada clipe das faixas
|
|
11
|
+
* afetadas, uma de cinco coisas: nada (está antes), andar b−a para a
|
|
12
|
+
* esquerda (está depois), sumir (está dentro), perder a ponta (cruza uma
|
|
13
|
+
* borda) ou virar dois (o trecho está no meio dele). Com vários trechos, a
|
|
14
|
+
* conta vai do ÚLTIMO para o primeiro: assim apagar um trecho nunca muda a
|
|
15
|
+
* posição dos que ainda faltam apagar.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const T = require('./timeline.js');
|
|
21
|
+
const { erro } = T;
|
|
22
|
+
|
|
23
|
+
// ── adicionar ────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Um clipe a partir do que o modelo manda. Mídia: `asset` (id), `from`/`to`
|
|
27
|
+
* em SEGUNDOS da origem ou `dur` em quadros. Posição: `at` em quadros
|
|
28
|
+
* (padrão: o fim da faixa).
|
|
29
|
+
*/
|
|
30
|
+
function montarClipe(tl, spec) {
|
|
31
|
+
const tipo = spec.type || (spec.asset ? (T.acharAsset(tl, spec.asset).kind) : null);
|
|
32
|
+
if (!tipo) throw erro('clipe sem "asset" nem "type"');
|
|
33
|
+
const base = { ...spec, type: tipo };
|
|
34
|
+
if (spec.asset) {
|
|
35
|
+
const a = T.acharAsset(tl, spec.asset);
|
|
36
|
+
base.asset = a.id;
|
|
37
|
+
if (tipo !== 'image') {
|
|
38
|
+
const de = Number(spec.from || 0);
|
|
39
|
+
const speed = Number(spec.speed || 1);
|
|
40
|
+
base.trimStartFrame = T.quadros(tl, de);
|
|
41
|
+
let dur = spec.dur !== undefined ? Number(spec.dur) : null;
|
|
42
|
+
if (dur === null) {
|
|
43
|
+
const ate = spec.to !== undefined ? Number(spec.to) : a.seconds;
|
|
44
|
+
if (!(ate > de)) throw erro(`"to" (${ate}) precisa ser maior que "from" (${de})`);
|
|
45
|
+
dur = Math.round(T.quadros(tl, ate - de) / speed);
|
|
46
|
+
}
|
|
47
|
+
base.durationFrames = dur;
|
|
48
|
+
} else {
|
|
49
|
+
base.durationFrames = spec.dur !== undefined ? Number(spec.dur) : tl.fps * 3;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
base.durationFrames = spec.dur !== undefined ? Number(spec.dur) : tl.fps * 2;
|
|
53
|
+
}
|
|
54
|
+
delete base.id;
|
|
55
|
+
return T.clipe(tl, base);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Sem faixa pedida e com posição: a primeira faixa do tipo onde o clipe
|
|
59
|
+
CABE sem sobrepor (a da legenda fica de fora). Não coube, faixa nova. */
|
|
60
|
+
function faixaLivre(tl, c, at) {
|
|
61
|
+
const tipo = c.type === 'image' ? 'video' : c.type;
|
|
62
|
+
const livre = tl.tracks.findIndex((t) => t.type === tipo && t.role !== 'captions'
|
|
63
|
+
&& !t.clips.some((x) => x.startFrame < at + c.durationFrames && T.fim(x) > at));
|
|
64
|
+
return livre >= 0 ? livre : 'new';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function adicionar(tl, spec) {
|
|
68
|
+
const c = montarClipe(tl, spec);
|
|
69
|
+
const pedida = spec.track === undefined && spec.at !== undefined ? faixaLivre(tl, c, Math.round(Number(spec.at))) : spec.track;
|
|
70
|
+
const ti = T.faixaPara(tl, c.type, pedida);
|
|
71
|
+
const faixa = tl.tracks[ti];
|
|
72
|
+
c.startFrame = spec.at !== undefined ? Math.round(Number(spec.at))
|
|
73
|
+
: faixa.clips.reduce((m, x) => Math.max(m, T.fim(x)), 0);
|
|
74
|
+
faixa.clips.push(c);
|
|
75
|
+
T.ordenar(tl);
|
|
76
|
+
return c;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── dividir e cortar ─────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/** A parte [ps, pe) (quadros da linha do tempo) de um clipe, como clipe novo. */
|
|
82
|
+
function pedaco(tl, c, ps, pe, id) {
|
|
83
|
+
const o = ps - c.startFrame;
|
|
84
|
+
const p = T.clone(c);
|
|
85
|
+
p.id = id;
|
|
86
|
+
p.startFrame = ps;
|
|
87
|
+
p.durationFrames = pe - ps;
|
|
88
|
+
if (c.type !== 'image' && c.type !== 'text') p.trimStartFrame = c.trimStartFrame + Math.round(o * (c.speed || 1));
|
|
89
|
+
if (o > 0) p.fadeIn = 0;
|
|
90
|
+
if (pe < T.fim(c)) p.fadeOut = 0;
|
|
91
|
+
for (const prop of Object.keys(p.keyframes || {})) {
|
|
92
|
+
p.keyframes[prop] = p.keyframes[prop].map((k) => ({ ...k, frame: k.frame - o }));
|
|
93
|
+
}
|
|
94
|
+
if (p.text) {
|
|
95
|
+
p.text.words = (p.text.words || []).filter((w) => w.e > o && w.f < pe - c.startFrame)
|
|
96
|
+
.map((w) => ({ ...w, f: Math.max(0, w.f - o), e: Math.min(pe - ps, w.e - o) }));
|
|
97
|
+
}
|
|
98
|
+
return p;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Divide no quadro `at` da linha do tempo. Devolve [esquerda, direita]. */
|
|
102
|
+
function dividir(tl, ref, at) {
|
|
103
|
+
const { clip: c, track } = T.achar(tl, ref);
|
|
104
|
+
const f = Math.round(Number(at));
|
|
105
|
+
if (!(f > c.startFrame && f < T.fim(c))) throw erro(`o quadro ${f} não está dentro de ${c.id} (${c.startFrame}-${T.fim(c)})`);
|
|
106
|
+
const esq = pedaco(tl, c, c.startFrame, f, c.id);
|
|
107
|
+
const dir = pedaco(tl, c, f, T.fim(c), T.novoId(tl));
|
|
108
|
+
const faixa = tl.tracks[track];
|
|
109
|
+
faixa.clips.splice(faixa.clips.indexOf(c), 1, esq, dir);
|
|
110
|
+
return [esq, dir];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** O que sobra de um clipe depois de apagar [a, b), já na posição nova. */
|
|
114
|
+
function cortar(tl, c, a, b) {
|
|
115
|
+
const s = c.startFrame;
|
|
116
|
+
const e = T.fim(c);
|
|
117
|
+
const tam = b - a;
|
|
118
|
+
if (e <= a) return [c];
|
|
119
|
+
if (s >= b) { c.startFrame -= tam; return [c]; }
|
|
120
|
+
if (s >= a && e <= b) return [];
|
|
121
|
+
const partes = [];
|
|
122
|
+
if (s < a) partes.push(pedaco(tl, c, s, a, c.id));
|
|
123
|
+
if (e > b) {
|
|
124
|
+
const dir = pedaco(tl, c, b, e, partes.length ? T.novoId(tl) : c.id);
|
|
125
|
+
dir.startFrame = b - tam;
|
|
126
|
+
partes.push(dir);
|
|
127
|
+
}
|
|
128
|
+
// Texto não vira dois: a legenda que cruza o corte só encolhe.
|
|
129
|
+
if (partes.length === 2 && c.type === 'text') {
|
|
130
|
+
const [esq, dir] = partes;
|
|
131
|
+
esq.durationFrames += dir.durationFrames;
|
|
132
|
+
esq.text.words = esq.text.words.concat(dir.text.words.map((w) => ({ ...w, f: w.f + (a - s), e: w.e + (a - s) })));
|
|
133
|
+
esq.fadeOut = c.fadeOut;
|
|
134
|
+
return [esq];
|
|
135
|
+
}
|
|
136
|
+
return partes;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Junta trechos que se tocam ou se cruzam, em ordem. */
|
|
140
|
+
function juntar(ranges) {
|
|
141
|
+
const lista = ranges.map(([a, b]) => [Math.round(a), Math.round(b)]).filter(([a, b]) => b > a && b > 0)
|
|
142
|
+
.map(([a, b]) => [Math.max(0, a), b]).sort((x, y) => x[0] - y[0]);
|
|
143
|
+
const out = [];
|
|
144
|
+
for (const r of lista) {
|
|
145
|
+
const ult = out[out.length - 1];
|
|
146
|
+
if (ult && r[0] <= ult[1]) ult[1] = Math.max(ult[1], r[1]);
|
|
147
|
+
else out.push([...r]);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Apaga os trechos e fecha o buraco. `tracks`: quais faixas (índices);
|
|
154
|
+
* padrão todas — o que mantém legenda e música no lugar da fala.
|
|
155
|
+
*/
|
|
156
|
+
function apagarTrechos(tl, ranges, { tracks } = {}) {
|
|
157
|
+
const lista = juntar(ranges);
|
|
158
|
+
const alvo = tracks ? new Set(tracks.map(Number)) : null;
|
|
159
|
+
for (const [a, b] of [...lista].reverse()) {
|
|
160
|
+
tl.tracks.forEach((t, i) => {
|
|
161
|
+
if (alvo && !alvo.has(i)) return;
|
|
162
|
+
t.clips = t.clips.flatMap((c) => cortar(tl, c, a, b));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
T.ordenar(tl);
|
|
166
|
+
return { ranges: lista, removedFrames: lista.reduce((s, [a, b]) => s + (b - a), 0) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Abre um buraco de `tam` quadros em `at` (o insert com ripple). */
|
|
170
|
+
function abrirEspaco(tl, at, tam, { tracks } = {}) {
|
|
171
|
+
const alvo = tracks ? new Set(tracks.map(Number)) : null;
|
|
172
|
+
tl.tracks.forEach((t, i) => {
|
|
173
|
+
if (alvo && !alvo.has(i)) return;
|
|
174
|
+
for (const c of [...t.clips]) {
|
|
175
|
+
if (c.startFrame < at && T.fim(c) > at && c.type !== 'text') dividir(tl, c.id, at);
|
|
176
|
+
}
|
|
177
|
+
for (const c of t.clips) {
|
|
178
|
+
if (c.startFrame >= at) c.startFrame += tam;
|
|
179
|
+
else if (T.fim(c) > at) c.durationFrames += tam; // texto que cruza: estica
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
T.ordenar(tl);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inserir(tl, spec) {
|
|
186
|
+
const at = Math.round(Number(spec.at || 0));
|
|
187
|
+
const c = montarClipe(tl, spec);
|
|
188
|
+
abrirEspaco(tl, at, c.durationFrames, { tracks: spec.rippleTracks });
|
|
189
|
+
return adicionar(tl, { ...spec, at, dur: c.durationFrames, from: spec.from, asset: spec.asset });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mover(tl, ref, { to, track }) {
|
|
193
|
+
const { clip: c, track: de } = T.achar(tl, ref);
|
|
194
|
+
if (to !== undefined) c.startFrame = Math.max(0, Math.round(Number(to)));
|
|
195
|
+
if (track !== undefined && Number(track) !== de) {
|
|
196
|
+
const para = T.faixaPara(tl, c.type, track === 'new' ? 'new' : Number(track));
|
|
197
|
+
const origem = tl.tracks.find((t) => t.clips.includes(c));
|
|
198
|
+
origem.clips.splice(origem.clips.indexOf(c), 1);
|
|
199
|
+
tl.tracks[para].clips.push(c);
|
|
200
|
+
}
|
|
201
|
+
T.ordenar(tl);
|
|
202
|
+
return c;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function remover(tl, refs, { ripple = false } = {}) {
|
|
206
|
+
const alvos = refs.map((r) => T.achar(tl, r));
|
|
207
|
+
const trechos = alvos.map(({ clip }) => [clip.startFrame, T.fim(clip)]);
|
|
208
|
+
for (const { clip } of alvos) {
|
|
209
|
+
const t = tl.tracks.find((x) => x.clips.includes(clip));
|
|
210
|
+
t.clips.splice(t.clips.indexOf(clip), 1);
|
|
211
|
+
}
|
|
212
|
+
if (ripple) {
|
|
213
|
+
// Só fecha o buraco onde NENHUMA faixa tem mais nada no trecho.
|
|
214
|
+
const livres = trechos.filter(([a, b]) => !T.todosOsClipes(tl).some(({ clip }) => clip.startFrame < b && T.fim(clip) > a));
|
|
215
|
+
if (livres.length) apagarTrechos(tl, livres);
|
|
216
|
+
}
|
|
217
|
+
limparFaixasVazias(tl);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function limparFaixasVazias(tl) {
|
|
221
|
+
tl.tracks = tl.tracks.filter((t) => t.clips.length || t.fixa);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── palavras → trechos ───────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Os trechos (quadros) das palavras pedidas. `mapa` é a transcrição já na
|
|
228
|
+
* linha do tempo: [{i, f, e}]. Palavras vizinhas apagadas juntas levam a
|
|
229
|
+
* pausa entre elas; palavra solta leva só ela.
|
|
230
|
+
*/
|
|
231
|
+
function trechosDasPalavras(mapa, indices) {
|
|
232
|
+
const quer = new Set(indices.map(Number));
|
|
233
|
+
const achadas = mapa.filter((w) => quer.has(w.i)).sort((a, b) => a.f - b.f);
|
|
234
|
+
const faltou = [...quer].filter((i) => !mapa.some((w) => w.i === i));
|
|
235
|
+
const ranges = [];
|
|
236
|
+
let atual = null;
|
|
237
|
+
for (const w of achadas) {
|
|
238
|
+
if (atual && w.i === atual.ultimo + 1 && w.f - atual.b <= 30) { atual.b = Math.max(atual.b, w.e); atual.ultimo = w.i; continue; }
|
|
239
|
+
if (atual) ranges.push([atual.a, atual.b]);
|
|
240
|
+
atual = { a: w.f, b: w.e, ultimo: w.i };
|
|
241
|
+
}
|
|
242
|
+
if (atual) ranges.push([atual.a, atual.b]);
|
|
243
|
+
return { ranges, faltou };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── propriedades e keyframes ─────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
function propriedades(tl, refs, props) {
|
|
249
|
+
const out = [];
|
|
250
|
+
for (const r of refs) {
|
|
251
|
+
const { clip } = T.achar(tl, r);
|
|
252
|
+
if (props.speed !== undefined && clip.type !== 'text' && clip.type !== 'image') {
|
|
253
|
+
// Mudar a velocidade mantém o trecho da origem: a duração é que muda.
|
|
254
|
+
const origem = T.consumo(clip);
|
|
255
|
+
clip.speed = Number(props.speed);
|
|
256
|
+
clip.durationFrames = Math.max(1, Math.round(origem / clip.speed));
|
|
257
|
+
}
|
|
258
|
+
const { speed, ...resto } = props;
|
|
259
|
+
T.aplicarProps(clip, resto);
|
|
260
|
+
if (props.dur !== undefined) clip.durationFrames = Math.round(Number(props.dur));
|
|
261
|
+
out.push(clip.id);
|
|
262
|
+
}
|
|
263
|
+
T.ordenar(tl);
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Keyframes em quadro DA LINHA DO TEMPO; guardados relativos ao clipe. */
|
|
268
|
+
function keyframes(tl, ref, prop, keys) {
|
|
269
|
+
const { clip } = T.achar(tl, ref);
|
|
270
|
+
if (!T.PROPS_ANIMAVEIS.includes(prop)) throw erro(`"${prop}" não anima (use ${T.PROPS_ANIMAVEIS.join('/')})`);
|
|
271
|
+
const lista = (keys || []).map((k) => {
|
|
272
|
+
const interp = k.interp || 'ease';
|
|
273
|
+
if (!T.INTERP.includes(interp)) throw erro(`interp "${interp}" (use ${T.INTERP.join('/')})`);
|
|
274
|
+
return { frame: Math.round(Number(k.frame)) - clip.startFrame, value: Number(k.value), interp };
|
|
275
|
+
}).sort((a, b) => a.frame - b.frame);
|
|
276
|
+
if (lista.length) clip.keyframes[prop] = lista;
|
|
277
|
+
else delete clip.keyframes[prop];
|
|
278
|
+
return clip;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── layouts ──────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
const CANTOS = { pip_tl: [0, 0], pip_tr: [1, 0], pip_bl: [0, 1], pip_br: [1, 1] };
|
|
284
|
+
const LAYOUTS = ['full', 'side_by_side', 'top_bottom', 'grid2x2', 'grid3x3', ...Object.keys(CANTOS)];
|
|
285
|
+
|
|
286
|
+
function aspecto(tl, c) {
|
|
287
|
+
const a = c.asset && tl.assets[c.asset];
|
|
288
|
+
return a && a.width && a.height ? a.width / a.height : 16 / 9;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function layout(tl, nome, refs, { size = 0.34, margin = 0.04 } = {}) {
|
|
292
|
+
if (!LAYOUTS.includes(nome)) throw erro(`layout "${nome}" (use ${LAYOUTS.join('/')})`);
|
|
293
|
+
const alvos = refs.map((r) => T.achar(tl, r));
|
|
294
|
+
const caixa = (x, [cx, cy, w, h]) => { x.clip.transform = { ...x.clip.transform, cx, cy, w, h }; x.clip.fit = x.clip.fit === 'contain' ? 'contain' : 'cover'; };
|
|
295
|
+
const avisos = [];
|
|
296
|
+
if (nome === 'full') alvos.forEach((x) => caixa(x, [0.5, 0.5, 1, 1]));
|
|
297
|
+
if (nome === 'side_by_side') alvos.slice(0, 2).forEach((x, i) => caixa(x, [0.25 + i * 0.5, 0.5, 0.5, 1]));
|
|
298
|
+
if (nome === 'top_bottom') alvos.slice(0, 2).forEach((x, i) => caixa(x, [0.5, 0.25 + i * 0.5, 1, 0.5]));
|
|
299
|
+
if (nome.startsWith('grid')) {
|
|
300
|
+
const n = nome === 'grid2x2' ? 2 : 3;
|
|
301
|
+
alvos.slice(0, n * n).forEach((x, i) => caixa(x, [(i % n + 0.5) / n, (Math.floor(i / n) + 0.5) / n, 1 / n, 1 / n]));
|
|
302
|
+
}
|
|
303
|
+
if (CANTOS[nome]) {
|
|
304
|
+
if (alvos.length < 2) throw erro('pip precisa de dois clipes: [principal, janelinha]');
|
|
305
|
+
caixa(alvos[0], [0.5, 0.5, 1, 1]);
|
|
306
|
+
const [qx, qy] = CANTOS[nome];
|
|
307
|
+
const w = size;
|
|
308
|
+
const h = (w * tl.width) / (aspecto(tl, alvos[1].clip) * tl.height);
|
|
309
|
+
const mx = margin;
|
|
310
|
+
const my = (margin * tl.width) / tl.height;
|
|
311
|
+
caixa(alvos[1], [qx ? 1 - mx - w / 2 : mx + w / 2, qy ? 1 - my - h / 2 : my + h / 2, w, h]);
|
|
312
|
+
if (alvos[1].track > alvos[0].track) avisos.push(`a janelinha ${alvos[1].clip.id} está numa faixa ABAIXO de ${alvos[0].clip.id} e fica escondida: move {"id":"${alvos[1].clip.id}","track":"new"}`);
|
|
313
|
+
}
|
|
314
|
+
return avisos;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── textos e legendas ────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function textos(tl, lista) {
|
|
320
|
+
return lista.map((t) => {
|
|
321
|
+
if (!t.content && !(t.words || []).length) throw erro('texto sem "content"');
|
|
322
|
+
const at = Math.round(Number(t.at || 0));
|
|
323
|
+
return adicionar(tl, {
|
|
324
|
+
type: 'text', track: t.track, at, dur: t.dur !== undefined ? t.dur : tl.fps * 2,
|
|
325
|
+
content: t.content, style: t.style, animation: t.animation || 'popIn',
|
|
326
|
+
transform: { cx: t.cx !== undefined ? t.cx : 0.5, cy: t.cy !== undefined ? t.cy : 0.5, w: t.w || 0.9, h: t.h || 0.2 },
|
|
327
|
+
fadeIn: t.fadeIn, fadeOut: t.fadeOut, words: t.words,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const fimDeFrase = (s) => /[.!?…,;:]$/.test(String(s || '').trim());
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* As legendas a partir da transcrição JÁ na linha do tempo (`mapa`, depois
|
|
336
|
+
* dos cortes). Quebra em `maxWords`, em pontuação e em pausa; cada legenda
|
|
337
|
+
* guarda o tempo de cada palavra, para a animação acender a palavra dita.
|
|
338
|
+
*/
|
|
339
|
+
function legendas(tl, mapa, { maxWords = 3, animation = 'highlightPop', style = {}, cy = 0.72, w = 0.86, maxGap = 12 } = {}) {
|
|
340
|
+
// Refazer a legenda substitui a anterior, em vez de empilhar duas.
|
|
341
|
+
for (const t of tl.tracks) t.clips = t.clips.filter((c) => !c.caption);
|
|
342
|
+
limparFaixasVazias(tl);
|
|
343
|
+
const ws = [...mapa].sort((a, b) => a.f - b.f);
|
|
344
|
+
const grupos = [];
|
|
345
|
+
let g = [];
|
|
346
|
+
ws.forEach((p, k) => {
|
|
347
|
+
g.push(p);
|
|
348
|
+
const prox = ws[k + 1];
|
|
349
|
+
if (!prox || g.length >= maxWords || fimDeFrase(p.texto) || prox.f - p.e > maxGap) { grupos.push(g); g = []; }
|
|
350
|
+
});
|
|
351
|
+
const faixa = tl.tracks.findIndex((t) => t.type === 'text' && t.role === 'captions');
|
|
352
|
+
let ti = faixa;
|
|
353
|
+
if (ti < 0) { ti = T.faixaPara(tl, 'text', 'new'); tl.tracks[ti].role = 'captions'; }
|
|
354
|
+
const criados = [];
|
|
355
|
+
grupos.forEach((grupo, k) => {
|
|
356
|
+
const at = grupo[0].f;
|
|
357
|
+
const prox = grupos[k + 1];
|
|
358
|
+
// Pausa curta até a próxima: a legenda fica até ela entrar (sem piscar).
|
|
359
|
+
const fala = grupo[grupo.length - 1].e;
|
|
360
|
+
let ate = fala + 4;
|
|
361
|
+
if (prox) ate = prox[0].f - fala <= maxGap ? prox[0].f : Math.min(ate, prox[0].f);
|
|
362
|
+
const dur = Math.max(1, ate - at);
|
|
363
|
+
const c = T.clipe(tl, {
|
|
364
|
+
type: 'text', startFrame: at, durationFrames: dur, content: grupo.map((p) => p.texto).join(' '),
|
|
365
|
+
style, animation, transform: { cx: 0.5, cy, w, h: 0.16 },
|
|
366
|
+
words: grupo.map((p) => ({ t: p.texto, f: Math.max(0, p.f - at), e: Math.min(dur, p.e - at) })),
|
|
367
|
+
});
|
|
368
|
+
c.caption = true;
|
|
369
|
+
tl.tracks[ti].clips.push(c);
|
|
370
|
+
criados.push(c);
|
|
371
|
+
});
|
|
372
|
+
T.ordenar(tl);
|
|
373
|
+
return criados;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
module.exports = {
|
|
377
|
+
LAYOUTS, montarClipe, adicionar, dividir, cortar, juntar, apagarTrechos, abrirEspaco, inserir,
|
|
378
|
+
mover, remover, trechosDasPalavras, propriedades, keyframes, layout, textos, legendas, limparFaixasVazias,
|
|
379
|
+
};
|