primocode 8.35.0 → 8.36.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/lib/act.js +68 -0
- package/lib/browser.js +292 -1
- package/lib/catalogo.js +11 -2
- package/lib/claude-engine.js +25 -2
- package/lib/codex-engine.js +9 -2
- package/lib/pipeline.js +447 -0
- package/lib/studio.js +16 -3
- package/lib/tools.js +2 -1
- package/package.json +2 -2
- package/studio/ferramentas/corte.md +73 -10
- package/studio/server.py +78 -4
- package/studio/web/js/analise.js +319 -11
- package/studio/web/js/audio.js +367 -55
- package/studio/web/js/render.js +109 -2
- package/studio/web/js/video.js +4 -0
package/studio/server.py
CHANGED
|
@@ -751,11 +751,19 @@ def criar_analise(dados):
|
|
|
751
751
|
for velho in sorted(ANALISES, key=lambda k: ANALISES[k]["criado"])[:len(ANALISES) - LIMITE_ANALISES + 1]:
|
|
752
752
|
ANALISES.pop(velho, None)
|
|
753
753
|
|
|
754
|
+
# `manter` e `repetida` viajam SEM valor padrão aqui, e isso é de propósito:
|
|
755
|
+
# quem decide quanto de pausa fica é a página (ela conhece o tamanho de cada
|
|
756
|
+
# uma), e um padrão escrito também deste lado seria um segundo dono da mesma
|
|
757
|
+
# escolha — os dois divergem na primeira mudança e o que vale passa a ser o
|
|
758
|
+
# que ninguém lembra de olhar.
|
|
759
|
+
pedido = dados or {}
|
|
754
760
|
ident = uuid.uuid4().hex[:10]
|
|
755
761
|
ANALISES[ident] = {
|
|
756
762
|
"id": ident, "arquivo": arquivo, "estado": "pedido",
|
|
757
|
-
"silencio": float(
|
|
758
|
-
"pausa": float(
|
|
763
|
+
"silencio": float(pedido.get("silencio") or 0.45),
|
|
764
|
+
"pausa": float(pedido.get("pausa") or 0.6),
|
|
765
|
+
"manter": None if pedido.get("manter") is None else float(pedido["manter"]),
|
|
766
|
+
"repetida": pedido.get("repetida") is not False,
|
|
759
767
|
"criado": time.time(), "resultado": None,
|
|
760
768
|
}
|
|
761
769
|
return {"id": ident, "pagina": "/analise#" + ident, "estado": "pedido"}, 201
|
|
@@ -770,10 +778,16 @@ def gravar_analise(ident, dados):
|
|
|
770
778
|
tarefa["resultado"] = {"erro": str(dados["erro"])[:300]}
|
|
771
779
|
else:
|
|
772
780
|
tarefa["estado"] = "pronto"
|
|
781
|
+
# ESTE DICIONÁRIO É UMA PENEIRA, e campo que não está aqui morre em
|
|
782
|
+
# silêncio. Foi assim que a lista de falas repetidas sumiu no dia em que
|
|
783
|
+
# a página passou a devolvê-la: o vídeo saía com o tropeço já cortado e
|
|
784
|
+
# nada na tela dizia o que tinha sido tirado. Campo novo na página entra
|
|
785
|
+
# AQUI junto, ou não existe.
|
|
773
786
|
tarefa["resultado"] = {
|
|
774
787
|
"duracao": dados.get("duracao"),
|
|
775
788
|
"trechos": dados.get("trechos") or [],
|
|
776
789
|
"pausas": dados.get("pausas") or [],
|
|
790
|
+
"repetidos": dados.get("repetidos") or [],
|
|
777
791
|
"cortado": dados.get("cortado"),
|
|
778
792
|
}
|
|
779
793
|
return {"ok": True, "estado": tarefa["estado"]}, 200
|
|
@@ -820,8 +834,10 @@ def revisar_monotonia(doc, ferramenta):
|
|
|
820
834
|
|
|
821
835
|
if ferramenta == "corte" and not doc.get("trilha"):
|
|
822
836
|
avisos.append(
|
|
823
|
-
"o vídeo está sem trilha.
|
|
824
|
-
"lofi, epico, alegre, tenso) e nenhum custa arquivo nem licença
|
|
837
|
+
"o vídeo está sem trilha. Oito estilos gerados existem (pulso, ambiente, "
|
|
838
|
+
"lofi, epico, alegre, tenso, suave, foco) e nenhum custa arquivo nem licença. "
|
|
839
|
+
"Se alguém FALA no vídeo, use suave ou foco: as duas foram feitas para ficar "
|
|
840
|
+
"embaixo de uma voz. Escolha "
|
|
825
841
|
"pelo assunto.")
|
|
826
842
|
|
|
827
843
|
duracoes = [c.get("duracao") for c in cenas if isinstance(c.get("duracao"), (int, float))]
|
|
@@ -840,6 +856,55 @@ def revisar_monotonia(doc, ferramenta):
|
|
|
840
856
|
'toda animação de entrada é "%s". Varie: subir, surgir, esquerda, direita, '
|
|
841
857
|
"zoom, desfoque, cair, girar, saltar, revelar, cortina, elastico." % anims[0])
|
|
842
858
|
|
|
859
|
+
# ── O QUE FALTA, E NÃO SÓ O QUE SE REPETE ───────────────────────────
|
|
860
|
+
#
|
|
861
|
+
# "Falta de efeitos visuais: não adiciona efeitos, não insere ícones ou
|
|
862
|
+
# elementos visuais de apoio no vídeo final."
|
|
863
|
+
#
|
|
864
|
+
# Os avisos acima cobram VARIEDADE — eles só disparam quando alguma coisa
|
|
865
|
+
# foi usada e usada sempre igual. Um vídeo que não usa NADA passava por
|
|
866
|
+
# todos eles calado, e era esse o vídeo do relato: texto sobre fundo, do
|
|
867
|
+
# primeiro segundo ao último. O que falta não aparece contando repetição;
|
|
868
|
+
# aparece contando ausência.
|
|
869
|
+
todos = [e for c in cenas for e in (c.get("elementos") or []) if isinstance(e, dict)]
|
|
870
|
+
tipos = {e.get("tipo") for e in todos}
|
|
871
|
+
|
|
872
|
+
if not doc.get("acabamento") and not any(c.get("acabamento") for c in cenas):
|
|
873
|
+
avisos.append(
|
|
874
|
+
"nenhuma cena tem acabamento. É a textura por cima do quadro inteiro "
|
|
875
|
+
"(grao, vinheta, scanlines, luz, brilho, moldura) e é o que tira a cara "
|
|
876
|
+
'de template: "acabamento": "grao" no documento vale para o vídeo todo.')
|
|
877
|
+
|
|
878
|
+
apoio = {"icone", "selo", "forma", "grafico", "progresso", "dispositivo",
|
|
879
|
+
"conversa", "passos", "opcoes", "numeros", "avatar", "citacao"}
|
|
880
|
+
if not (tipos & apoio):
|
|
881
|
+
avisos.append(
|
|
882
|
+
"o vídeo é só texto e imagem: nenhum elemento de apoio em %d cenas. "
|
|
883
|
+
'Um "icone" ao lado do argumento (raio, alvo, foguete, escudo, relogio, '
|
|
884
|
+
"lampada, cadeado, crescimento, pessoas, dinheiro, globo, fogo…), um "
|
|
885
|
+
'"selo" de estado, um "numeros" com "contar": true — é isto que faz a '
|
|
886
|
+
"tela parecer produzida em vez de um slide com legenda." % len(cenas))
|
|
887
|
+
|
|
888
|
+
movimento = any(e.get("chaves") or e.get("movimento") for e in todos)
|
|
889
|
+
if not movimento and not anims:
|
|
890
|
+
avisos.append(
|
|
891
|
+
"nada se mexe: nenhum elemento tem anim, chaves ou movimento. Imagem "
|
|
892
|
+
'parada em vídeo denuncia preguiça — toda "imagem" leva "movimento" '
|
|
893
|
+
"(aproximar, afastar, esquerda, direita, cima), e uma ou duas peças por "
|
|
894
|
+
"vídeo levam chaves de animação.")
|
|
895
|
+
|
|
896
|
+
# Só a voz com tela montada: existe e quase nunca é lembrado, porque é o
|
|
897
|
+
# contrário do `mudo` e o contrário não estava no vocabulário até agora.
|
|
898
|
+
clipes = [e for e in todos if e.get("tipo") == "clipe"]
|
|
899
|
+
if clipes and len(clipes) >= 3 and not any(
|
|
900
|
+
e.get("apenas") == "som" or e.get("som") is True for e in clipes):
|
|
901
|
+
avisos.append(
|
|
902
|
+
"todos os %d clipes mostram a gravação. Se o que vale é o que ele DIZ, "
|
|
903
|
+
'"apenas": "som" no clipe mantém a voz e libera o quadro para a tela que '
|
|
904
|
+
"você montar por cima — é o vídeo dele virando narração da sua peça. "
|
|
905
|
+
"Vale para a parte em que ele explica, não para a em que ele aparece."
|
|
906
|
+
% len(clipes))
|
|
907
|
+
|
|
843
908
|
return avisos
|
|
844
909
|
|
|
845
910
|
|
|
@@ -962,6 +1027,15 @@ def _clipe_invalido(e, i, j):
|
|
|
962
1027
|
'"de": 12.5, "ate": 19.' % (onde, campo, v))
|
|
963
1028
|
if v < 0:
|
|
964
1029
|
return '%s tem "%s": %r — segundo negativo não existe.' % (onde, campo, v)
|
|
1030
|
+
# "apenas": "som" entrega a VOZ da gravação com a tela montada por cima.
|
|
1031
|
+
# Junto com "mudo": true ele não entrega nada: sem imagem porque foi
|
|
1032
|
+
# escondido, sem som porque foi silenciado. A cena sairia com a duração do
|
|
1033
|
+
# trecho e o conteúdo de um silêncio — que é exatamente o tipo de defeito
|
|
1034
|
+
# que só aparece quando alguém assiste o vídeo pronto.
|
|
1035
|
+
if (e.get("apenas") == "som" or e.get("som") is True) and e.get("mudo"):
|
|
1036
|
+
return ('%s tem "apenas": "som" com "mudo": true. Um cancela o outro: o clipe some da '
|
|
1037
|
+
'tela E fica sem áudio, e a cena vira um silêncio do tamanho do trecho. Para a '
|
|
1038
|
+
'voz dele com a sua tela por cima, tire o "mudo".' % onde)
|
|
965
1039
|
de, ate = e.get("de"), e.get("ate")
|
|
966
1040
|
if isinstance(de, (int, float)) and isinstance(ate, (int, float)) and ate <= de:
|
|
967
1041
|
return ('%s tem "ate": %r antes ou igual a "de": %r. O trecho ficaria com duração zero ou '
|
package/studio/web/js/analise.js
CHANGED
|
@@ -105,20 +105,306 @@
|
|
|
105
105
|
return pausas;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/* O
|
|
109
|
-
|
|
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
|
+
*/
|
|
110
137
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
346
|
|
|
115
|
-
|
|
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) {
|
|
116
398
|
var trechos = [];
|
|
117
399
|
var cursor = 0;
|
|
118
400
|
pausas.forEach(function (p) {
|
|
119
|
-
var
|
|
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);
|
|
120
406
|
if (ate - cursor > 0.35) trechos.push([+cursor.toFixed(2), +ate.toFixed(2)]);
|
|
121
|
-
cursor = Math.max(0, p[1] -
|
|
407
|
+
cursor = Math.max(0, p[1] - metade);
|
|
122
408
|
});
|
|
123
409
|
if (duracao - cursor > 0.35) trechos.push([+cursor.toFixed(2), +duracao.toFixed(2)]);
|
|
124
410
|
return trechos;
|
|
@@ -164,10 +450,26 @@
|
|
|
164
450
|
andar(0.85, "achando as pausas");
|
|
165
451
|
|
|
166
452
|
var pausas = acharPausas(env, tarefa.pausa || 0.6, lim.corte);
|
|
167
|
-
var
|
|
168
|
-
var somaTrechos = trechos.reduce(function (s, t) { return s + (t[1] - t[0]); }, 0);
|
|
169
|
-
|
|
453
|
+
var todos = trechosUteis(pausas, buffer.duration, tarefa.manter);
|
|
170
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);
|
|
171
473
|
conta("Sobram " + trechos.length + " trecho(s), somando " + mmss(somaTrechos) +
|
|
172
474
|
" de " + mmss(buffer.duration) + ".");
|
|
173
475
|
andar(1, "pronto");
|
|
@@ -176,6 +478,12 @@
|
|
|
176
478
|
duracao: +buffer.duration.toFixed(2),
|
|
177
479
|
pausas: pausas.map(function (p) { return [+p[0].toFixed(2), +p[1].toFixed(2)]; }),
|
|
178
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
|
+
}),
|
|
179
487
|
cortado: +(buffer.duration - somaTrechos).toFixed(2),
|
|
180
488
|
}).then(function () {
|
|
181
489
|
conta("Resultado entregue ao PrimoCode. Pode fechar esta aba.");
|