primocode 8.11.2 → 8.13.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/studio.js +26 -2
- package/package.json +2 -2
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/ferramentas/corte.md +45 -2
- package/studio/server.py +126 -19
- package/studio/voz.py +307 -0
- package/studio/web/core.css +133 -2
- package/studio/web/js/editor.js +614 -99
- package/studio/web/js/ferramentas.js +4 -1
- package/studio/web/js/narracao.js +18 -2
- package/studio/web/js/render.js +114 -4
- package/studio/web/js/voz-neural.js +88 -178
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
ferramentas.js — o que cada ferramenta oferece na interface.
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Os dez que o render.css define. A lista tinha seis: quatro temas existiam
|
|
6
|
+
// no CSS, a IA podia usá-los, e o editor não os oferecia a ninguém.
|
|
7
|
+
var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta",
|
|
8
|
+
"retro", "oceano", "doce", "mono"];
|
|
6
9
|
|
|
7
10
|
var PALETA = ["#ff5a36", "#ffb03a", "#f4e04d", "#2fbf7f", "#1f8fe0", "#8b5cf6",
|
|
8
11
|
"#f472b6", "#ffffff", "#9aa4b2", "#111318"];
|
|
@@ -191,12 +191,28 @@ var Narracao = (function () {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
/* Mede quanto tempo a fala leva, sem ninguém ouvir.
|
|
194
|
+
/* Mede quanto tempo a fala leva, sem ninguém ouvir.
|
|
195
|
+
|
|
196
|
+
Com a voz neural o áudio vem pronto, em arquivo, e a duração está nos
|
|
197
|
+
metadados dele: perguntar é exato e é silencioso. Só o caminho do
|
|
198
|
+
speechSynthesis ainda precisa falar para cronometrar — e lá o volume
|
|
199
|
+
zero é obedecido de verdade. */
|
|
195
200
|
function medir(texto, cfg) {
|
|
201
|
+
cfg = cfg || {};
|
|
202
|
+
if (cfg.motor !== "sistema" && typeof VozNeural !== "undefined") {
|
|
203
|
+
return VozNeural.duracaoDe(texto, cfg).then(function (s) {
|
|
204
|
+
return s || medirPelaSintese(texto, cfg);
|
|
205
|
+
}).catch(function () { return medirPelaSintese(texto, cfg); });
|
|
206
|
+
}
|
|
207
|
+
return medirPelaSintese(texto, cfg);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function medirPelaSintese(texto, cfg) {
|
|
196
211
|
var c = {};
|
|
197
212
|
for (var k in (cfg || {})) c[k] = cfg[k];
|
|
198
213
|
c.volume = 0;
|
|
199
|
-
|
|
214
|
+
c.motor = "sistema";
|
|
215
|
+
return pelaSintese(texto, c);
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
function parar() {
|
package/studio/web/js/render.js
CHANGED
|
@@ -70,13 +70,23 @@ function aplicarFundo(no, fundo) {
|
|
|
70
70
|
/* --------------------------------------------------------------- gráficos */
|
|
71
71
|
|
|
72
72
|
function svgGrafico(e, l, a) {
|
|
73
|
-
|
|
73
|
+
/* Os dados podem chegar como "dados" ou como "itens", e o tipo como
|
|
74
|
+
"grafico", "estilo" ou "tipo".
|
|
75
|
+
|
|
76
|
+
O nome certo é `dados` + `grafico`, e é o que a spec ensina. Mas quem
|
|
77
|
+
escreve um gráfico — a IA ou uma pessoa — chuta `itens`, porque é o
|
|
78
|
+
campo de TODOS os outros elementos da mesma spec (lista, cartoes,
|
|
79
|
+
numeros usam `itens`). Com o nome trocado esta função devolvia string
|
|
80
|
+
vazia: o gráfico simplesmente não existia na tela, sem erro nenhum.
|
|
81
|
+
Aceitar as duas grafias custa uma linha e evita a falha muda. */
|
|
82
|
+
var bruto = e.dados || e.itens || [];
|
|
83
|
+
var dados = bruto.map(function (d) {
|
|
74
84
|
return Array.isArray(d) ? { r: d[0], v: +d[1] || 0 } : { r: d.rotulo, v: +d.valor || 0 };
|
|
75
85
|
});
|
|
76
86
|
if (!dados.length) return "";
|
|
77
87
|
var cores = e.cores || ["var(--c-tom)", "var(--c-tom2)", "#7c9cff", "#2fbf7f", "#f472b6", "#facc15"];
|
|
78
88
|
var max = Math.max.apply(null, dados.map(function (d) { return d.v; })) || 1;
|
|
79
|
-
var tipo = e.grafico || "barras";
|
|
89
|
+
var tipo = e.grafico || e.estilo || e.tipoGrafico || "barras";
|
|
80
90
|
var pad = { e: 8, d: 8, t: 14, b: 42 };
|
|
81
91
|
var iw = l - pad.e - pad.d, ih = a - pad.t - pad.b;
|
|
82
92
|
var p = ['<svg viewBox="0 0 ' + l + " " + a + '" width="' + l + '" height="' + a + '">'];
|
|
@@ -140,9 +150,79 @@ function svgGrafico(e, l, a) {
|
|
|
140
150
|
var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
|
|
141
151
|
citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1 };
|
|
142
152
|
|
|
153
|
+
/* ── chaves de animação ──────────────────────────────────────────────────
|
|
154
|
+
Transforma a lista de instantes de um elemento numa regra @keyframes de
|
|
155
|
+
verdade e prende ela no elemento.
|
|
156
|
+
|
|
157
|
+
Os nomes precisam ser únicos por página: o mesmo documento aparece várias
|
|
158
|
+
vezes ao mesmo tempo (palco, miniaturas do painel, clipes da tira), e
|
|
159
|
+
dois @keyframes de mesmo nome com conteúdo diferente se atropelariam —
|
|
160
|
+
a última regra injetada venceria em TODAS as cópias. */
|
|
161
|
+
var _nChave = 0;
|
|
162
|
+
|
|
163
|
+
function _numero(v, padrao) {
|
|
164
|
+
return typeof v === "number" && isFinite(v) ? v : padrao;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function aplicarChaves(no, e, ctx) {
|
|
168
|
+
var chaves = e.chaves.slice().sort(function (a, b) {
|
|
169
|
+
return _numero(a.t, 0) - _numero(b.t, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
var nome = "ch" + (++_nChave);
|
|
173
|
+
var passos = chaves.map(function (k) {
|
|
174
|
+
var pct = Math.max(0, Math.min(1, _numero(k.t, 0))) * 100;
|
|
175
|
+
var mov = [];
|
|
176
|
+
// x e y de uma chave são DESLOCAMENTO a partir de onde o elemento está.
|
|
177
|
+
// Assim a chave continua valendo se a pessoa arrastar o elemento no
|
|
178
|
+
// palco depois — o contrário obrigaria a refazer todas as chaves.
|
|
179
|
+
var dx = _numero(k.x, 0), dy = _numero(k.y, 0);
|
|
180
|
+
if (dx || dy) mov.push("translate(" + dx + "px," + dy + "px)");
|
|
181
|
+
var esc = _numero(k.escala, 1);
|
|
182
|
+
if (esc !== 1) mov.push("scale(" + esc + ")");
|
|
183
|
+
var giro = _numero(k.giro, 0);
|
|
184
|
+
if (giro) mov.push("rotate(" + giro + "deg)");
|
|
185
|
+
|
|
186
|
+
var linhas = ["transform:" + (mov.length ? mov.join(" ") : "none")];
|
|
187
|
+
var op = _numero(k.opacidade, 1);
|
|
188
|
+
linhas.push("opacity:" + op);
|
|
189
|
+
if (typeof k.desfoque === "number" && k.desfoque > 0) {
|
|
190
|
+
linhas.push("filter:blur(" + k.desfoque + "px)");
|
|
191
|
+
}
|
|
192
|
+
return pct.toFixed(3) + "%{" + linhas.join(";") + "}";
|
|
193
|
+
}).join("");
|
|
194
|
+
|
|
195
|
+
// A regra vai na RAIZ da cena, não dentro do elemento.
|
|
196
|
+
//
|
|
197
|
+
// Presa no próprio elemento ela era apagada segundos depois: quem desenha
|
|
198
|
+
// o conteúdo (texto, lista, número) escreve o innerHTML e leva junto tudo
|
|
199
|
+
// que estivesse ali. O elemento continuava com `animation-name: ch1`
|
|
200
|
+
// apontando para uma regra que não existia mais — a animação simplesmente
|
|
201
|
+
// não acontecia, sem erro nenhum no console. A cena só recebe filhos, e
|
|
202
|
+
// por isso a regra sobrevive lá.
|
|
203
|
+
var folha = document.createElement("style");
|
|
204
|
+
folha.textContent = "@keyframes " + nome + "{" + passos + "}";
|
|
205
|
+
(ctx.raiz || no).appendChild(folha);
|
|
206
|
+
|
|
207
|
+
var dur = _numero(ctx.duracao, 4);
|
|
208
|
+
no.style.animationName = nome;
|
|
209
|
+
no.style.animationDuration = dur + "s";
|
|
210
|
+
no.style.animationTimingFunction = e.suavizar === false ? "linear" : "cubic-bezier(.4,0,.2,1)";
|
|
211
|
+
no.style.animationFillMode = "both";
|
|
212
|
+
no.style.animationIterationCount = e.repetir ? "infinite" : "1";
|
|
213
|
+
}
|
|
214
|
+
|
|
143
215
|
function renderElemento(e, ctx) {
|
|
144
216
|
var no = document.createElement("div");
|
|
145
217
|
var tipo = e.tipo || "texto";
|
|
218
|
+
/* Qual elemento do documento é este nó.
|
|
219
|
+
|
|
220
|
+
O editor precisava saber, e antes deduzia pela ordem dos nós na tela.
|
|
221
|
+
A dedução estava errada: os layouts de fluxo separam cabeçalho de corpo
|
|
222
|
+
e desenham o cabeçalho primeiro, então a ordem na tela NÃO é a ordem do
|
|
223
|
+
documento. O efeito era clicar num texto e o editor selecionar outro —
|
|
224
|
+
e mexer no elemento errado. O índice vem carimbado daqui. */
|
|
225
|
+
if (ctx && ctx.ordem && ctx.ordem.has(e)) no.dataset.i = ctx.ordem.get(e);
|
|
146
226
|
/* Tipo inventado ("titulo", "subtitulo"...) caía num default sem estilo:
|
|
147
227
|
texto de 16px num canvas de 1920 — invisível — ou div vazio quando o
|
|
148
228
|
conteúdo estava noutro campo. O servidor novo recusa; documentos velhos
|
|
@@ -178,11 +258,27 @@ function renderElemento(e, ctx) {
|
|
|
178
258
|
|
|
179
259
|
/* animação */
|
|
180
260
|
var an = e.anim || ctx.animPadrao;
|
|
181
|
-
if (an && an !== "nenhuma") {
|
|
261
|
+
if (an && an !== "nenhuma" && !(e.chaves && e.chaves.length > 1)) {
|
|
182
262
|
no.setAttribute("data-an", typeof an === "string" ? an : (an.entrada || "subir"));
|
|
183
263
|
no.style.setProperty("--atraso", (typeof an === "object" && an.atraso != null ? an.atraso : ctx.i * 0.09 + 0.1));
|
|
184
264
|
}
|
|
185
265
|
|
|
266
|
+
/* ── chaves de animação ────────────────────────────────────────────────
|
|
267
|
+
Antes um elemento só sabia ENTRAR: escolhia-se um efeito de entrada e
|
|
268
|
+
acabava ali. Não dava para fazer nada percorrer a cena, crescer devagar
|
|
269
|
+
ou sumir no fim — que é o que separa animação de slide com efeito.
|
|
270
|
+
|
|
271
|
+
Uma chave é um instante: `{t: 0.5, x: 200, escala: 1.2}` diz onde o
|
|
272
|
+
elemento está na metade da cena. Duas ou mais viram uma animação CSS de
|
|
273
|
+
verdade, gerada aqui, com a duração da cena. O navegador interpola;
|
|
274
|
+
nós só descrevemos os pontos.
|
|
275
|
+
|
|
276
|
+
Quem tem chaves não recebe animação de entrada: as duas brigariam pelo
|
|
277
|
+
mesmo transform, e a chave é a mais específica das duas. */
|
|
278
|
+
if (ctx.animar && e.chaves && e.chaves.length > 1) {
|
|
279
|
+
aplicarChaves(no, e, ctx);
|
|
280
|
+
}
|
|
281
|
+
|
|
186
282
|
switch (tipo) {
|
|
187
283
|
case "texto": {
|
|
188
284
|
var papel = PAPEIS[e.papel] || PAPEIS.corpo;
|
|
@@ -560,7 +656,21 @@ function renderCena(cena, doc, op) {
|
|
|
560
656
|
// tela vertical pede tipografia maior: os padrões são calibrados em 1920x1080
|
|
561
657
|
var escalaTipo = H > W ? 1.35 : 1;
|
|
562
658
|
no.style.setProperty("--esc", escalaTipo);
|
|
563
|
-
var ctx = {
|
|
659
|
+
var ctx = {
|
|
660
|
+
i: 0, escalaTipo: escalaTipo,
|
|
661
|
+
animPadrao: op.animar ? (cena.anim || "subir") : null,
|
|
662
|
+
// as chaves precisam saber quanto dura a cena para virar segundos
|
|
663
|
+
animar: !!op.animar,
|
|
664
|
+
duracao: cena.duracao || 4,
|
|
665
|
+
raiz: no,
|
|
666
|
+
// objeto do elemento -> posição dele em cena.elementos, tirada ANTES de
|
|
667
|
+
// qualquer separação por layout
|
|
668
|
+
ordem: (function () {
|
|
669
|
+
var m = new Map();
|
|
670
|
+
(cena.elementos || []).forEach(function (e, i) { m.set(e, i); });
|
|
671
|
+
return m;
|
|
672
|
+
})(),
|
|
673
|
+
};
|
|
564
674
|
|
|
565
675
|
if (layout === "livre") {
|
|
566
676
|
fluxo.style.padding = "0";
|
|
@@ -1,204 +1,112 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
2
|
voz-neural.js — locução com voz de verdade.
|
|
3
3
|
|
|
4
|
-
── O
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
── NOTA HONESTA ────────────────────────────────────────────────────────
|
|
25
|
-
O caminho 1 foi escrito a partir do protocolo do edge-tts e a lista de
|
|
26
|
-
vozes foi confirmada no ar, mas a SÍNTESE não pôde ser testada de onde
|
|
27
|
-
este código foi escrito (o proxy de lá não passa WebSocket). O caminho 2
|
|
28
|
-
foi testado de ponta a ponta e devolveu MP3 real. Por isso a corrente
|
|
29
|
-
existe: se o 1 não subir na sua máquina, o 2 assume sem você notar.
|
|
4
|
+
── O QUE MUDOU, E POR QUÊ ──────────────────────────────────────────────
|
|
5
|
+
Este arquivo já tentou sintetizar no próprio navegador, falando por
|
|
6
|
+
WebSocket com o serviço de leitura em voz alta do Edge. Não funciona
|
|
7
|
+
mais: a Microsoft passou a responder 403 na síntese. A lista de vozes
|
|
8
|
+
ainda responde, o que enganava — parecia vivo e não estava.
|
|
9
|
+
|
|
10
|
+
Com o caminho neural morto, toda narração caía no speechSynthesis, o TTS
|
|
11
|
+
da própria máquina. Ele existe em todo computador, e é justamente por
|
|
12
|
+
isso que soa a computador. Era o que se ouvia em TODO vídeo.
|
|
13
|
+
|
|
14
|
+
Agora a síntese acontece no servidor do Studio, em Python (studio/voz.py),
|
|
15
|
+
com o Piper: rede neural que roda na máquina do usuário, offline, sem
|
|
16
|
+
chave, sem cota e sem limite — e com vozes graves de verdade, escolhidas
|
|
17
|
+
por frequência MEDIDA, não por adjetivo. Este arquivo virou o que devia
|
|
18
|
+
ter sido desde o começo: um cliente magro de /api/voz.
|
|
19
|
+
|
|
20
|
+
A vantagem não é só timbre. O servidor devolve um Blob, e Blob serve
|
|
21
|
+
tanto para tocar quanto para ir junto no vídeo exportado — coisa que o
|
|
22
|
+
speechSynthesis nunca permitiu, porque ele fala e some.
|
|
30
23
|
========================================================================== */
|
|
31
24
|
|
|
32
25
|
var VozNeural = (function () {
|
|
33
26
|
"use strict";
|
|
34
27
|
|
|
35
|
-
/*
|
|
36
|
-
|
|
37
|
-
var
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
28
|
+
/* Preenchido por /api/voz/status na primeira consulta. Começa com a voz
|
|
29
|
+
padrão do servidor para a interface ter o que mostrar antes da resposta. */
|
|
30
|
+
var VOZES = [];
|
|
31
|
+
var estado = null;
|
|
32
|
+
|
|
33
|
+
function status(forcar) {
|
|
34
|
+
if (estado && !forcar) return Promise.resolve(estado);
|
|
35
|
+
return fetch("/api/voz/status")
|
|
36
|
+
.then(function (r) { return r.json(); })
|
|
37
|
+
.then(function (s) {
|
|
38
|
+
estado = s;
|
|
39
|
+
VOZES = (s.piper && s.piper.vozes) || [];
|
|
40
|
+
return s;
|
|
41
|
+
})
|
|
42
|
+
.catch(function () { return { piper: { pronta: false, vozes: [] } }; });
|
|
43
|
+
}
|
|
51
44
|
|
|
52
45
|
function vozes() { return VOZES.slice(); }
|
|
53
|
-
function vozPadrao() { return VOZES[0].id; }
|
|
54
46
|
function graves() { return VOZES.filter(function (v) { return v.grave; }); }
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
/* O serviço exige um token derivado do relógio, arredondado para baixo de
|
|
59
|
-
5 em 5 minutos, concatenado com a chave pública do cliente e passado por
|
|
60
|
-
SHA-256. Sem ele o servidor recusa a conexão. */
|
|
61
|
-
function selo() {
|
|
62
|
-
var ticks = Math.floor(Date.now() / 1000) + EPOCA_WIN;
|
|
63
|
-
ticks -= ticks % 300;
|
|
64
|
-
/* de segundos para intervalos de 100 nanossegundos */
|
|
65
|
-
var texto = String(ticks * 1e7) + TOKEN;
|
|
66
|
-
var bytes = new TextEncoder().encode(texto);
|
|
67
|
-
return crypto.subtle.digest("SHA-256", bytes).then(function (buf) {
|
|
68
|
-
return Array.prototype.map
|
|
69
|
-
.call(new Uint8Array(buf), function (b) { return ("0" + b.toString(16)).slice(-2); })
|
|
70
|
-
.join("")
|
|
71
|
-
.toUpperCase();
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function agoraISO() {
|
|
76
|
-
return new Date().toUTCString().replace("GMT", "GMT+0000 (Coordinated Universal Time)");
|
|
47
|
+
function vozPadrao() {
|
|
48
|
+
var p = VOZES.filter(function (v) { return v.padrao; })[0];
|
|
49
|
+
return p ? p.id : "";
|
|
77
50
|
}
|
|
78
51
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
52
|
+
/* Manda baixar o motor e a voz. A interface chama isto quando o usuário
|
|
53
|
+
escolhe uma voz que ainda não está na máquina. */
|
|
54
|
+
function instalar(voz, esperar) {
|
|
55
|
+
return fetch("/api/voz/instalar", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/json" },
|
|
58
|
+
body: JSON.stringify({ voz: voz || "", esperar: esperar || 0 }),
|
|
59
|
+
}).then(function (r) { return r.json(); });
|
|
83
60
|
}
|
|
84
61
|
|
|
85
|
-
/*
|
|
62
|
+
/* --------------------------------------------------------------- fala --- */
|
|
86
63
|
|
|
87
|
-
/*
|
|
88
|
-
|
|
89
|
-
function
|
|
64
|
+
/* O texto vira áudio pelo melhor motor que o servidor tiver pronto. O
|
|
65
|
+
navegador não decide nada: só pede e toca. */
|
|
66
|
+
function audioDe(texto, cfg) {
|
|
90
67
|
cfg = cfg || {};
|
|
91
|
-
var
|
|
92
|
-
|
|
93
|
-
o que separa locução de leitura de robô. */
|
|
94
|
-
var vel = cfg.velocidade != null ? cfg.velocidade : -8;
|
|
95
|
-
var tom = cfg.tom != null ? cfg.tom : -4;
|
|
96
|
-
|
|
97
|
-
return selo().then(function (gec) {
|
|
98
|
-
return new Promise(function (ok, erro) {
|
|
99
|
-
var url = "wss://" + HOST + "/edge/v1?TrustedClientToken=" + TOKEN
|
|
100
|
-
+ "&Sec-MS-GEC=" + gec + "&Sec-MS-GEC-Version=" + VERSAO_GEC;
|
|
101
|
-
var ws;
|
|
102
|
-
try { ws = new WebSocket(url); } catch (e) { return erro(e); }
|
|
103
|
-
ws.binaryType = "arraybuffer";
|
|
104
|
-
|
|
105
|
-
var pedacos = [];
|
|
106
|
-
var fechou = false;
|
|
107
|
-
var relogio = setTimeout(function () {
|
|
108
|
-
if (!fechou) { fechou = true; try { ws.close(); } catch (e) {} erro(new Error("voz neural nao respondeu")); }
|
|
109
|
-
}, 6000);
|
|
110
|
-
|
|
111
|
-
function desistir(e) {
|
|
112
|
-
if (fechou) return;
|
|
113
|
-
fechou = true;
|
|
114
|
-
clearTimeout(relogio);
|
|
115
|
-
try { ws.close(); } catch (x) {}
|
|
116
|
-
erro(e instanceof Error ? e : new Error("voz neural falhou"));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
ws.onerror = desistir;
|
|
120
|
-
ws.onclose = function () {
|
|
121
|
-
if (fechou) return;
|
|
122
|
-
fechou = true;
|
|
123
|
-
clearTimeout(relogio);
|
|
124
|
-
if (pedacos.length) ok(new Blob(pedacos, { type: "audio/mpeg" }));
|
|
125
|
-
else erro(new Error("voz neural fechou sem audio"));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
ws.onopen = function () {
|
|
129
|
-
ws.send(
|
|
130
|
-
"X-Timestamp:" + agoraISO() + "\r\n"
|
|
131
|
-
+ "Content-Type:application/json; charset=utf-8\r\n"
|
|
132
|
-
+ "Path:speech.config\r\n\r\n"
|
|
133
|
-
+ '{"context":{"synthesis":{"audio":{"metadataoptions":'
|
|
134
|
-
+ '{"sentenceBoundaryEnabled":"false","wordBoundaryEnabled":"false"},'
|
|
135
|
-
+ '"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}}'
|
|
136
|
-
);
|
|
137
|
-
var ssml = '<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="pt-BR">'
|
|
138
|
-
+ '<voice name="' + voz + '">'
|
|
139
|
-
+ '<prosody pitch="' + tom + 'Hz" rate="' + vel + '%" volume="+0%">'
|
|
140
|
-
+ escapar(texto)
|
|
141
|
-
+ "</prosody></voice></speak>";
|
|
142
|
-
ws.send(
|
|
143
|
-
"X-RequestId:" + pedido() + "\r\n"
|
|
144
|
-
+ "Content-Type:application/ssml+xml\r\n"
|
|
145
|
-
+ "X-Timestamp:" + agoraISO() + "Z\r\n"
|
|
146
|
-
+ "Path:ssml\r\n\r\n" + ssml
|
|
147
|
-
);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
ws.onmessage = function (ev) {
|
|
151
|
-
if (typeof ev.data === "string") {
|
|
152
|
-
if (ev.data.indexOf("Path:turn.end") !== -1) {
|
|
153
|
-
fechou = true;
|
|
154
|
-
clearTimeout(relogio);
|
|
155
|
-
try { ws.close(); } catch (x) {}
|
|
156
|
-
if (pedacos.length) ok(new Blob(pedacos, { type: "audio/mpeg" }));
|
|
157
|
-
else erro(new Error("voz neural nao mandou audio"));
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
/* Binário: 2 bytes de tamanho do cabeçalho, o cabeçalho, e o áudio. */
|
|
162
|
-
var b = new Uint8Array(ev.data);
|
|
163
|
-
if (b.length < 2) return;
|
|
164
|
-
var tamCabecalho = (b[0] << 8) | b[1];
|
|
165
|
-
if (b.length > 2 + tamCabecalho) pedacos.push(b.slice(2 + tamCabecalho));
|
|
166
|
-
};
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function pedido() {
|
|
172
|
-
var s = "";
|
|
173
|
-
for (var i = 0; i < 32; i++) s += "0123456789abcdef"[Math.floor(Math.random() * 16)];
|
|
174
|
-
return s;
|
|
175
|
-
}
|
|
68
|
+
var t = String(texto || "").trim();
|
|
69
|
+
if (!t) return Promise.reject(new Error("sem texto"));
|
|
176
70
|
|
|
177
|
-
|
|
71
|
+
var q = "texto=" + encodeURIComponent(t);
|
|
72
|
+
if (cfg.voz) q += "&voz=" + encodeURIComponent(cfg.voz);
|
|
73
|
+
if (cfg.motor) q += "&motor=" + encodeURIComponent(cfg.motor);
|
|
74
|
+
if (cfg.grave) q += "&grave=1";
|
|
75
|
+
if (cfg.velocidade) q += "&velocidade=" + encodeURIComponent(cfg.velocidade);
|
|
178
76
|
|
|
179
|
-
|
|
180
|
-
navegador direto o pedido morre. Uma voz só, sem escolha — mas responde. */
|
|
181
|
-
function pelaGoogle(texto) {
|
|
182
|
-
return fetch("/api/voz?texto=" + encodeURIComponent(texto)).then(function (r) {
|
|
77
|
+
return fetch("/api/voz?" + q).then(function (r) {
|
|
183
78
|
if (!r.ok) throw new Error("voz do servidor falhou (" + r.status + ")");
|
|
184
79
|
return r.blob();
|
|
185
80
|
});
|
|
186
81
|
}
|
|
187
82
|
|
|
188
|
-
/*
|
|
189
|
-
|
|
190
|
-
/* O texto vira áudio pelo melhor caminho que responder. Devolve um Blob de
|
|
191
|
-
MP3, que serve tanto para tocar quanto para ir junto no vídeo exportado —
|
|
192
|
-
coisa que o speechSynthesis nunca permitiu, porque ele fala e some. */
|
|
193
|
-
function audioDe(texto, cfg) {
|
|
194
|
-
cfg = cfg || {};
|
|
195
|
-
var t = String(texto || "").trim();
|
|
196
|
-
if (!t) return Promise.reject(new Error("sem texto"));
|
|
83
|
+
/* Quanto tempo a narração vai levar, SEM tocar nada.
|
|
197
84
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
85
|
+
O editor precisa disso para ajustar a duração da cena ao texto. Antes
|
|
86
|
+
ele media falando com volume zero — funcionava porque o speechSynthesis
|
|
87
|
+
obedece volume. Agora o áudio vem em Blob, e "tocar mudo" seria tocar
|
|
88
|
+
alto. Os metadados do arquivo já trazem a duração exata: é só perguntar,
|
|
89
|
+
e é mais preciso do que cronometrar a fala. */
|
|
90
|
+
function duracaoDe(texto, cfg) {
|
|
91
|
+
return audioDe(texto, cfg).then(function (blob) {
|
|
92
|
+
return new Promise(function (ok) {
|
|
93
|
+
var a = new Audio();
|
|
94
|
+
var url = URL.createObjectURL(blob);
|
|
95
|
+
var respondeu = false;
|
|
96
|
+
function fim(s) {
|
|
97
|
+
if (respondeu) return;
|
|
98
|
+
respondeu = true;
|
|
99
|
+
URL.revokeObjectURL(url);
|
|
100
|
+
ok(s);
|
|
101
|
+
}
|
|
102
|
+
a.onloadedmetadata = function () {
|
|
103
|
+
fim(isFinite(a.duration) && a.duration > 0 ? a.duration : 0);
|
|
104
|
+
};
|
|
105
|
+
a.onerror = function () { fim(0); };
|
|
106
|
+
setTimeout(function () { fim(0); }, 8000);
|
|
107
|
+
a.preload = "metadata";
|
|
108
|
+
a.src = url;
|
|
109
|
+
});
|
|
202
110
|
});
|
|
203
111
|
}
|
|
204
112
|
|
|
@@ -232,10 +140,12 @@ var VozNeural = (function () {
|
|
|
232
140
|
.catch(function (e) { return { ok: false, error: e.message }; });
|
|
233
141
|
}
|
|
234
142
|
|
|
143
|
+
status();
|
|
144
|
+
|
|
235
145
|
return {
|
|
236
|
-
vozes: vozes, vozPadrao: vozPadrao, graves: graves,
|
|
146
|
+
vozes: vozes, vozPadrao: vozPadrao, graves: graves, status: status,
|
|
147
|
+
instalar: instalar, duracaoDe: duracaoDe,
|
|
237
148
|
audioDe: audioDe, falar: falar, parar: parar, falando: falando, testar: testar,
|
|
238
|
-
_selo: selo
|
|
239
149
|
};
|
|
240
150
|
})();
|
|
241
151
|
|