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.
@@ -2,7 +2,10 @@
2
2
  ferramentas.js — o que cada ferramenta oferece na interface.
3
3
  ========================================================================== */
4
4
 
5
- var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta"];
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
- return falar(texto, c);
214
+ c.motor = "sistema";
215
+ return pelaSintese(texto, c);
200
216
  }
201
217
 
202
218
  function parar() {
@@ -70,13 +70,23 @@ function aplicarFundo(no, fundo) {
70
70
  /* --------------------------------------------------------------- gráficos */
71
71
 
72
72
  function svgGrafico(e, l, a) {
73
- var dados = (e.dados || []).map(function (d) {
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 = { i: 0, escalaTipo: escalaTipo, animPadrao: op.animar ? (cena.anim || "subir") : null };
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 PROBLEMA ──────────────────────────────────────────────────────────
5
- A narração usava speechSynthesis, o TTS da própria máquina. Ele existe em
6
- todo computador, e é justamente esse o problema: soa a computador. Para
7
- vídeo institucional não serve — e o dono pediu voz densa, cinematográfica,
8
- com possibilidade de ESCOLHER.
9
-
10
- ── OS TRÊS CAMINHOS, DO MELHOR PARA O PIOR ─────────────────────────────
11
- 1. NEURAL (Edge) vozes neurais da Microsoft. Grátis, sem chave, sem
12
- limite prático, e as pt-BR são boas de verdade:
13
- Antonio (masculina, grave — a de vídeo), Francisca
14
- (feminina) e Thalita (multilíngue).
15
- Fala por WebSocket, que não passa por CORS.
16
- 2. GOOGLE (proxy) uma voz só, sem escolha, mas responde sempre. Vai pelo
17
- servidor do Studio porque o Google não manda cabeçalho
18
- de CORS — do navegador direto seria bloqueado.
19
- 3. SISTEMA o speechSynthesis de antes. Rascunho, e nada mais.
20
-
21
- Cada caminho cai no seguinte quando falha. Narração é acabamento: se a voz
22
- não vier, o vídeo continua existindo, mudo.
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
- /* Constantes do serviço de leitura em voz alta do Edge. */
36
- var TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
37
- var HOST = "speech.platform.bing.com/consumer/speech/synthesize/readaloud";
38
- var VERSAO_GEC = "1-130.0.2849.68";
39
- /* 1601-01-01 → 1970-01-01 em segundos: o relógio do Windows começa antes. */
40
- var EPOCA_WIN = 11644473600;
41
-
42
- /* As vozes que valem para narrar em português. `grave` é o que o dono
43
- chamou de densa: a que sustenta abertura de vídeo. */
44
- var VOZES = [
45
- { id: "pt-BR-AntonioNeural", nome: "Antônio", tom: "masculina, grave", grave: true, padrao: true },
46
- { id: "pt-BR-FranciscaNeural", nome: "Francisca", tom: "feminina, calorosa" },
47
- { id: "pt-BR-ThalitaMultilingualNeural", nome: "Thalita", tom: "feminina, moderna" },
48
- { id: "pt-PT-DuarteNeural", nome: "Duarte", tom: "masculina de Portugal", grave: true },
49
- { id: "pt-PT-RaquelNeural", nome: "Raquel", tom: "feminina de Portugal" }
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
- /* ---------------------------------------------------------------- DRM --- */
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
- function escapar(t) {
80
- return String(t || "")
81
- .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
82
- .replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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
- /* -------------------------------------------------------------- neural -- */
62
+ /* --------------------------------------------------------------- fala --- */
86
63
 
87
- /* Uma frase → um Blob de MP3. Abre a conexão, manda a configuração e o
88
- SSML, junta os pedaços binários até o "turn.end". */
89
- function pelaNeural(texto, cfg) {
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 voz = cfg.voz || vozPadrao();
92
- /* Voz de narração pede um passo mais lento e um tom um pouco abaixo — é
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
- /* -------------------------------------------------------------- google -- */
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
- /* Pelo servidor do Studio: o Google não manda cabeçalho de CORS, então do
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
- /* --------------------------------------------------------------- fala --- */
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
- if (cfg.motor === "google") return pelaGoogle(t);
199
- return pelaNeural(t, cfg).catch(function (e) {
200
- if (window.console) console.warn("[voz] neural falhou, indo de Google:", e.message);
201
- return pelaGoogle(t);
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