primocode 8.42.1 → 9.0.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.
Files changed (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +599 -327
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
@@ -1,510 +0,0 @@
1
- /* Onde estão as pausas — a conta que permite cortá-las.
2
- *
3
- * "Ele vai cortar as pausas demoradas no vídeo."
4
- *
5
- * Cortar pausa é escolher trechos, e escolher trechos exige saber onde a fala
6
- * para. Ninguém no PrimoCode sabe ler o áudio de um .mp4: o projeto não tem
7
- * dependência nenhuma, e o Python da máquina lê WAV e mais nada. O navegador
8
- * sabe, desde sempre, em qualquer sistema, sem instalar coisa alguma. Então a
9
- * conta acontece aqui e o resultado volta para o agente pelo servidor.
10
- *
11
- * O método é o de sempre em edição: envelope de energia, limiar relativo ao
12
- * que o próprio arquivo tem, e histerese para não picotar a fala.
13
- */
14
- (function () {
15
- "use strict";
16
-
17
- var painel = document.getElementById("painel");
18
- var barra = document.getElementById("barra");
19
-
20
- function conta(texto) {
21
- var p = document.createElement("p");
22
- p.textContent = texto;
23
- painel.appendChild(p);
24
- painel.scrollTop = painel.scrollHeight;
25
- }
26
-
27
- function andar(fracao, rotulo) {
28
- barra.style.width = Math.round(Math.max(0, Math.min(1, fracao)) * 100) + "%";
29
- barra.setAttribute("data-rotulo", rotulo || "");
30
- }
31
-
32
- function mmss(s) {
33
- s = Math.max(0, s || 0);
34
- var m = Math.floor(s / 60);
35
- return m + ":" + String(Math.floor(s % 60)).padStart(2, "0");
36
- }
37
-
38
- /* ── o envelope ─────────────────────────────────────────────────────────
39
- RMS em janelas de 20ms. Pico não serve: um estalo de teclado tem pico de
40
- fala e energia de nada, e cortaria a pausa ao meio. */
41
- var JANELA = 0.02;
42
-
43
- function envelope(buffer) {
44
- var canais = buffer.numberOfChannels;
45
- var passo = Math.max(1, Math.round(buffer.sampleRate * JANELA));
46
- var quantas = Math.floor(buffer.length / passo);
47
- var fora = new Float32Array(quantas);
48
- var dados = [];
49
- for (var c = 0; c < canais; c++) dados.push(buffer.getChannelData(c));
50
-
51
- for (var j = 0; j < quantas; j++) {
52
- var soma = 0;
53
- var base = j * passo;
54
- for (var c2 = 0; c2 < canais; c2++) {
55
- var d = dados[c2];
56
- for (var i = 0; i < passo; i++) { var v = d[base + i]; soma += v * v; }
57
- }
58
- fora[j] = Math.sqrt(soma / (passo * canais));
59
- }
60
- return fora;
61
- }
62
-
63
- /* O limiar sai do PRÓPRIO arquivo. Um número fixo (-40 dB, digamos) trata
64
- igual uma gravação de estúdio e um celular num café — e no café ele
65
- acharia que não há pausa nenhuma, porque o chão de ruído já passa do
66
- limiar. Aqui o chão é a mediana das janelas mais quietas, e a fala é o
67
- percentil alto; o corte fica entre os dois, mais perto do chão. */
68
- function limiarDe(env) {
69
- var ordenado = Float32Array.from(env).sort();
70
- var q = function (p) { return ordenado[Math.min(ordenado.length - 1, Math.floor(ordenado.length * p))]; };
71
- var chao = q(0.10);
72
- var fala = q(0.90);
73
- if (!(fala > 0)) return { corte: 0, chao: chao, fala: fala };
74
- // Um sexto do caminho entre o silêncio e a fala. Baixo de propósito: é
75
- // melhor deixar uma pausa passar do que cortar a última sílaba de uma
76
- // frase, que é o defeito que se ouve.
77
- return { corte: Math.max(chao * 2.2, chao + (fala - chao) * 0.16), chao: chao, fala: fala };
78
- }
79
-
80
- /* Junta as janelas quietas em pausas, com histerese: uma janela alta no meio
81
- do silêncio não reabre a fala, e vice-versa. `minimo` é o que conta como
82
- "pausa demorada" — abaixo disso é respiração, e cortar respiração deixa a
83
- fala robótica. */
84
- function acharPausas(env, minimo, corte) {
85
- var pausas = [];
86
- var inicio = -1;
87
- var seguidas = 0;
88
- var TOLERA = 3; // ~60ms de estalo não encerram a pausa
89
- for (var j = 0; j < env.length; j++) {
90
- if (env[j] < corte) {
91
- if (inicio < 0) inicio = j;
92
- seguidas = 0;
93
- } else if (inicio >= 0) {
94
- seguidas++;
95
- if (seguidas > TOLERA) {
96
- var fim = j - seguidas;
97
- if ((fim - inicio) * JANELA >= minimo) pausas.push([inicio * JANELA, fim * JANELA]);
98
- inicio = -1; seguidas = 0;
99
- }
100
- }
101
- }
102
- if (inicio >= 0 && (env.length - inicio) * JANELA >= minimo) {
103
- pausas.push([inicio * JANELA, env.length * JANELA]);
104
- }
105
- return pausas;
106
- }
107
-
108
- /* ── O ERRO DE FALA, E A REPETIÇÃO ────────────────────────────────────
109
- *
110
- * "O sistema não identifica quando o usuário erra uma fala ou repete
111
- * algo — deveria cortar esses trechos, mas não corta."
112
- *
113
- * Não identificava porque não havia nada aqui: a análise só sabia achar
114
- * silêncio. Isto é a parte nova.
115
- *
116
- * ── COMO DÁ PARA SABER SEM ENTENDER O QUE FOI DITO ──────────────────
117
- * Não há transcrição. Mas errar e repetir tem uma marca ACÚSTICA que não
118
- * depende de entender: quando alguém tropeça, ele volta e diz a MESMA
119
- * COISA logo em seguida — "o Primo— o PrimoCode é um agente". As duas
120
- * tentativas ficam coladas e soam parecidas, porque são as mesmas palavras
121
- * na mesma voz, dita do mesmo jeito.
122
- *
123
- * Então o que se mede é a semelhança entre um trecho e o COMEÇO do
124
- * seguinte. Alta semelhança + o primeiro sendo mais curto = tentativa
125
- * abortada, e quem vale é a segunda.
126
- *
127
- * ── POR QUE FORMA DO ESPECTRO, E NÃO O ENVELOPE ─────────────────────
128
- * O envelope (a energia) já existe aqui e seria de graça — mas ele só diz
129
- * QUANTO som há, não QUAL. Duas frases diferentes ditas no mesmo ritmo têm
130
- * envelopes quase idênticos, e cortar por isso apagaria fala boa: o
131
- * defeito mais caro possível, porque some conteúdo sem ninguém ver.
132
- *
133
- * A forma do espectro diz o que está soando. Oito faixas por janela, cada
134
- * janela normalizada — assim o que se compara é o TIMBRE, não o volume, e
135
- * a pessoa falando mais alto na segunda vez continua sendo a mesma frase.
136
- */
137
-
138
- /* FFT radix-2, no lugar. Trinta linhas e nenhuma dependência: o estúdio
139
- inteiro roda sem `node_modules`, e uma biblioteca de áudio aqui seria a
140
- primeira. */
141
- function fft(re, im) {
142
- var n = re.length;
143
- for (var i = 1, j = 0; i < n; i++) {
144
- var bit = n >> 1;
145
- for (; j & bit; bit >>= 1) j ^= bit;
146
- j ^= bit;
147
- if (i < j) {
148
- var tr = re[i]; re[i] = re[j]; re[j] = tr;
149
- var ti = im[i]; im[i] = im[j]; im[j] = ti;
150
- }
151
- }
152
- for (var tam = 2; tam <= n; tam <<= 1) {
153
- var ang = -2 * Math.PI / tam;
154
- var wr = Math.cos(ang), wi = Math.sin(ang);
155
- for (var k = 0; k < n; k += tam) {
156
- var cr = 1, ci = 0;
157
- for (var m = 0; m < tam / 2; m++) {
158
- var ar = re[k + m], ai = im[k + m];
159
- var br = re[k + m + tam / 2] * cr - im[k + m + tam / 2] * ci;
160
- var bi = re[k + m + tam / 2] * ci + im[k + m + tam / 2] * cr;
161
- re[k + m] = ar + br; im[k + m] = ai + bi;
162
- re[k + m + tam / 2] = ar - br; im[k + m + tam / 2] = ai - bi;
163
- var ncr = cr * wr - ci * wi;
164
- ci = cr * wi + ci * wr; cr = ncr;
165
- }
166
- }
167
- }
168
- }
169
-
170
- var JANELA_FFT = 1024; // ~23ms a 44,1kHz: uma sílaba tem várias
171
- var FAIXAS = 8;
172
-
173
- var PASSO_S = 0.02; // uma linha a cada 20ms
174
-
175
- /**
176
- * A "cara" de um pedaço do áudio: uma linha de 8 números por janela, mais a
177
- * FORÇA de cada linha antes de normalizar.
178
- *
179
- * A força não é enfeite: depois de normalizada, uma janela de silêncio vira
180
- * um vetor unitário apontando para o ruído — e dois ruídos podem "casar" tão
181
- * bem quanto duas falas iguais. É por ela que `aparar` sabe onde a fala
182
- * começa de verdade.
183
- */
184
- function impressao(dados, taxa, de, ate) {
185
- var passo = Math.round(taxa * PASSO_S);
186
- var i0 = Math.max(0, Math.round(de * taxa));
187
- var i1 = Math.min(dados.length, Math.round(ate * taxa));
188
- var linhas = [];
189
- var forca = [];
190
- var re = new Float64Array(JANELA_FFT);
191
- var im = new Float64Array(JANELA_FFT);
192
-
193
- for (var p = i0; p + JANELA_FFT <= i1; p += passo) {
194
- for (var k = 0; k < JANELA_FFT; k++) {
195
- // Hann: sem janela, o corte do bloco vira um clique que aparece em
196
- // todas as faixas e embaralha a comparação.
197
- var w = 0.5 * (1 - Math.cos(2 * Math.PI * k / (JANELA_FFT - 1)));
198
- re[k] = dados[p + k] * w;
199
- im[k] = 0;
200
- }
201
- fft(re, im);
202
-
203
- var faixa = new Float64Array(FAIXAS);
204
- // Faixas em escala logarítmica: o ouvido separa grave de agudo assim, e
205
- // a fala vive quase toda embaixo de 4kHz.
206
- for (var b = 0; b < FAIXAS; b++) {
207
- var de2 = Math.floor(Math.pow(2, b) * 2);
208
- var ate2 = Math.min(JANELA_FFT / 2, Math.floor(Math.pow(2, b + 1) * 2));
209
- var soma = 0;
210
- for (var q = de2; q < ate2; q++) soma += re[q] * re[q] + im[q] * im[q];
211
- faixa[b] = Math.log(1 + soma);
212
- }
213
- // Normaliza a linha: o que importa é a FORMA, não o volume. Sem isto,
214
- // falar mais alto na segunda tentativa faria as duas parecerem
215
- // diferentes — justamente quando são a mesma frase.
216
- var norma = 0;
217
- for (var b2 = 0; b2 < FAIXAS; b2++) norma += faixa[b2] * faixa[b2];
218
- norma = Math.sqrt(norma) || 1;
219
- for (var b3 = 0; b3 < FAIXAS; b3++) faixa[b3] /= norma;
220
- linhas.push(faixa);
221
- forca.push(norma);
222
- }
223
- return { linhas: linhas, forca: forca };
224
- }
225
-
226
- /**
227
- * Tira o silêncio das pontas.
228
- *
229
- * Sem isto a comparação é entre coisas desalinhadas e o detector não acha
230
- * nada: o trecho guarda meia pausa em cada ponta (é o conserto do vídeo
231
- * cansativo, logo abaixo), então o primeiro trecho começa na fala e o
232
- * segundo começa em 0,35s de ar — um deslocamento de dezessete janelas, que
233
- * é mais que uma sílaba inteira. Alinhar pelo COMEÇO DA FALA é o que faz as
234
- * duas tentativas ficarem uma em cima da outra.
235
- */
236
- function aparar(imp) {
237
- var f = imp.forca;
238
- if (!f.length) return [];
239
- var pico = 0;
240
- for (var i = 0; i < f.length; i++) if (f[i] > pico) pico = f[i];
241
- var corte = pico * 0.35;
242
- var de = 0, ate = f.length - 1;
243
- while (de < f.length && f[de] < corte) de++;
244
- while (ate > de && f[ate] < corte) ate--;
245
- return imp.linhas.slice(de, ate + 1);
246
- }
247
-
248
- /* Quanto se admite de folga no alinhamento: 6 janelas, ~120ms. Ninguém
249
- recomeça uma frase no mesmo milissegundo, e sem esta busca uma diferença
250
- de meia sílaba já derruba a semelhança abaixo de qualquer limiar. */
251
- var FOLGA = 6;
252
-
253
- /** Quanto duas impressões se parecem, 0 a 1, no melhor alinhamento. */
254
- function semelhanca(a, b) {
255
- var melhor = 0;
256
- for (var d = 0; d <= FOLGA; d++) {
257
- melhor = Math.max(melhor, umAlinhamento(a, b, d), umAlinhamento(b, a, d));
258
- }
259
- return melhor;
260
- }
261
-
262
- function umAlinhamento(a, b, desloca) {
263
- var n = Math.min(a.length, b.length - desloca);
264
- if (n < 8) return 0; // curto demais para significar algo
265
- var soma = 0;
266
- for (var i = 0; i < n; i++) {
267
- var s = 0;
268
- for (var k = 0; k < FAIXAS; k++) s += a[i][k] * b[i + desloca][k];
269
- soma += s;
270
- }
271
- return soma / n;
272
- }
273
-
274
- /* ── AS DUAS FORMAS DE ERRAR UMA FALA ─────────────────────────────────
275
- *
276
- * Elas não se parecem, e uma regra só não pega as duas:
277
- *
278
- * · O TROPEÇO. "O Primo— o PrimoCode é um agente de IA." A primeira
279
- * tentativa morre no meio, é CURTA, e o que vem depois começa igual e
280
- * segue em frente. O sinal é o PEDAÇO: o primeiro trecho é um começo do
281
- * segundo.
282
- *
283
- * · A REGRAVAÇÃO. A pessoa diz a frase inteira, não gosta, e diz de novo
284
- * inteira. Os dois trechos têm o MESMO tamanho e o mesmo conteúdo do
285
- * começo ao fim.
286
- *
287
- * ── O QUE SEGURA A MÃO ───────────────────────────────────────────────
288
- * Nenhuma das duas pode confundir com FALA BOA que por acaso se parece. Duas
289
- * frases seguidas da mesma pessoa, no mesmo microfone, sobre o mesmo assunto
290
- * já se parecem bastante — e apagar uma delas é o defeito mais caro deste
291
- * arquivo, porque some conteúdo e o vídeo continua parecendo certo. Por isso
292
- * cada regra pede DUAS coisas ao mesmo tempo: semelhança alta E uma relação
293
- * de tamanho que só existe no erro.
294
- *
295
- * Fala boa repetida de propósito ("e eu repito: não vale a pena") não é
296
- * cortada, e isso é decisão: quem repete de propósito repete com outras
297
- * palavras em volta, e o pedaço igual é curto demais para passar no mínimo
298
- * de janelas da `semelhanca`.
299
- */
300
-
301
- /* Acima disto, duas falas são a MESMA fala. Alto de propósito: errar para o
302
- lado de não cortar deixa um tropeço no vídeo, que se vê e se conserta;
303
- errar para o outro apaga fala boa, que ninguém vê faltar até alguém
304
- assistir inteiro. */
305
- var IGUAIS = 0.985;
306
- /* A regravação inteira pede mais, porque ali se joga fora um trecho INTEIRO
307
- de fala e não um pedaço de um segundo. */
308
- var IGUAIS_INTEIRA = 0.992;
309
- /* Uma tentativa abortada é curta — quem tropeça volta rápido. */
310
- var ABORTADA_MAXIMA = 3.5;
311
- /* E ela é um PEDAÇO do que vem depois. Sem esta conta, dois trechos de fala
312
- de verdade com o mesmo tamanho e o mesmo assunto viram "repetição". */
313
- var FRAGMENTO_ATE = 0.7;
314
- /* Regravação: os dois duram quase o mesmo. */
315
- var MESMO_TAMANHO = 0.15;
316
-
317
- /**
318
- * Quais trechos são tropeço, e por quê.
319
- *
320
- * Devolve os índices a descartar e a razão de cada um — a razão é o que
321
- * permite CONTAR na tela o que foi tirado, em vez de o vídeo simplesmente
322
- * sair menor sem explicação. Vídeo que encurta sozinho e não diz por quê é
323
- * indistinguível de vídeo quebrado.
324
- */
325
- function acharRepeticoes(trechos, dados, taxa) {
326
- var fora = [];
327
- for (var i = 0; i < trechos.length - 1; i++) {
328
- var a = trechos[i], b = trechos[i + 1];
329
- var da = a[1] - a[0], db = b[1] - b[0];
330
- if (da > ABORTADA_MAXIMA && Math.abs(da - db) / Math.max(da, db) > MESMO_TAMANHO) continue;
331
-
332
- var ia = aparar(impressao(dados, taxa, a[0], a[1]));
333
- if (ia.length < 8) continue;
334
-
335
- if (da <= ABORTADA_MAXIMA && da < db * FRAGMENTO_ATE) {
336
- // Tropeço: compara a tentativa com o COMEÇO do que vem depois, que é
337
- // onde a fala recomeça igual. A janela de b é o tamanho de a mais uma
338
- // folga — nela cabe o ar da ponta que `aparar` vai tirar.
339
- var ib = aparar(impressao(dados, taxa, b[0], Math.min(b[1], b[0] + da + 1.2)));
340
- var s = semelhanca(ia, ib);
341
- if (s >= IGUAIS) {
342
- fora.push({ indice: i, semelhanca: +s.toFixed(3), motivo: "tentativa abortada" });
343
- }
344
- continue;
345
- }
346
-
347
- if (Math.abs(da - db) / Math.max(da, db) <= MESMO_TAMANHO) {
348
- // Regravação: aqui a comparação é do começo ao FIM dos dois. Comparar
349
- // só o começo diria "igual" para duas frases que começam com as mesmas
350
- // três palavras e terminam dizendo coisas opostas.
351
- var ib2 = aparar(impressao(dados, taxa, b[0], b[1]));
352
- var s2 = semelhanca(ia, ib2);
353
- if (s2 >= IGUAIS_INTEIRA && Math.abs(ia.length - ib2.length) <= FOLGA * 3) {
354
- fora.push({ indice: i, semelhanca: +s2.toFixed(3), motivo: "fala regravada" });
355
- }
356
- }
357
- }
358
- return fora;
359
- }
360
-
361
- /* ── O CORTE QUE NÃO COME A PAUSA ──────────────────────────────────────
362
- *
363
- * "O sistema corta todas as pausas, inclusive as propositais, deixando o
364
- * vídeo cansativo."
365
- *
366
- * Estava certo. A versão anterior pulava a pausa INTEIRA: o trecho terminava
367
- * onde o silêncio começava e o próximo começava onde ele acabava. Toda
368
- * respiração sumia, e falar sem respirar cansa quem ouve — o vídeo ficava
369
- * correndo sem parar, do primeiro segundo ao último.
370
- *
371
- * O que cansa não é a pausa: é o ar parado. Então a pausa não é removida,
372
- * é ENCURTADA — e o corte sai do MEIO dela, deixando metade do que fica de
373
- * cada lado. Assim a fala continua com a queda natural no fim da frase e o
374
- * ataque limpo na frase seguinte, que é o que soa humano.
375
- *
376
- * Quanto fica depende do tamanho, porque pausa curta e pausa longa querem
377
- * dizer coisas diferentes:
378
- *
379
- * · até 1,5s — é respiração. Fica quase inteira (até 0,5s), porque foi
380
- * assim que a pessoa falou.
381
- * · acima disso — ou é ar parado, ou é uma pausa DE PROPÓSITO antes de
382
- * uma frase importante. Fica 0,7s: o suficiente para a intenção
383
- * sobreviver, sem o vazio.
384
- *
385
- * Quem quiser o corte seco de antes passa `manter: 0`.
386
- */
387
- var MANTER_CURTA = 0.5;
388
- var MANTER_LONGA = 0.7;
389
- var LIMITE_LONGA = 1.5;
390
-
391
- function quantoManter(duracaoDaPausa, pedido) {
392
- if (pedido != null) return Math.max(0, Math.min(pedido, duracaoDaPausa));
393
- var teto = duracaoDaPausa > LIMITE_LONGA ? MANTER_LONGA : MANTER_CURTA;
394
- return Math.min(teto, duracaoDaPausa);
395
- }
396
-
397
- function trechosUteis(pausas, duracao, manterPedido) {
398
- var trechos = [];
399
- var cursor = 0;
400
- pausas.forEach(function (p) {
401
- var tamanho = p[1] - p[0];
402
- var manter = quantoManter(tamanho, manterPedido);
403
- if (tamanho - manter <= 0.05) return; // nada a ganhar: deixa inteira
404
- var metade = manter / 2;
405
- var ate = Math.min(duracao, p[0] + metade);
406
- if (ate - cursor > 0.35) trechos.push([+cursor.toFixed(2), +ate.toFixed(2)]);
407
- cursor = Math.max(0, p[1] - metade);
408
- });
409
- if (duracao - cursor > 0.35) trechos.push([+cursor.toFixed(2), +duracao.toFixed(2)]);
410
- return trechos;
411
- }
412
-
413
- /* ── a tarefa ──────────────────────────────────────────────────────────── */
414
-
415
- function responder(id, corpo) {
416
- return fetch("/api/analise/" + id, {
417
- method: "PUT",
418
- headers: { "Content-Type": "application/json" },
419
- body: JSON.stringify(corpo),
420
- });
421
- }
422
-
423
- function rodar(id) {
424
- var tarefa;
425
- andar(0.05, "buscando o pedido");
426
-
427
- fetch("/api/analise/" + id).then(function (r) {
428
- if (!r.ok) throw new Error("este pedido de análise não existe mais");
429
- return r.json();
430
- }).then(function (t) {
431
- tarefa = t;
432
- document.getElementById("qual").textContent = t.arquivo;
433
- conta("Lendo " + t.arquivo + "…");
434
- andar(0.15, "baixando o áudio");
435
- return fetch(t.arquivo);
436
- }).then(function (r) {
437
- if (!r.ok) throw new Error("não consegui abrir o arquivo");
438
- return r.arrayBuffer();
439
- }).then(function (bruto) {
440
- conta((bruto.byteLength / 1048576).toFixed(1) + " MB lidos. Decodificando o áudio…");
441
- andar(0.35, "decodificando");
442
- var Ctx = window.AudioContext || window.webkitAudioContext;
443
- return new Ctx().decodeAudioData(bruto);
444
- }).then(function (buffer) {
445
- conta("Áudio de " + mmss(buffer.duration) + " a " + buffer.sampleRate + " Hz.");
446
- andar(0.6, "medindo a energia");
447
-
448
- var env = envelope(buffer);
449
- var lim = limiarDe(env);
450
- andar(0.85, "achando as pausas");
451
-
452
- var pausas = acharPausas(env, tarefa.pausa || 0.6, lim.corte);
453
- var todos = trechosUteis(pausas, buffer.duration, tarefa.manter);
454
- conta(pausas.length + " pausa(s) de mais de " + (tarefa.pausa || 0.6) + "s.");
455
-
456
- /* O erro de fala se procura DEPOIS das pausas, e não antes: é a pausa
457
- que separa a tentativa que morreu da que vale. Quem pedir
458
- `repetida: false` fica só com o corte de pausa — o comportamento
459
- antigo, para quem quer editar à mão. */
460
- andar(0.92, "ouvindo se algo foi dito duas vezes");
461
- var repetidos = tarefa.repetida === false ? []
462
- : acharRepeticoes(todos, buffer.getChannelData(0), buffer.sampleRate);
463
- var descartar = {};
464
- repetidos.forEach(function (r) { descartar[r.indice] = r; });
465
- var trechos = todos.filter(function (t, i) { return !descartar[i]; });
466
- repetidos.forEach(function (r) {
467
- var t = todos[r.indice];
468
- conta("Fora " + mmss(t[0]) + "–" + mmss(t[1]) + ": " + r.motivo +
469
- " (semelhança " + r.semelhanca + ").");
470
- });
471
-
472
- var somaTrechos = trechos.reduce(function (s, t) { return s + (t[1] - t[0]); }, 0);
473
- conta("Sobram " + trechos.length + " trecho(s), somando " + mmss(somaTrechos) +
474
- " de " + mmss(buffer.duration) + ".");
475
- andar(1, "pronto");
476
-
477
- return responder(id, {
478
- duracao: +buffer.duration.toFixed(2),
479
- pausas: pausas.map(function (p) { return [+p[0].toFixed(2), +p[1].toFixed(2)]; }),
480
- trechos: trechos,
481
- // O que foi tirado por repetição vai NOMEADO: o agente precisa poder
482
- // contar para a pessoa o que sumiu do vídeo dela.
483
- repetidos: repetidos.map(function (r) {
484
- return { de: todos[r.indice][0], ate: todos[r.indice][1],
485
- motivo: r.motivo, semelhanca: r.semelhanca };
486
- }),
487
- cortado: +(buffer.duration - somaTrechos).toFixed(2),
488
- }).then(function () {
489
- conta("Resultado entregue ao PrimoCode. Pode fechar esta aba.");
490
- document.body.setAttribute("data-estado", "pronto");
491
- // Fecha sozinha quando foi o CLI que abriu: ninguém pediu uma aba,
492
- // pediram um vídeo editado.
493
- setTimeout(function () { try { window.close(); } catch (e) {} }, 2500);
494
- });
495
- }).catch(function (e) {
496
- conta("Não deu: " + e.message);
497
- document.body.setAttribute("data-estado", "erro");
498
- andar(1, "erro");
499
- responder(id, { erro: e.message }).catch(function () {});
500
- });
501
- }
502
-
503
- var id = (location.hash || "").replace(/^#/, "").trim();
504
- if (!/^[a-z0-9]+$/.test(id)) {
505
- conta("Esta página é aberta pelo PrimoCode quando ele precisa ouvir um vídeo seu.");
506
- document.body.setAttribute("data-estado", "erro");
507
- } else {
508
- rodar(id);
509
- }
510
- })();