primocode 10.0.9 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/primocode.js +25 -28
- package/lib/act.js +5 -2
- package/lib/catalogo.js +7 -4
- package/lib/config.js +10 -2
- package/lib/estudio/assets.js +86 -6
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +214 -0
- package/lib/estudio/cinema.js +37 -17
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/edicao.js +36 -17
- package/lib/estudio/estilo.js +35 -8
- package/lib/estudio/fluido.js +352 -0
- package/lib/estudio/gerativo.js +21 -3
- package/lib/estudio/hibrido.js +16 -12
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +245 -42
- package/lib/estudio/kit/fluido.jsx +614 -0
- package/lib/estudio/kit/gerativo.jsx +15 -5
- package/lib/estudio/kit/index.jsx +2 -1
- package/lib/estudio/kit/tres.jsx +88 -21
- package/lib/estudio/kit.js +3 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +3 -4
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +36 -25
- package/lib/estudio/remotion.js +53 -6
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +140 -8
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +15 -1
- package/lib/tools.js +31 -5
- package/lib/video/filters.js +6 -1
- package/lib/video/ops.js +4 -3
- package/lib/video/timeline.js +1 -1
- package/lib/video/zoom.js +275 -0
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +3 -5
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/voz_kokoro.py +98 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zoom.js — o zoom que segue o cursor numa gravação de tela.
|
|
3
|
+
*
|
|
4
|
+
* Três peças, como conta pura e testável sem vídeo nenhum:
|
|
5
|
+
*
|
|
6
|
+
* · os FOCOS: o clique, quando há registro dele; sem clique, o lugar onde
|
|
7
|
+
* o movimento PARA (o cursor anda, assenta e alguém vai clicar ali);
|
|
8
|
+
* · os TRECHOS: focos próximos no tempo viram um zoom só; uma pausa longa
|
|
9
|
+
* ou um salto para longe (com tempo de sobra) fecham o zoom e abrem outro;
|
|
10
|
+
* · a CÂMERA: dentro do trecho ela fica parada enquanto o cursor anda numa
|
|
11
|
+
* zona morta no meio da vista, e só anda quando ele sai dali — puxada por
|
|
12
|
+
* uma mola criticamente amortecida, sem tremer e sem passar do ponto.
|
|
13
|
+
*
|
|
14
|
+
* Reimplementação independente da ideia (curva, regra de clique e mola):
|
|
15
|
+
* nenhuma linha vem de outro projeto.
|
|
16
|
+
*
|
|
17
|
+
* Saída: keyframes de zoom e do FOCO do zoom (fx, fy: 0–1 do clipe), que o
|
|
18
|
+
* filters.js já sabe desenhar no zoompan.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { spawn } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const lim = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
26
|
+
|
|
27
|
+
// ── medir: onde a tela mexeu (quando ninguém gravou o cursor) ────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Quadros cinza (Uint8Array w*h, um por amostra) → [{t, x, y, move}].
|
|
31
|
+
* x,y = centro do que mudou desde o quadro anterior (o cursor, numa tela
|
|
32
|
+
* parada). Mudança grande demais (rolagem, troca de janela) não é cursor.
|
|
33
|
+
*/
|
|
34
|
+
function movimento(quadros, w, h, fps, { limiar = 24, maxFracao = 0.2 } = {}) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (let i = 1; i < quadros.length; i++) {
|
|
37
|
+
const a = quadros[i - 1];
|
|
38
|
+
const b = quadros[i];
|
|
39
|
+
let n = 0;
|
|
40
|
+
let sx = 0;
|
|
41
|
+
let sy = 0;
|
|
42
|
+
for (let k = 0; k < w * h; k++) {
|
|
43
|
+
if (Math.abs(a[k] - b[k]) > limiar) { n++; sx += k % w; sy += Math.floor(k / w); }
|
|
44
|
+
}
|
|
45
|
+
const t = i / fps;
|
|
46
|
+
if (n < 2 || n > maxFracao * w * h) out.push({ t, x: null, y: null, move: false });
|
|
47
|
+
else out.push({ t, x: (sx / n + 0.5) / w, y: (sy / n + 0.5) / h, move: true });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Trilha de cursor [{t,x,y,click?}] → amostras com `move` (andou desde a anterior). */
|
|
53
|
+
function doCursor(cursor, { parado = 0.004 } = {}) {
|
|
54
|
+
const c = cursor.filter((p) => Number.isFinite(p.t) && Number.isFinite(p.x) && Number.isFinite(p.y)).sort((a, b) => a.t - b.t);
|
|
55
|
+
return c.map((p, i) => ({ ...p, move: i > 0 && Math.hypot(p.x - c[i - 1].x, p.y - c[i - 1].y) > parado }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Os focos: os cliques, se houver; senão, onde o movimento para — andou por
|
|
60
|
+
* `minAndou` s e depois ficou parado por `minParado` s. [{t, x, y}].
|
|
61
|
+
*/
|
|
62
|
+
function focos(amostras, { minAndou = 0.15, minParado = 0.3 } = {}) {
|
|
63
|
+
const cliques = amostras.filter((a) => a.click);
|
|
64
|
+
if (cliques.length) return cliques.map(({ t, x, y }) => ({ t, x, y }));
|
|
65
|
+
const out = [];
|
|
66
|
+
let ini = null;
|
|
67
|
+
let ult = null;
|
|
68
|
+
for (const a of amostras) {
|
|
69
|
+
if (a.move) { if (ini === null) ini = a.t; ult = a; continue; }
|
|
70
|
+
if (ult && ini !== null && ult.t - ini >= minAndou - 1e-9 && a.t - ult.t >= minParado - 1e-9) {
|
|
71
|
+
out.push({ t: ult.t, x: ult.x, y: ult.y });
|
|
72
|
+
ini = null;
|
|
73
|
+
ult = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── os trechos e a câmera ────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Focos (t em QUADROS do clipe) → trechos [{de, ate, focos}]. Fecha o trecho
|
|
83
|
+
* na pausa maior que `juntar` ou no salto maior que `longe` quando há tempo
|
|
84
|
+
* de sair e voltar (2 rampas); salto rápido fica no mesmo trecho e vira pan.
|
|
85
|
+
*/
|
|
86
|
+
function trechos(fs, { fps, juntar = 2.5, longe = 0.35, antes = 0.4, segura = 1.2, rampa = 0.5 } = {}) {
|
|
87
|
+
const out = [];
|
|
88
|
+
const R = rampa * fps;
|
|
89
|
+
for (const f of [...fs].sort((a, b) => a.t - b.t)) {
|
|
90
|
+
const u = out[out.length - 1];
|
|
91
|
+
const ult = u && u.focos[u.focos.length - 1];
|
|
92
|
+
const gap = ult ? f.t - ult.t : Infinity;
|
|
93
|
+
if (u && gap <= juntar * fps && !(Math.hypot(f.x - ult.x, f.y - ult.y) > longe && gap > 2 * R + segura * fps)) { u.focos.push(f); continue; }
|
|
94
|
+
out.push({ focos: [f] });
|
|
95
|
+
}
|
|
96
|
+
for (const s of out) {
|
|
97
|
+
s.de = Math.max(0, Math.round(s.focos[0].t - antes * fps - R));
|
|
98
|
+
s.ate = Math.round(s.focos[s.focos.length - 1].t + segura * fps);
|
|
99
|
+
}
|
|
100
|
+
// Um trecho não invade a saída do anterior.
|
|
101
|
+
for (let i = 1; i < out.length; i++) out[i - 1].ate = Math.max(out[i - 1].focos[out[i - 1].focos.length - 1].t, Math.min(out[i - 1].ate, out[i].de - R));
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A câmera, quadro a quadro: [{z, x, y}] para `dur` quadros. `pos(f)` = onde
|
|
107
|
+
* o cursor está no quadro f (ou null). Zona morta = fração da meia-vista.
|
|
108
|
+
*/
|
|
109
|
+
function camera(dur, ts, pos, { fps, zoom = 1.8, rampa = 0.5, zonaMorta = 0.45, hz = 1.4 } = {}) {
|
|
110
|
+
const R = Math.max(1, Math.round(rampa * fps));
|
|
111
|
+
const out = [];
|
|
112
|
+
const w = 2 * Math.PI * hz;
|
|
113
|
+
const dt = 1 / fps;
|
|
114
|
+
let cam = { x: 0.5, y: 0.5 };
|
|
115
|
+
let vel = { x: 0, y: 0 };
|
|
116
|
+
let alvo = { ...cam };
|
|
117
|
+
let s = null;
|
|
118
|
+
for (let f = 0; f < dur; f++) {
|
|
119
|
+
const novo = ts.find((t) => f >= t.de && f < t.ate + R);
|
|
120
|
+
if (novo !== s) {
|
|
121
|
+
s = novo;
|
|
122
|
+
if (s) { cam = { x: s.focos[0].x, y: s.focos[0].y }; alvo = { ...cam }; vel = { x: 0, y: 0 }; }
|
|
123
|
+
}
|
|
124
|
+
let z = 1;
|
|
125
|
+
if (s) {
|
|
126
|
+
const p = f < s.de + R ? (f - s.de) / R : f >= s.ate ? 1 - (f - s.ate) / R : 1;
|
|
127
|
+
z = 1 + (zoom - 1) * p * p * (3 - 2 * p);
|
|
128
|
+
// Segue o cursor só entre o primeiro e o último foco: antes, mira o foco; depois, segura.
|
|
129
|
+
const c = f >= s.focos[0].t && f <= s.focos[s.focos.length - 1].t ? pos(f) : null;
|
|
130
|
+
const meia = 0.5 / zoom;
|
|
131
|
+
if (c) {
|
|
132
|
+
for (const k of ['x', 'y']) {
|
|
133
|
+
const d = c[k] - alvo[k];
|
|
134
|
+
const zm = zonaMorta * meia;
|
|
135
|
+
if (Math.abs(d) > zm) alvo[k] = c[k] - Math.sign(d) * zm;
|
|
136
|
+
alvo[k] = lim(alvo[k], meia, 1 - meia);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of ['x', 'y']) {
|
|
140
|
+
vel[k] += (w * w * (alvo[k] - cam[k]) - 2 * w * vel[k]) * dt; // mola crítica: chega sem passar
|
|
141
|
+
cam[k] += vel[k] * dt;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const meiaZ = 0.5 / z;
|
|
145
|
+
out.push({ z, x: lim(cam.x, meiaZ, 1 - meiaZ), y: lim(cam.y, meiaZ, 1 - meiaZ) });
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Ramer–Douglas–Peucker numa série: os quadros que bastam para a reta não errar mais que `tol`. */
|
|
151
|
+
function enxugar(v, tol) {
|
|
152
|
+
const manter = new Set([0, v.length - 1]);
|
|
153
|
+
const pilha = [[0, v.length - 1]];
|
|
154
|
+
while (pilha.length) {
|
|
155
|
+
const [a, b] = pilha.pop();
|
|
156
|
+
let pior = -1;
|
|
157
|
+
let max = tol;
|
|
158
|
+
for (let i = a + 1; i < b; i++) {
|
|
159
|
+
const d = Math.abs(v[i] - (v[a] + ((v[b] - v[a]) * (i - a)) / (b - a)));
|
|
160
|
+
if (d > max) { max = d; pior = i; }
|
|
161
|
+
}
|
|
162
|
+
if (pior >= 0) { manter.add(pior); pilha.push([a, pior], [pior, b]); }
|
|
163
|
+
}
|
|
164
|
+
return [...manter].sort((x, y) => x - y);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Câmera → keyframes {zoom, fx, fy} em quadros do clipe ({frame, value, interp}). */
|
|
168
|
+
function keyframes(cam, { tol = 0.002 } = {}) {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const [prop, k] of [['zoom', 'z'], ['fx', 'x'], ['fy', 'y']]) {
|
|
171
|
+
const v = cam.map((c) => c[k]);
|
|
172
|
+
out[prop] = enxugar(v, prop === 'zoom' ? 0.01 : tol).map((f) => ({ frame: f, value: Math.round(v[f] * 10000) / 10000, interp: 'linear' }));
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Tudo junto. `amostras` com t em QUADROS do clipe e x,y em 0–1 do clipe.
|
|
179
|
+
* Devolve {focos, trechos, cam, keys}.
|
|
180
|
+
*/
|
|
181
|
+
function planejar(amostras, dur, opts) {
|
|
182
|
+
const fs = focos(amostras.map((a) => ({ ...a, t: a.t / opts.fps })), opts).map((f) => ({ ...f, t: Math.round(f.t * opts.fps) }));
|
|
183
|
+
const ts = trechos(fs, opts);
|
|
184
|
+
const comPos = amostras.filter((a) => a.x !== null && a.x !== undefined);
|
|
185
|
+
let i = 0;
|
|
186
|
+
const pos = (f) => { // a última posição conhecida até f (f só cresce)
|
|
187
|
+
while (i + 1 < comPos.length && comPos[i + 1].t <= f) i++;
|
|
188
|
+
return comPos[i] && comPos[i].t <= f ? comPos[i] : null;
|
|
189
|
+
};
|
|
190
|
+
const cam = camera(dur, ts, pos, opts);
|
|
191
|
+
return { focos: fs, trechos: ts, cam, keys: keyframes(cam) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── o ffmpeg mede ────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/** Quadros cinza pequenos da origem [de, ate) segundos, a `fps` por segundo. */
|
|
197
|
+
function quadrosCinza(arquivo, de, ate, { fps = 15, w = 160, h = 90 } = {}) {
|
|
198
|
+
const video = require('../estudio/video.js');
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const p = spawn(video.ffmpeg(), ['-hide_banner', '-loglevel', 'error', '-ss', String(de), '-t', String(ate - de), '-i', arquivo,
|
|
201
|
+
'-vf', `fps=${fps},scale=${w}:${h}:flags=area,format=gray`, '-f', 'rawvideo', '-']);
|
|
202
|
+
const partes = [];
|
|
203
|
+
p.stdout.on('data', (d) => partes.push(d));
|
|
204
|
+
p.on('error', reject);
|
|
205
|
+
p.on('close', (code) => {
|
|
206
|
+
if (code !== 0) return reject(new Error('não consegui ler os quadros'));
|
|
207
|
+
const b = Buffer.concat(partes);
|
|
208
|
+
const out = [];
|
|
209
|
+
for (let k = 0; k + w * h <= b.length; k += w * h) out.push(b.subarray(k, k + w * h));
|
|
210
|
+
resolve(out);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── a operação do editor ─────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/** Ponto da ORIGEM (0–1) → ponto do clipe (0–1), depois do encaixe (cover/contain). */
|
|
218
|
+
function paraClipe(tl, clip, asset, x, y) {
|
|
219
|
+
if (clip.fit === 'stretch') return { x, y };
|
|
220
|
+
const bw = clip.transform.w * tl.width;
|
|
221
|
+
const bh = clip.transform.h * tl.height;
|
|
222
|
+
const aw = asset.width || bw;
|
|
223
|
+
const ah = asset.height || bh;
|
|
224
|
+
const esc = clip.fit === 'contain' ? Math.min(bw / aw, bh / ah) : Math.max(bw / aw, bh / ah);
|
|
225
|
+
return { x: lim(0.5 + ((x - 0.5) * aw * esc) / bw, 0, 1), y: lim(0.5 + ((y - 0.5) * ah * esc) / bh, 0, 1) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const OPS = {
|
|
229
|
+
auto_zoom: {
|
|
230
|
+
uso: 'auto_zoom {id?,zoom?:1.8,cursor?:[{t(s da origem),x,y(0–1),click?}]|cursorFile?,deadZone?:0.45,remove?} — gravação de tela: entra no clique (sem cursor: onde o movimento para), segue com zona morta e mola, sai na pausa',
|
|
231
|
+
async run(ctx, a) {
|
|
232
|
+
const T = require('./timeline.js');
|
|
233
|
+
const E = require('./edits.js');
|
|
234
|
+
const { mutar } = require('./ops.js');
|
|
235
|
+
if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."}');
|
|
236
|
+
const primeiro = T.todosOsClipes(ctx.tl).find((x) => x.clip.type === 'video');
|
|
237
|
+
if (!a.id && !primeiro) throw T.erro('não há clipe de vídeo');
|
|
238
|
+
const { clip } = T.achar(ctx.tl, a.id || primeiro.clip.id);
|
|
239
|
+
if (a.remove) return mutar(ctx, 'auto_zoom', (tl) => { for (const p of ['zoom', 'fx', 'fy']) E.keyframes(tl, clip.id, p, []); });
|
|
240
|
+
const asset = ctx.tl.assets[clip.asset];
|
|
241
|
+
const fps = ctx.tl.fps;
|
|
242
|
+
const { de, ate } = T.janelaOrigem(ctx.tl, clip);
|
|
243
|
+
let cru;
|
|
244
|
+
let fonte;
|
|
245
|
+
if (a.cursor || a.cursorFile) {
|
|
246
|
+
cru = doCursor(a.cursor || JSON.parse(require('fs').readFileSync(a.cursorFile, 'utf8')));
|
|
247
|
+
fonte = cru.some((x) => x.click) ? 'cliques' : 'cursor';
|
|
248
|
+
} else {
|
|
249
|
+
const mfps = 15;
|
|
250
|
+
const h = 2 * Math.round((80 * (asset.height || 9)) / (asset.width || 16));
|
|
251
|
+
cru = movimento(await quadrosCinza(asset.path, de, ate, { fps: mfps, w: 160, h }), 160, h, mfps).map((x) => ({ ...x, t: x.t + de }));
|
|
252
|
+
fonte = 'movimento';
|
|
253
|
+
}
|
|
254
|
+
const amostras = [];
|
|
255
|
+
for (const x of cru) {
|
|
256
|
+
const f = T.origemParaQuadro(ctx.tl, clip, x.t);
|
|
257
|
+
if (f === null) continue;
|
|
258
|
+
const p = x.x === null ? { x: null, y: null } : paraClipe(ctx.tl, clip, asset, x.x, x.y);
|
|
259
|
+
amostras.push({ ...x, ...p, t: f - clip.startFrame });
|
|
260
|
+
}
|
|
261
|
+
const pl = planejar(amostras, clip.durationFrames, { fps, zoom: lim(Number(a.zoom) || 1.8, 1.1, 3), zonaMorta: a.deadZone ?? 0.45 });
|
|
262
|
+
const zooms = pl.trechos.map((t) => [t.de + clip.startFrame, t.ate + clip.startFrame]);
|
|
263
|
+
if (!pl.focos.length) return { ok: true, nada: true, fonte, focos: 0, aviso: 'nenhum clique nem parada do cursor: nada para aproximar' };
|
|
264
|
+
return mutar(ctx, 'auto_zoom', (tl) => {
|
|
265
|
+
for (const [p, ks] of Object.entries(pl.keys)) E.keyframes(tl, clip.id, p, ks.map((k) => ({ ...k, frame: k.frame + clip.startFrame })));
|
|
266
|
+
return { fonte, focos: pl.focos.length, zooms };
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Sem linha de exemplo: a ajuda principal cabe em 60 linhas, e o `uso` já mostra a forma.
|
|
273
|
+
const EX = {};
|
|
274
|
+
|
|
275
|
+
module.exports = { movimento, doCursor, focos, trechos, camera, enxugar, keyframes, planejar, quadrosCinza, paraClipe, OPS, EX };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.0
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/hibrido.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/revisor.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -25,4 +25,7 @@ superlativo sem número.
|
|
|
25
25
|
Número concreto vence adjetivo: "responde em 2 segundos" vale mais que
|
|
26
26
|
"ultrarrápido".
|
|
27
27
|
|
|
28
|
+
**Revisando um texto pronto**: aponte o trecho que soa errado e reescreva só
|
|
29
|
+
ele. O resto, que já funciona, fica como está.
|
|
30
|
+
|
|
28
31
|
Não use emoji para dar emoção a um texto que não tem nenhuma.
|
|
@@ -12,7 +12,8 @@ a correção. Primeiro o que impede alguém de usar; acabamento vai no fim.
|
|
|
12
12
|
- O que é clicável é `button` ou `a`, com hover, estado pressionado e foco
|
|
13
13
|
visível (`:focus-visible`).
|
|
14
14
|
- Alvo de toque de pelo menos 44×44px. Nada importante escondido só no hover.
|
|
15
|
-
-
|
|
15
|
+
- Todo clique responde em até 200 ms. Ação que demora mostra que está
|
|
16
|
+
trabalhando e não pode ser enviada duas vezes.
|
|
16
17
|
- Ação que apaga pede confirmação ou oferece desfazer.
|
|
17
18
|
- O endereço guarda o estado (aba, filtro, página): o voltar do navegador e o
|
|
18
19
|
link compartilhado levam ao mesmo lugar.
|
|
@@ -32,6 +33,7 @@ novo) e cheio (texto longo quebra ou termina em `…`; mil itens não travam).
|
|
|
32
33
|
**Layout**
|
|
33
34
|
- De 360px a 1440px sem rolagem para o lado; respeita a área segura do celular
|
|
34
35
|
(`env(safe-area-inset-bottom)`).
|
|
36
|
+
- Tela cheia é `100dvh`, nunca `100vh` (no celular, `vh` some sob a barra).
|
|
35
37
|
- Imagem com `width`/`height` ou `aspect-ratio`, para a página não pular.
|
|
36
38
|
- Número em coluna com `tabular-nums`; título com `text-wrap: balance`.
|
|
37
39
|
|
|
@@ -43,5 +45,9 @@ rolagem acompanham) e contraste de 4.5:1 nos dois temas. Animação só em
|
|
|
43
45
|
`transform` e `opacity`, curta (150 a 300ms) e desligada em
|
|
44
46
|
`prefers-reduced-motion`. Nunca `transition: all`.
|
|
45
47
|
|
|
48
|
+
**Pior caso** (teste com o dado ruim): Texto enorme (`min-width: 0` e
|
|
49
|
+
`overflow-wrap: anywhere`); Lista vazia; largura de 320px; Rede lenta (DevTools,
|
|
50
|
+
Slow 3G); Mil itens (paginar ou virtualizar).
|
|
51
|
+
|
|
46
52
|
**Feche com a contagem**: quantos impedem o uso, quantos atrapalham, quantos são
|
|
47
53
|
acabamento. E conserte nessa mesma ordem.
|
|
@@ -57,11 +57,9 @@ Em vídeo de produto, anúncio e vinheta, escreva **uma cena de 8 a 15 s** no
|
|
|
57
57
|
- **A troca é no mesmo lugar e instante**: o foguete some onde o ícone nasce; o
|
|
58
58
|
app nasce do tamanho e da COR do ícone.
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Pedido de motion (ou sem referências): `estilo: "gerativo"`, só o roteiro
|
|
64
|
-
(`fala`, `destaque`); `roteiro.semente` sorteia outro, `modelo3d` traz o 3D.
|
|
60
|
+
Sem imagem é o padrão (foto só se pedirem). Decida `clima`, o impacto (`destaque`, ≤1 em 3 partes), o
|
|
61
|
+
respiro e o `estilo` deste roteiro: `gerativo`, `fluido`, `impacto`, `vox`, `marca`, `lancamento`; vazio, gira.
|
|
62
|
+
`hibrido` só com imagem pedida. Leia os avisos da revisão.
|
|
65
63
|
|
|
66
64
|
## Cor e tipo
|
|
67
65
|
|
|
@@ -27,6 +27,12 @@ Tudo pelo `gh` (GitHub CLI) e pelo `git`, rodando no terminal do projeto.
|
|
|
27
27
|
4. `gh pr create --fill` (ou `--title` e `--body` dizendo o que e por quê).
|
|
28
28
|
Diga o link do PR.
|
|
29
29
|
|
|
30
|
+
**Segredo:** `.env` só existe na máquina. Nunca crie, edite ou renomeie
|
|
31
|
+
`.env` pelo site do GitHub (renomear `.env.example` para `.env` lá já vazou
|
|
32
|
+
chave em repositório público). O `.env.example` leva só os nomes, sem valor.
|
|
33
|
+
Antes do push: `git ls-files | grep '\.env$'` tem de vir vazio. Vazou? Troque a
|
|
34
|
+
chave na hora; apagar o arquivo não basta, ela fica no histórico.
|
|
35
|
+
|
|
30
36
|
**Nunca** faça `push --force` em `main`, nunca apague branch dos outros, nunca
|
|
31
37
|
commite `.env`. Conflito: resolva lendo os dois lados, não escolhendo um às
|
|
32
38
|
cegas.
|
|
@@ -31,6 +31,9 @@ ela quem entra na conta dela.
|
|
|
31
31
|
**3. Confira:** abra a URL publicada com browser_open e leia a página. Só diga
|
|
32
32
|
que está no ar se ela abriu com o conteúdo certo. Termine dizendo o LINK.
|
|
33
33
|
|
|
34
|
+
**Link compartilhável:** `og:image` com URL absoluta (`https://.../capa.png`),
|
|
35
|
+
nunca `/capa.png`; sem isso o link sai sem imagem no WhatsApp.
|
|
36
|
+
|
|
34
37
|
**Nunca** publique `.env`, chave ou senha: confira se `.gitignore` e o
|
|
35
38
|
`.vercelignore`/`.netlifyignore` os deixam de fora. Variáveis secretas vão no
|
|
36
39
|
painel do serviço (ou `vercel env add`), não no código.
|
|
@@ -26,6 +26,8 @@ tudo com a mesma duração; `transition: all`; texto piscando; nada que responda
|
|
|
26
26
|
que acompanha o scroll (parallax, barra de progresso, imagem que cresce), use
|
|
27
27
|
`animation-timeline: view()` e deixe a página parada onde isso não existe (`@supports`).
|
|
28
28
|
- **Só `transform` e `opacity`.** Animar `top`, `width`, `height` ou `box-shadow` trava no celular.
|
|
29
|
+
Confira com grep: zero `transition: all` no CSS.
|
|
30
|
+
- **Seção de tela cheia** usa `100dvh`, e não `100vh`, pra não ficar atrás da barra do celular.
|
|
29
31
|
- **Troca de página ou de estado:** a View Transitions API (`document.startViewTransition`), com o
|
|
30
32
|
caminho sem animação quando o navegador não tiver.
|
|
31
33
|
- **Biblioteca só se precisar.** CSS e um pouco de JS resolvem quase tudo. Timeline longa ou texto
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""voz_kokoro.py — a voz local, sem cota e sem servidor (Kokoro-82M, na CPU).
|
|
3
|
+
|
|
4
|
+
O Gemini é a voz de gente, mas a cota grátis é pequena e às vezes o áudio
|
|
5
|
+
volta cortado (medido em 25/09). O Kokoro roda nesta máquina: 11 s de áudio
|
|
6
|
+
em 3,5 s, mais 4,3 s para carregar o modelo, sem corte no fim (05/10).
|
|
7
|
+
|
|
8
|
+
Roda pelo `uv` (lib/estudio/kokoro.js monta o comando), com as versões
|
|
9
|
+
fixadas: sem `transformers>=4.45` o uv escolhe a 4.12 e o tokenizers não
|
|
10
|
+
compila; sem ESPEAK_DATA_PATH o espeak embutido aponta para uma pasta que
|
|
11
|
+
não existe.
|
|
12
|
+
|
|
13
|
+
Não se chama kokoro.py: a pasta do script entra no sys.path e o nome
|
|
14
|
+
esconderia o pacote `kokoro` (medido em 05/10).
|
|
15
|
+
|
|
16
|
+
Uso (a mesma entrada e a mesma saída do `narrar.py roteiro`):
|
|
17
|
+
voz_kokoro.py roteiro --arquivo partes.json --saida narracao.mp3 [--voz pf_dora]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
TAXA = 24000
|
|
29
|
+
RESPIRO = 0.12 # o mesmo respiro curto entre as partes do narrar.py
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _palavras(texto, de, ate):
|
|
33
|
+
"""O Kokoro em português não devolve o tempo de cada palavra: ele é
|
|
34
|
+
repartido pelo tamanho da palavra dentro da parte (melhor que nada para
|
|
35
|
+
a tipografia seguir a voz)."""
|
|
36
|
+
ws = texto.split()
|
|
37
|
+
total = sum(len(w) for w in ws) or 1
|
|
38
|
+
out, t = [], de
|
|
39
|
+
for w in ws:
|
|
40
|
+
d = (ate - de) * len(w) / total
|
|
41
|
+
out.append({"de": round(t, 3), "ate": round(t + d, 3), "texto": w})
|
|
42
|
+
t += d
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def comando_roteiro(args):
|
|
47
|
+
import numpy as np
|
|
48
|
+
import soundfile as sf
|
|
49
|
+
from kokoro import KPipeline
|
|
50
|
+
|
|
51
|
+
partes = json.loads(Path(args.arquivo).read_text(encoding="utf-8"))
|
|
52
|
+
if not partes:
|
|
53
|
+
return {"ok": False, "error": "o roteiro veio vazio"}
|
|
54
|
+
pipe = KPipeline(lang_code="p") # português do Brasil
|
|
55
|
+
pausa = np.zeros(int(TAXA * RESPIRO), dtype=np.float32)
|
|
56
|
+
blocos, trechos, palavras, t = [], [], [], 0.0
|
|
57
|
+
for parte in partes:
|
|
58
|
+
texto = re.sub(r"\s+", " ", str(parte.get("texto") or "")).strip()
|
|
59
|
+
if not texto:
|
|
60
|
+
trechos.append({"de": round(t, 3), "ate": round(t, 3), "palavras": []})
|
|
61
|
+
continue
|
|
62
|
+
audio = np.concatenate([np.asarray(a, dtype=np.float32) for _, _, a in pipe(texto, voice=args.voz)])
|
|
63
|
+
dur = len(audio) / TAXA
|
|
64
|
+
ws = _palavras(texto, t, t + dur)
|
|
65
|
+
palavras += ws
|
|
66
|
+
trechos.append({"de": round(t, 3), "ate": round(t + dur, 3), "palavras": ws})
|
|
67
|
+
blocos += [audio, pausa]
|
|
68
|
+
t += dur + RESPIRO
|
|
69
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
70
|
+
wav = Path(tmp) / "voz.wav"
|
|
71
|
+
sf.write(str(wav), np.concatenate(blocos), TAXA)
|
|
72
|
+
r = subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(wav),
|
|
73
|
+
"-af", "loudnorm=I=-14:TP=-1.5:LRA=9", "-c:a", "libmp3lame", "-q:a", "3",
|
|
74
|
+
str(args.saida)], capture_output=True, timeout=180)
|
|
75
|
+
if r.returncode != 0:
|
|
76
|
+
return {"ok": False, "error": (r.stderr or b"").decode("utf-8", "ignore")[-300:]}
|
|
77
|
+
return {"ok": True, "arquivo": str(args.saida), "motor": "kokoro", "voz": f"kokoro {args.voz}",
|
|
78
|
+
"segundos": round(t, 3), "partes": trechos, "palavras": palavras}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main():
|
|
82
|
+
p = argparse.ArgumentParser(description="Narração local do PrimoCode Studio (Kokoro)")
|
|
83
|
+
sub = p.add_subparsers(dest="comando", required=True)
|
|
84
|
+
r = sub.add_parser("roteiro")
|
|
85
|
+
r.add_argument("--arquivo", required=True)
|
|
86
|
+
r.add_argument("--saida", required=True)
|
|
87
|
+
r.add_argument("--voz", default="pm_alex")
|
|
88
|
+
args = p.parse_args()
|
|
89
|
+
try:
|
|
90
|
+
saida = comando_roteiro(args)
|
|
91
|
+
except Exception as e: # nunca derruba a corrente: o JS passa para a próxima voz
|
|
92
|
+
saida = {"ok": False, "error": f"o kokoro falhou: {str(e)[:300]}"}
|
|
93
|
+
print(json.dumps(saida, ensure_ascii=False))
|
|
94
|
+
sys.exit(0 if saida.get("ok") else 1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|