primocode 8.42.2 → 9.1.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 (104) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  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 +42 -195
  7. package/lib/catalogo.js +100 -61
  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 +576 -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 +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
@@ -1,903 +0,0 @@
1
- /* ==========================================================================
2
- player.js — apresenta, reproduz com trilha, grava e imprime.
3
- Serve tanto para /v/<id> quanto para o HTML autônomo exportado.
4
- ========================================================================== */
5
-
6
- (function () {
7
- "use strict";
8
-
9
- var palco = document.getElementById("palco");
10
- if (!palco) return;
11
-
12
- var arq = null, doc = {}, cenas = [], atual = 0;
13
- var tocando = false, quadro = null, t0 = 0, decorrido = 0;
14
- var trilha = null, gravador = null, pedacos = [];
15
- /* Resolve quando o arquivo está MONTADO no palco. É por ela que a exportação
16
- escondida (studio_exportar) sabe quando pode começar — antes disto não há
17
- cena para fotografar, e um export que começa cedo sai preto. */
18
- var carregadoOk = null;
19
- var carregado = new Promise(function (ok) { carregadoOk = ok; });
20
- var ehVideo = false, transporte = null;
21
-
22
- /* ------------------------------------------------------------- avisos -- */
23
-
24
- function aviso(texto, ms) {
25
- var a = document.querySelector(".aviso");
26
- if (!a) { a = document.createElement("div"); a.className = "aviso"; document.body.appendChild(a); }
27
- a.textContent = texto;
28
- a.classList.add("on");
29
- clearTimeout(a._t);
30
- a._t = setTimeout(function () { a.classList.remove("on"); }, ms || 1800);
31
- }
32
-
33
- function mmss(s) {
34
- s = Math.max(0, Math.floor(s || 0));
35
- return String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
36
- }
37
-
38
- /* Quanto dura o maior trecho de vídeo do usuário nesta cena.
39
- Cortar as pausas de uma gravação vira uma fila de cenas, cada uma com um
40
- pedaço do MESMO arquivo. Se cada uma dessas cenas durasse os 4 segundos
41
- do padrão, o corte não seria corte nenhum: o trecho de 1,2s ficaria
42
- congelado 2,8s e o de 9s sairia pela metade. O trecho é que manda. */
43
- function duracaoClipe(c) {
44
- var maior = 0;
45
- (c.elementos || []).forEach(function (e) {
46
- if (!e || e.tipo !== "clipe") return;
47
- var de = Number(e.de) || 0, ate = Number(e.ate);
48
- if (isFinite(ate) && ate > de) maior = Math.max(maior, ate - de);
49
- });
50
- return maior;
51
- }
52
-
53
- function duracaoDe(i) {
54
- /* "duracao": "3s" (texto) fazia acc virar concatenação e o relógio dava
55
- vídeo de 00:00. Número são ou o padrão — nunca corrente de texto. */
56
- var c = cenas[i] || {};
57
- var d = Number(c.duracao);
58
- /* O padrão de 4s vale para cena de texto. Onde há gravação, o padrão é o
59
- trecho — e o teto sobe junto, porque 60s é guarda contra erro de
60
- digitação e um pedaço de vídeo medido não é erro de digitação. */
61
- var clipe = duracaoClipe(c);
62
- if (!(isFinite(d) && d > 0)) d = clipe > 0 ? Math.min(600, clipe) : 4;
63
- else d = Math.min(clipe > 0 ? 600 : 60, d);
64
- /* A narração pré-renderizada tem duração conhecida (o servidor mediu o
65
- WAV e gravou `narracaoDur` na cena). A cena dura o que for MAIOR:
66
- o que o autor pediu ou a fala inteira + um respiro. É isto que torna
67
- a linha do tempo determinística — o "cena espera a voz" do laço vira
68
- só a rede de segurança para narração sintetizada na hora. */
69
- var n = Number(c.narracaoDur);
70
- if (isFinite(n) && n > 0 && c.narracaoAudio) d = Math.max(d, Math.min(90, n + 0.4));
71
- return d;
72
- }
73
- function total() { return cenas.reduce(function (s, _, i) { return s + duracaoDe(i); }, 0); }
74
- function inicioDe(i) { var s = 0; for (var k = 0; k < i; k++) s += duracaoDe(k); return s; }
75
-
76
- /* ----------------------------------------------------------- montagem -- */
77
-
78
- function montar(a) {
79
- arq = a;
80
- doc = a.doc || {};
81
- document.title = a.titulo || "PrimoCode";
82
- palco.innerHTML = "";
83
- ehVideo = a.ferramenta === "corte";
84
-
85
- if (a.ferramenta === "prosa") return montarProsa();
86
- if (a.ferramenta === "grade") return montarGrade();
87
-
88
- cenas = doc.cenas || [];
89
- // O rodapé (nome do deck, "03 / 08") é coisa de APRESENTAÇÃO: o vídeo e
90
- // a arte não têm. Ver o rodapé em render.js.
91
- var rodape = a.ferramenta === "prisma" ? { titulo: a.titulo, total: cenas.length } : null;
92
- cenas.forEach(function (c, i) {
93
- var no = renderCena(c, doc, {
94
- animar: true,
95
- rodape: rodape ? { titulo: rodape.titulo, indice: i + 1, total: rodape.total } : null,
96
- });
97
- no.style.position = "absolute";
98
- no.style.inset = "0";
99
- no.style.display = i === 0 ? "block" : "none";
100
- palco.appendChild(no);
101
- });
102
- palco.style.cssText = "position:absolute;left:50%;top:50%;width:" +
103
- (doc.largura || 1920) + "px;height:" + (doc.altura || 1080) + "px";
104
-
105
- ajustar();
106
- ir(0);
107
- if (carregadoOk) { carregadoOk(a); carregadoOk = null; }
108
-
109
- if (location.hash === "#imprimir") return setTimeout(imprimirTudo, 350);
110
-
111
- if (ehVideo) {
112
- montarTransporte();
113
- prepararTrilha();
114
- tocar();
115
- // /v/<id>#gravar: o CLI abre assim quando o usuário pede "baixa o
116
- // vídeo". O navegador pede a permissão de tela (a "aprovação"), o
117
- // usuário aceita, o vídeo roda do zero gravando e baixa no fim —
118
- // nenhuma chamada de IA no caminho inteiro.
119
- if (location.hash.indexOf("gravar") !== -1) {
120
- setTimeout(function () {
121
- aviso("aceite a permissão para gravar — o download sai no fim", 4000);
122
- iniciarGravacao();
123
- }, 700);
124
- }
125
- }
126
- }
127
-
128
- function montarProsa() {
129
- palco.style.cssText = "position:static";
130
- document.body.style.overflow = "auto";
131
- document.body.style.background = "#17171b";
132
- var w = document.createElement("div");
133
- w.style.cssText = "display:grid;place-items:center;padding:40px 20px";
134
- w.appendChild(renderProsa(doc));
135
- palco.appendChild(w);
136
- }
137
-
138
- function montarGrade() {
139
- palco.style.cssText = "position:static";
140
- document.body.style.overflow = "auto";
141
- document.body.style.background = "#17171b";
142
- var g = document.createElement("div");
143
- g.style.cssText = "padding:26px";
144
- (doc.abas || []).forEach(function (aba, i) {
145
- var t = document.createElement("div");
146
- t.style.cssText = "color:#8d8d9a;font:600 11px/1 system-ui;letter-spacing:.12em;" +
147
- "text-transform:uppercase;margin:22px 0 10px";
148
- t.textContent = aba.nome || "Planilha " + (i + 1);
149
- g.appendChild(t);
150
- var caixa = document.createElement("div");
151
- caixa.style.cssText = "overflow:auto;border-radius:12px;border:1px solid rgba(255,255,255,.09)";
152
- caixa.appendChild(renderGrade(doc, { aba: i }));
153
- g.appendChild(caixa);
154
- });
155
- palco.appendChild(g);
156
- }
157
-
158
- function ajustar() {
159
- if (!cenas.length) return;
160
- var alturaUI = ehVideo ? 74 : 0;
161
- var larguraUI = painel ? 360 : 0;
162
- var e = Math.min((window.innerWidth - larguraUI) / (doc.largura || 1920),
163
- (window.innerHeight - alturaUI) / (doc.altura || 1080));
164
- palco.style.left = ((window.innerWidth - larguraUI) / 2) + "px";
165
- palco.style.transform = "translate(-50%,-50%) scale(" + e + ")";
166
- palco.style.marginTop = ehVideo ? (-alturaUI / 2) + "px" : "0";
167
- }
168
-
169
- /* ---------------------------------------------------------- navegação -- */
170
-
171
- function ir(n, semRelogio) {
172
- if (!cenas.length) return;
173
- var anterior = atual;
174
- atual = Math.max(0, Math.min(cenas.length - 1, n));
175
- if (anterior !== atual) pararClipes(anterior); // o áudio da cena que saiu vai junto
176
- Array.prototype.forEach.call(palco.children, function (no, i) {
177
- no.style.display = i === atual ? "block" : "none";
178
- if (i === atual) {
179
- no.classList.remove("animar");
180
- void no.offsetWidth; // reinicia entrada e transição
181
- no.classList.add("animar");
182
- contar(no);
183
- }
184
- });
185
- /* SFX na entrada da cena — só durante a reprodução (navegar com as setas
186
- não faz barulho) e nunca na primeira montagem. `doc.sfx === false`
187
- desliga tudo; `cena.sfx` escolhe o efeito ("whoosh" | "impacto" |
188
- "click" | "nenhum") e ganha do automático por transição. */
189
- if (sfx && tocando && ehVideo && atual !== anterior && doc.sfx !== false) {
190
- var cenaNova = cenas[atual] || {};
191
- var somDaCena = cenaNova.som != null ? cenaNova.som : cenaNova.sfx;
192
- if (somDaCena !== "nenhum") {
193
- if (somDaCena) sfx.tocar(somDaCena);
194
- else sfx.transicao(cenaNova.transicao, atual);
195
- }
196
- /* SOM POR ELEMENTO: `som` num elemento toca quando ELE entra — no
197
- mesmo instante da animação de entrada (o mesmo atraso da cascata
198
- que o render.js dá ao --atraso). Um número que faz "ding" ao
199
- aparecer, um ícone que faz "pop", a barra que sobe com o riser: é
200
- o que separa motion de slide animado. Nome da biblioteca ou
201
- {arquivo, volume} — o arquivo entra no grafo e vai para o MP4. */
202
- agendarSonsDosElementos(cenaNova);
203
- }
204
- narrar();
205
- pintarApresentador();
206
- if (!semRelogio) decorrido = inicioDe(atual);
207
- sincronizarClipes();
208
- pintar();
209
- if (location.hash.indexOf("#imprimir") !== 0) {
210
- history.replaceState(null, "", "#/" + (atual + 1));
211
- }
212
- }
213
-
214
- function proxima() { if (atual < cenas.length - 1) ir(atual + 1); else if (ehVideo) fim(); }
215
- function anterior() { ir(atual - 1); }
216
-
217
- /* ── clipes: a gravação do usuário andando junto com a cena ─────────────
218
- Um `clipe` é um trecho do arquivo — `de` e `ate` em segundos. Quem manda
219
- no tempo é o relógio do player, não o do <video>: senão cada cena com
220
- gravação puxaria a linha do tempo para o lado dela e a trilha, a narração
221
- e a exportação sairiam cada uma num compasso.
222
-
223
- Mas corrigir `currentTime` a cada quadro deixa a imagem tremida e corta o
224
- áudio. Então o <video> toca sozinho, que é o que ele faz bem, e só é
225
- puxado de volta quando a diferença passa de um quinto de segundo — que é
226
- mais que o ouvido perdoa numa fala. */
227
- var DERIVA = 0.2;
228
-
229
- function clipesDe(i) {
230
- var no = palco.children[i];
231
- return no ? no.querySelectorAll("video") : [];
232
- }
233
-
234
- function trechoDe(v) {
235
- var de = parseFloat(v.dataset.de), ate = parseFloat(v.dataset.ate);
236
- return { de: isFinite(de) ? de : 0, ate: isFinite(ate) ? ate : Infinity };
237
- }
238
-
239
- /* A voz do usuário tem de entrar no ARQUIVO, não só sair na caixa de som.
240
- O MP4 leva as faixas de áudio que estão no grafo da trilha e mais nada —
241
- um <video> tocando por fora sai pelo alto-falante e some do arquivo, que
242
- é o pior dos mundos: parece certo na tela e chega mudo em quem recebe.
243
-
244
- Entra pela `entradaVoz`, a mesma porta da narração: fica fora do volume
245
- da trilha e fora do duck. Quem abaixa é a música; a fala, nunca.
246
- `createMediaElementSource` só aceita ser chamado uma vez por elemento —
247
- daí a marca no próprio nó. */
248
- function ligarAoGrafo(v) {
249
- if (v._ligado || !trilha || !trilha.ctx || !trilha.entradaVoz) return;
250
- v._ligado = true;
251
- if (v.muted) return; // clipe mudo não tem o que ligar
252
- try {
253
- trilha.ctx.createMediaElementSource(v).connect(trilha.entradaVoz);
254
- if (trilha.duck) trilha.duck(true); // a música cede enquanto ele fala
255
- } catch (_) { /* já conectado, ou sem Web Audio: toca direto mesmo */ }
256
- }
257
-
258
- function tocarClipe(v) {
259
- ligarAoGrafo(v);
260
- if (trilha && trilha.ctx && trilha.ctx.state === "suspended") trilha.ctx.resume();
261
- var p = v.play();
262
- if (p && p.catch) p.catch(function () {}); // autoplay barrado: fica o quadro
263
- }
264
-
265
- function pararClipes(i) {
266
- Array.prototype.forEach.call(clipesDe(i), function (v) { try { v.pause(); } catch (e) {} });
267
- }
268
-
269
- /* Chamado a cada quadro do laço, e depois de qualquer salto no tempo. */
270
- function sincronizarClipes() {
271
- var vs = clipesDe(atual);
272
- if (!vs.length) return;
273
- var dentro = Math.max(0, decorrido - inicioDe(atual));
274
- Array.prototype.forEach.call(vs, function (v) {
275
- if (v.readyState < 1) return; // ainda sem metadados: nada a buscar
276
- var t = trechoDe(v);
277
- var alvo = t.de + dentro;
278
- /* Passou do fim do trecho: segura o último quadro em vez de invadir o
279
- que vem depois no arquivo — invadir é justamente a pausa que se
280
- queria cortar. */
281
- if (alvo >= t.ate) { if (!v.paused) v.pause(); return; }
282
- /* Uma busca por vez, e com respiro. Numa máquina lenta (ou na
283
- exportação escondida, que rasteriza a cena a cada quadro no mesmo
284
- fio), o <video> fica atrás do relógio por mais de DERIVA em TODO
285
- quadro: puxá-lo de volta a cada rAF é uma busca em cima da outra, e
286
- um vídeo que só busca nunca tem quadro pronto — readyState fica em 1,
287
- o export não desenha o clipe e o arquivo sai com o chão da cena no
288
- lugar da gravação. Medido quadro a quadro no diagnóstico. */
289
- if (v.seeking) return;
290
- var agora = performance.now();
291
- if (Math.abs(v.currentTime - alvo) > DERIVA && (!v._ultimaBusca || agora - v._ultimaBusca > 600)) {
292
- v._ultimaBusca = agora;
293
- try { v.currentTime = alvo; } catch (e) {}
294
- }
295
- if (tocando && v.paused) tocarClipe(v);
296
- else if (!tocando && !v.paused) v.pause();
297
- });
298
- }
299
-
300
- /* números que contam do zero até o valor ------------------------------- */
301
-
302
- function contar(cena) {
303
- Array.prototype.forEach.call(cena.querySelectorAll("b[data-contar]"), function (b) {
304
- var alvo = parseFloat(b.getAttribute("data-contar"));
305
- var molde = b.getAttribute("data-molde") || String(alvo);
306
- var casas = (String(alvo).split(".")[1] || "").length;
307
- var trecho = molde.match(/-?[\d.,]+/);
308
- if (!trecho) return;
309
- var inicio = performance.now(), dur = 1100;
310
- (function passo(agora) {
311
- var t = Math.min(1, (agora - inicio) / dur);
312
- var suave = 1 - Math.pow(1 - t, 3);
313
- var v = (alvo * suave).toFixed(casas);
314
- b.textContent = molde.replace(trecho[0], String(v).replace(".", casas ? "," : ""));
315
- if (t < 1) requestAnimationFrame(passo);
316
- else b.textContent = molde;
317
- })(inicio);
318
- });
319
- }
320
-
321
- /* --------------------------------------------------------- reprodução -- */
322
-
323
- function tocar() {
324
- if (tocando || !cenas.length) return;
325
- tocando = true;
326
- t0 = performance.now() - decorrido * 1000;
327
- if (trilha) { trilha.tocar(); }
328
- sincronizarClipes();
329
- laco();
330
- atualizarBotao();
331
- }
332
-
333
- function pausar() {
334
- tocando = false;
335
- cancelAnimationFrame(quadro);
336
- pararClipes(atual);
337
- if (trilha) trilha.parar();
338
- if (typeof Narracao !== "undefined") Narracao.parar();
339
- if (vozGravada) { vozGravada.pause(); vozGravada = null; }
340
- atualizarBotao();
341
- }
342
-
343
- function fim() {
344
- pausar();
345
- decorrido = total();
346
- pintar();
347
- aviso("fim", 1200);
348
- if (gravador && gravador.state === "recording") setTimeout(pararGravacao, 600);
349
- if (gravadorAudio && gravadorAudio.state === "recording") setTimeout(pararGravacao, 600);
350
- }
351
-
352
- /* A narração da cena ainda está no ar? */
353
- function narrando() {
354
- if (vozGravada && !vozGravada.ended && !vozGravada.paused) return true;
355
- if (typeof Narracao !== "undefined" && Narracao.falando && Narracao.falando()) return true;
356
- return false;
357
- }
358
-
359
- function laco() {
360
- if (!tocando) return;
361
- decorrido = (performance.now() - t0) / 1000;
362
- var alvo = cenas.length - 1;
363
- var acc = 0;
364
- for (var i = 0; i < cenas.length; i++) {
365
- acc += duracaoDe(i);
366
- if (decorrido < acc) { alvo = i; break; }
367
- }
368
- /* A CENA ESPERA A VOZ. O relógio andava só pela `duracao`, e uma narração
369
- mais longa que a cena vazava por cima da seguinte — era a "narração
370
- fora de sincronia". Se a fala da cena atual ainda está no ar, o relógio
371
- congela na beirada da cena: quem dita o corte é a voz. */
372
- var fimDaAtual = inicioDe(atual) + duracaoDe(atual);
373
- if ((alvo !== atual || decorrido >= total()) && narrando()) {
374
- t0 = performance.now() - (fimDaAtual - 0.05) * 1000;
375
- decorrido = fimDaAtual - 0.05;
376
- pintar();
377
- quadro = requestAnimationFrame(laco);
378
- return;
379
- }
380
- if (decorrido >= total()) return fim();
381
- if (alvo !== atual) ir(alvo, true);
382
- sincronizarClipes();
383
- pintar();
384
- quadro = requestAnimationFrame(laco);
385
- }
386
-
387
- function buscar(segundos) {
388
- decorrido = Math.max(0, Math.min(total() - 0.01, segundos));
389
- var acc = 0, alvo = 0;
390
- for (var i = 0; i < cenas.length; i++) {
391
- acc += duracaoDe(i);
392
- if (decorrido < acc) { alvo = i; break; }
393
- alvo = i;
394
- }
395
- ir(alvo, true);
396
- t0 = performance.now() - decorrido * 1000;
397
- if (trilha && trilha.elemento) trilha.elemento.currentTime = (doc.trilha.inicio || 0) + decorrido;
398
- sincronizarClipes();
399
- pintar();
400
- }
401
-
402
- /* Antes de exportar, os clipes precisam ter chegado — `drawImage` de um
403
- <video> sem quadro decodificado desenha nada, e o resultado seria a
404
- gravação do usuário faltando do vídeo dele. Espera com prazo: um arquivo
405
- que não carrega não pode travar a exportação da peça inteira. */
406
- function clipesProntos(prazo) {
407
- var todos = [];
408
- Array.prototype.forEach.call(palco.querySelectorAll("video"), function (v) {
409
- if (v.readyState >= 2) return;
410
- todos.push(new Promise(function (ok) {
411
- var pronto = function () { v.removeEventListener("loadeddata", pronto); ok(); };
412
- v.addEventListener("loadeddata", pronto);
413
- v.addEventListener("error", pronto);
414
- try { v.load(); } catch (e) {}
415
- }));
416
- });
417
- if (!todos.length) return Promise.resolve();
418
- return Promise.race([
419
- Promise.all(todos),
420
- new Promise(function (ok) { setTimeout(ok, prazo || 15000); }),
421
- ]);
422
- }
423
-
424
- /* ---------------------------------------------------------- narração -- */
425
-
426
- var vozGravada = null;
427
-
428
- function narrar() {
429
- if (typeof Narracao !== "undefined") Narracao.parar();
430
- if (vozGravada) { vozGravada.pause(); vozGravada = null; }
431
- if (trilha && trilha.duck) trilha.duck(false);
432
- if (mudo || doc.narracao === false) return;
433
-
434
- var cena = cenas[atual] || {};
435
-
436
- // voz gravada pelo usuário ganha da sintetizada, sempre
437
- if (cena.narracaoAudio) {
438
- vozGravada = new Audio(cena.narracaoAudio);
439
- vozGravada.volume = (doc.voz && doc.voz.volume != null) ? doc.voz.volume : 1;
440
- /* Pelo grafo da trilha, quando ele existe: a voz entra na gravação MP4
441
- sem "compartilhar áudio da aba", e a música cede espaço (duck)
442
- enquanto a fala dura. Sem grafo, toca direto como antes. */
443
- if (trilha && trilha.ctx && trilha.entradaVoz) {
444
- try {
445
- var fonteVoz = trilha.ctx.createMediaElementSource(vozGravada);
446
- fonteVoz.connect(trilha.entradaVoz);
447
- } catch (_) { /* elemento já conectado ou grafo indisponível */ }
448
- if (trilha.duck) {
449
- trilha.duck(true);
450
- vozGravada.onended = function () { if (trilha && trilha.duck) trilha.duck(false); };
451
- }
452
- }
453
- var p = vozGravada.play();
454
- if (p && p.catch) p.catch(function () {});
455
- return;
456
- }
457
-
458
- if (typeof Narracao === "undefined" || !Narracao.disponivel()) return;
459
- if (cena.narracao) {
460
- if (trilha && trilha.duck) {
461
- trilha.duck(true);
462
- Narracao.falar(cena.narracao, doc.voz || {}).then(function () {
463
- if (trilha && trilha.duck) trilha.duck(false);
464
- });
465
- } else {
466
- Narracao.falar(cena.narracao, doc.voz || {});
467
- }
468
- }
469
- }
470
-
471
- /* ----------------------------------------------------- apresentador --- */
472
-
473
- var painel = null, cronometro = 0, tiquetaque = null;
474
-
475
- function alternarApresentador() {
476
- if (painel) {
477
- painel.remove(); painel = null;
478
- clearInterval(tiquetaque); tiquetaque = null;
479
- ajustar();
480
- return;
481
- }
482
- painel = document.createElement("div");
483
- painel.className = "apresentador";
484
- painel.innerHTML =
485
- '<div><h4>Tempo</h4><div class="relogio" id="ap-relogio">00:00</div></div>' +
486
- '<div><h4>Notas</h4><div class="corpo" id="ap-notas"></div></div>' +
487
- '<div class="prox" id="ap-prox"></div>' +
488
- '<div class="dica">→ avança · ← volta<br>S fecha · T zera o tempo<br>F tela cheia · P imprime</div>';
489
- document.body.appendChild(painel);
490
- tiquetaque = setInterval(function () {
491
- cronometro++;
492
- var r = document.getElementById("ap-relogio");
493
- if (r) r.textContent = mmss(cronometro);
494
- }, 1000);
495
- pintarApresentador();
496
- ajustar();
497
- }
498
-
499
- function pintarApresentador() {
500
- if (!painel) return;
501
- var notas = (cenas[atual] || {}).notas;
502
- document.getElementById("ap-notas").innerHTML = notas
503
- ? String(notas).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/\n/g, "<br>")
504
- : '<span style="color:#5e6a77">— sem notas —</span>';
505
- var prox = cenas[atual + 1];
506
- var t = prox ? (prox.elementos || []).filter(function (e) {
507
- return e.tipo === "texto" || e.tipo === "citacao";
508
- })[0] : null;
509
- document.getElementById("ap-prox").textContent = prox
510
- ? "Próxima: " + ((t && t.texto) || "cena " + (atual + 2)).split("\n")[0]
511
- : "Última cena";
512
- }
513
-
514
- /* ------------------------------------------------------------ trilha -- */
515
-
516
- var sfx = null;
517
-
518
- /* Os sons dos ELEMENTOS da cena que acabou de entrar. O atraso é o mesmo
519
- que o render.js dá à animação de entrada (cascata: i × 0,09 s + 0,1 s, ou
520
- o `anim.atraso` declarado), então o som cai no instante em que o
521
- elemento aparece. Trocar de cena antes cancela o que ainda não tocou —
522
- senão o "ding" da cena 3 tocava em cima da cena 4. */
523
- var sonsAgendados = [];
524
- function agendarSonsDosElementos(cena) {
525
- sonsAgendados.forEach(clearTimeout);
526
- sonsAgendados = [];
527
- if (!sfx || !cena || !Array.isArray(cena.elementos)) return;
528
- cena.elementos.forEach(function (e, i) {
529
- if (!e || !e.som) return;
530
- var atraso = (e.anim && typeof e.anim === "object" && e.anim.atraso != null)
531
- ? +e.anim.atraso
532
- : (e.chaves && e.chaves.length > 1 ? 0 : i * 0.09 + 0.1);
533
- if (typeof e.som === "object" && e.som.em != null) atraso = +e.som.em; // segundos na cena, explícito
534
- sonsAgendados.push(setTimeout(function () { sfx.tocar(e.som); }, Math.max(0, atraso) * 1000));
535
- });
536
- }
537
-
538
- function prepararTrilha() {
539
- if (typeof criarTrilha !== "function") return;
540
- var cfg = doc.trilha;
541
- /* Sem trilha o grafo ainda nasce (tipo "nenhuma"): é por ele que a
542
- narração e os SFX entram na gravação MP4. */
543
- if (!cfg || (!cfg.src && cfg.tipo !== "gerada")) cfg = { tipo: "nenhuma" };
544
- trilha = criarTrilha(cfg, function (msg) { aviso(msg, 3000); });
545
- if (trilha && trilha.ctx && trilha.no && typeof criarSfx === "function" && doc.sfx !== false) {
546
- sfx = criarSfx(trilha.ctx, trilha.no);
547
- }
548
- }
549
-
550
- /* --------------------------------------------------------- transporte -- */
551
-
552
- function montarTransporte() {
553
- transporte = document.createElement("div");
554
- transporte.className = "transporte";
555
- transporte.innerHTML =
556
- '<button class="tp-btn" id="tp-tocar" title="Tocar / pausar (K)"></button>' +
557
- '<div class="tp-tempo" id="tp-agora">00:00</div>' +
558
- '<div class="tp-barra" id="tp-barra"><div class="tp-cheio" id="tp-cheio"></div>' +
559
- '<div class="tp-marcas" id="tp-marcas"></div></div>' +
560
- '<div class="tp-tempo" id="tp-total">00:00</div>' +
561
- '<button class="tp-btn pequeno" id="tp-som" title="Mudo (M)"></button>' +
562
- '<input class="tp-vol" id="tp-vol" type="range" min="0" max="100" value="70">' +
563
- '<button class="tp-btn texto primo" id="tp-gravar" title="Baixar vídeo ou áudio">Baixar ▾</button>';
564
- document.body.appendChild(transporte);
565
-
566
- document.getElementById("tp-tocar").onclick = function () { tocando ? pausar() : tocar(); };
567
- document.getElementById("tp-som").onclick = alternarMudo;
568
- document.getElementById("tp-gravar").onclick = menuBaixar;
569
- document.getElementById("tp-vol").oninput = function () {
570
- if (trilha) trilha.volume(this.value / 100);
571
- };
572
-
573
- var barra = document.getElementById("tp-barra");
574
- barra.onclick = function (ev) {
575
- var r = barra.getBoundingClientRect();
576
- buscar(((ev.clientX - r.left) / r.width) * total());
577
- };
578
-
579
- var marcas = document.getElementById("tp-marcas");
580
- cenas.forEach(function (_, i) {
581
- if (!i) return;
582
- var m = document.createElement("i");
583
- m.style.left = (inicioDe(i) / total() * 100) + "%";
584
- marcas.appendChild(m);
585
- });
586
-
587
- document.getElementById("tp-total").textContent = mmss(total());
588
- atualizarBotao();
589
- }
590
-
591
- function atualizarBotao() {
592
- var b = document.getElementById("tp-tocar");
593
- if (b) b.innerHTML = svgIcone(tocando ? "pausa" : "play");
594
- var s = document.getElementById("tp-som");
595
- if (s) s.innerHTML = svgIcone("olho");
596
- }
597
-
598
- var mudo = false;
599
- function alternarMudo() {
600
- mudo = !mudo;
601
- if (trilha) trilha.volume(mudo ? 0 : (document.getElementById("tp-vol").value / 100));
602
- aviso(mudo ? "som desligado" : "som ligado", 900);
603
- }
604
-
605
- function pintar() {
606
- var c = document.getElementById("tp-cheio");
607
- if (!c) return;
608
- c.style.width = (decorrido / total() * 100) + "%";
609
- document.getElementById("tp-agora").textContent = mmss(decorrido);
610
- }
611
-
612
- /* ---------------------------------------------------------- gravação --- */
613
-
614
- function alternarGravacao() {
615
- if (gravador && gravador.state === "recording") return pararGravacao();
616
- iniciarGravacao();
617
- }
618
-
619
- /* Menu do botão Baixar: vídeo completo ou só o áudio.
620
-
621
- O áudio sai DIRETO do grafo (trilha + narração + SFX, já mixados com
622
- ducking) — sem pedir permissão de tela nenhuma, porque não há tela:
623
- é o MediaRecorder pendurado no fluxo da Web Audio. */
624
- function menuBaixar() {
625
- if ((gravador && gravador.state === "recording") ||
626
- (gravadorAudio && gravadorAudio.state === "recording")) {
627
- return pararGravacao();
628
- }
629
- var velho = document.getElementById("menu-baixar");
630
- if (velho) { velho.remove(); return; }
631
- var m = document.createElement("div");
632
- m.id = "menu-baixar";
633
- m.className = "menu-baixar";
634
- m.innerHTML =
635
- '<button data-acao="mp4">🎬 Vídeo completo <span>' + (gravaMp4() ? "MP4" : "WebM") + " · imagem + som</span></button>" +
636
- '<button data-acao="audio">🎧 Só o áudio <span>narração + trilha + efeitos</span></button>';
637
- document.body.appendChild(m);
638
- m.onclick = function (ev) {
639
- var b = ev.target.closest("button");
640
- if (!b) return;
641
- m.remove();
642
- if (b.dataset.acao === "mp4") iniciarGravacao();
643
- else iniciarGravacaoAudio();
644
- };
645
- setTimeout(function () {
646
- document.addEventListener("click", function fecha(e) {
647
- if (!m.contains(e.target)) { m.remove(); document.removeEventListener("click", fecha); }
648
- });
649
- }, 0);
650
- }
651
-
652
- /* ------------------------------------------------ gravação só de áudio -- */
653
-
654
- var gravadorAudio = null, pedacosAudio = [];
655
-
656
- var FORMATOS_AUDIO = [
657
- "audio/mp4", // AAC → .m4a: toca em tudo, importa em tudo
658
- "audio/webm;codecs=opus",
659
- "audio/webm",
660
- ];
661
-
662
- function iniciarGravacaoAudio() {
663
- if (!trilha || !trilha.fluxo) return aviso("áudio indisponível neste navegador", 2500);
664
- var tipo = "";
665
- for (var i = 0; i < FORMATOS_AUDIO.length; i++) {
666
- if (MediaRecorder.isTypeSupported(FORMATOS_AUDIO[i])) { tipo = FORMATOS_AUDIO[i]; break; }
667
- }
668
- pedacosAudio = [];
669
- gravadorAudio = new MediaRecorder(trilha.fluxo, tipo ? { mimeType: tipo, audioBitsPerSecond: 192000 } : undefined);
670
- gravadorAudio.ondataavailable = function (ev) { if (ev.data.size) pedacosAudio.push(ev.data); };
671
- gravadorAudio.onstop = baixarAudio;
672
- gravadorAudio.start();
673
- var b = document.getElementById("tp-gravar");
674
- if (b) { b.classList.add("gravando"); b.textContent = "Parar"; }
675
- buscar(0);
676
- tocar();
677
- aviso("gravando o áudio — para sozinho no fim", 2600);
678
- }
679
-
680
- function baixarAudio() {
681
- var mime = (gravadorAudio && gravadorAudio.mimeType) || "audio/webm";
682
- var ext = /mp4/.test(mime) ? ".m4a" : ".weba";
683
- var blob = new Blob(pedacosAudio, { type: mime.split(";")[0] });
684
- var url = URL.createObjectURL(blob);
685
- var a = document.createElement("a");
686
- a.href = url;
687
- a.download = (arq.titulo || "audio").replace(/[^\w\-]+/g, "-").toLowerCase() + ext;
688
- a.click();
689
- setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
690
- aviso("áudio salvo em " + ext, 2600);
691
- gravadorAudio = null;
692
- }
693
-
694
- /* O melhor formato que este navegador sabe gravar.
695
- *
696
- * A ordem importa: H.264 com AAC é o MP4 que toca em qualquer lugar —
697
- * WhatsApp, Instagram, PowerPoint, TV. AV1 em MP4 é MP4 de verdade mas ainda
698
- * não toca em tudo, então vem depois. O webm é o último recurso: abre em
699
- * navegador e pouco mais.
700
- */
701
- var FORMATOS = [
702
- "video/mp4;codecs=avc1.42E01E,mp4a.40.2",
703
- "video/mp4;codecs=avc1,mp4a.40.2",
704
- "video/mp4;codecs=avc1",
705
- "video/mp4",
706
- "video/webm;codecs=vp9,opus",
707
- "video/webm",
708
- ];
709
-
710
- function escolherFormato() {
711
- for (var i = 0; i < FORMATOS.length; i++) {
712
- if (MediaRecorder.isTypeSupported(FORMATOS[i])) return FORMATOS[i];
713
- }
714
- return "video/webm";
715
- }
716
-
717
- function gravaMp4() { return /mp4/.test(escolherFormato()); }
718
-
719
- /* Baixar o vídeo = DESENHAR o vídeo, não gravar a tela.
720
- *
721
- * Aqui havia getDisplayMedia: pedia permissão, o usuário escolhia uma
722
- * janela, e o navegador salvava o que estivesse aparecendo. O ponteiro do
723
- * mouse entrava no vídeo, notificação entrava no vídeo, e a resolução era
724
- * a da janela em vez da do projeto. Quem vai VENDER a ferramenta não pede
725
- * ao cliente para compartilhar a tela para exportar um arquivo dele.
726
- *
727
- * Agora o player TOCA e o video.js fotografa a cena que está no palco,
728
- * quadro a quadro, para um canvas do tamanho exato do projeto. O áudio é
729
- * o mesmo grafo de sempre — trilha, narração e efeitos, já mixados com o
730
- * ducking — então o arquivo sai idêntico ao que se ouve aqui.
731
- */
732
- /* Monta o arquivo com o PLAYER como fonte, sem baixar. É o que o botão usa
733
- por baixo e o que a exportação escondida (studio_exportar) chama por
734
- fora — um caminho só, para o arquivo sair igual nos dois.
735
-
736
- O player é a fonte: ele sabe encadear cenas, narrar no tempo certo e
737
- tocar os efeitos. O exportador só pergunta o que está na tela. */
738
- function gerarArquivo(op) {
739
- op = op || {};
740
- if (typeof VideoExport === "undefined" || !VideoExport.disponivel()) {
741
- return Promise.reject(new Error("este navegador não sabe gerar vídeo"));
742
- }
743
- var fonte = {
744
- duracao: total(),
745
- // A gravação do usuário precisa estar decodificada antes do primeiro
746
- // quadro; sem isso ela sairia do arquivo final, e só nele.
747
- preparar: function () { return clipesProntos(); },
748
- comecar: function () { buscar(0); tocar(); },
749
- parar: function () { pausar(); },
750
- no: function () { return palco.children[atual] || null; },
751
- };
752
- // O AudioContext nasce suspenso sem gesto; a exportação escondida chega
753
- // com um gesto emprestado, e este resume é o que o gasta.
754
- if (trilha && trilha.ctx && trilha.ctx.state === "suspended") { try { trilha.ctx.resume(); } catch (e) {} }
755
- return VideoExport.gerar(arq, {
756
- fonte: fonte,
757
- fps: op.fps,
758
- fluxoAudio: trilha && trilha.fluxo,
759
- aoAndar: op.aoAndar,
760
- });
761
- }
762
-
763
- function iniciarGravacao() {
764
- var b = document.getElementById("tp-gravar");
765
- if (!b || b.dataset.exportando) return;
766
- if (typeof VideoExport === "undefined" || !VideoExport.disponivel()) {
767
- return aviso("este navegador não sabe gerar vídeo", 3200);
768
- }
769
-
770
- b.dataset.exportando = "1";
771
- b.classList.add("gravando");
772
- aviso("montando o vídeo — não feche esta aba", 3000);
773
-
774
- gerarArquivo({
775
- aoAndar: function (f) { b.textContent = Math.round(f * 100) + "%"; },
776
- }).then(function (r) {
777
- VideoExport.salvar(r, arq.titulo);
778
- aviso(r.mp4 ? "vídeo salvo em .mp4" : "salvo em .webm (este navegador não faz mp4)", 3400);
779
- }).catch(function (e) {
780
- aviso("não consegui gerar: " + e.message, 4200);
781
- }).then(function () {
782
- delete b.dataset.exportando;
783
- b.classList.remove("gravando");
784
- b.textContent = "Baixar ▾";
785
- });
786
- }
787
-
788
-
789
- function pararGravacao() {
790
- if (gravador && gravador.state === "recording") gravador.stop();
791
- if (gravadorAudio && gravadorAudio.state === "recording") gravadorAudio.stop();
792
- var b = document.getElementById("tp-gravar");
793
- if (b) { b.classList.remove("gravando"); b.textContent = "Baixar ▾"; }
794
- }
795
-
796
- function baixarGravacao() {
797
- var mime = (gravador && gravador.mimeType) || "video/webm";
798
- var ext = /mp4/.test(mime) ? ".mp4" : ".webm";
799
- var blob = new Blob(pedacos, { type: mime.split(";")[0] });
800
- var url = URL.createObjectURL(blob);
801
- var a = document.createElement("a");
802
- a.href = url;
803
- a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() + ext;
804
- a.click();
805
- setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
806
- aviso("vídeo salvo em " + ext, 2600);
807
- }
808
-
809
- /* -------------------------------------------------------------- print -- */
810
-
811
- function imprimirTudo() {
812
- palco.style.cssText = "position:static;transform:none;width:auto;height:auto";
813
- Array.prototype.forEach.call(palco.children, function (n) {
814
- n.style.display = "block";
815
- n.style.position = "relative";
816
- n.style.inset = "auto";
817
- n.classList.remove("animar");
818
- });
819
- var p = document.getElementById("progresso");
820
- if (p) p.remove();
821
- if (transporte) transporte.remove();
822
- window.print();
823
- }
824
-
825
- /* ------------------------------------------------------------ eventos -- */
826
-
827
- window.addEventListener("resize", ajustar);
828
-
829
- document.addEventListener("keydown", function (ev) {
830
- if (ev.metaKey || ev.ctrlKey || /input|textarea/i.test(ev.target.tagName)) return;
831
- switch (ev.key) {
832
- case "ArrowRight": case " ": case "PageDown":
833
- ev.preventDefault(); if (ehVideo) pausar(); proxima(); break;
834
- case "ArrowLeft": case "PageUp":
835
- ev.preventDefault(); if (ehVideo) pausar(); anterior(); break;
836
- case "Home": pausar(); ir(0); break;
837
- case "End": pausar(); ir(cenas.length - 1); break;
838
- case "k": case "K": tocando ? pausar() : tocar(); break;
839
- case "m": case "M": alternarMudo(); break;
840
- case "s": case "S": alternarApresentador(); break;
841
- case "t": case "T": cronometro = 0; break;
842
- case "n": case "N":
843
- if (typeof Narracao !== "undefined" && Narracao.disponivel()) {
844
- Narracao.falando() ? Narracao.parar() : narrar();
845
- }
846
- break;
847
- case "f": case "F":
848
- if (document.fullscreenElement) document.exitFullscreen();
849
- else document.documentElement.requestFullscreen();
850
- break;
851
- case "p": case "P": window.print(); break;
852
- case "Escape": pausar(); pararGravacao(); break;
853
- }
854
- });
855
-
856
- var tx = null;
857
- document.addEventListener("touchstart", function (e) { tx = e.touches[0].clientX; }, { passive: true });
858
- document.addEventListener("touchend", function (e) {
859
- if (tx == null) return;
860
- var d = e.changedTouches[0].clientX - tx;
861
- if (Math.abs(d) > 50) { pausar(); d < 0 ? proxima() : anterior(); }
862
- tx = null;
863
- }, { passive: true });
864
-
865
- document.addEventListener("click", function (e) {
866
- if (!cenas.length || e.target.closest("a,button,input,.transporte")) return;
867
- if (ehVideo) { tocando ? pausar() : tocar(); return; }
868
- e.clientX < window.innerWidth * 0.3 ? anterior() : proxima();
869
- });
870
-
871
- window.addEventListener("message", function (e) {
872
- var m = e.data || {};
873
- if (m.tipo === "arquivo") montar(m.arquivo);
874
- if (m.tipo === "ir") { pausar(); ir(m.n); }
875
- });
876
-
877
- /* --------------------------------------------------------------- carga */
878
-
879
- var embutido = document.getElementById("dados");
880
- if (embutido) {
881
- montar(JSON.parse(embutido.textContent));
882
- } else {
883
- var id = location.pathname.split("/").pop();
884
- // lido antes de montar: ir() reescreve o hash e não deve pausar a reprodução
885
- var inicio = parseInt((location.hash.match(/^#\/(\d+)/) || [])[1], 10);
886
- fetch("/api/arquivos/" + id)
887
- .then(function (r) { return r.json(); })
888
- .then(function (a) {
889
- if (a.erro) { palco.innerHTML = '<div style="color:#888;padding:40px">Arquivo não encontrado.</div>'; return; }
890
- montar(a);
891
- if (!isNaN(inicio) && inicio > 1) { pausar(); ir(inicio - 1); }
892
- });
893
- }
894
-
895
- window.Player = {
896
- ir: function (n) { ir(n - 1); }, proxima: proxima, anterior: anterior,
897
- tocar: tocar, parar: pausar, buscar: buscar, gravar: alternarGravacao,
898
- get cena() { return atual + 1; }, get total() { return cenas.length; },
899
- // Para a exportação escondida: espera o arquivo montado, troca o que
900
- // precisar (um clipe convertido) e gera pelo mesmo caminho do botão.
901
- carregado: carregado, remontar: montar, exportar: gerarArquivo,
902
- };
903
- })();