vibeeditor-render 0.2.0 → 0.3.1
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/README.md +19 -2
- package/cli/sistema.js +158 -0
- package/cli/vibeeditor-render.js +50 -8
- package/core/audio.js +140 -18
- package/core/cache.js +21 -1
- package/core/config.js +5 -0
- package/core/ffmpeg.js +149 -0
- package/core/jobs.js +169 -24
- package/core/normalizar.js +454 -0
- package/core/nucleo.js +8 -3
- package/core/servidor.js +22 -1
- package/core/transcricao.js +197 -0
- package/core/voz.js +227 -0
- package/host-dist/15fb600d42056579.woff2 +0 -0
- package/host-dist/1895916a733c4d0e.woff2 +0 -0
- package/host-dist/1e96c5356ee905bc.woff2 +0 -0
- package/host-dist/2a8028ec716097bf.woff2 +0 -0
- package/host-dist/3061f5b6eb250216.woff2 +0 -0
- package/host-dist/376dd8dc38524313.woff2 +0 -0
- package/host-dist/41be664a74a50674.woff2 +0 -0
- package/host-dist/428.bundle.js +1 -1
- package/host-dist/46019c506ae1d42c.woff2 +0 -0
- package/host-dist/50129cb1dcd58da5.woff2 +0 -0
- package/host-dist/5ab57485954531d6.woff2 +0 -0
- package/host-dist/606cdd767ee835e0.woff2 +0 -0
- package/host-dist/66df0abfcfe8da3e.woff2 +0 -0
- package/host-dist/6903de9124a5cab6.woff2 +0 -0
- package/host-dist/693cf6c39ab15de9.woff2 +0 -0
- package/host-dist/72884976680cf128.woff2 +0 -0
- package/host-dist/7777133e901cd5ed.woff2 +0 -0
- package/host-dist/78db4f537e4b09d0.woff2 +0 -0
- package/host-dist/7a78f1ce0329757f.woff2 +0 -0
- package/host-dist/7c3e92e6007b76fd.woff2 +0 -0
- package/host-dist/81df42c325a360a6.woff2 +0 -0
- package/host-dist/8633e74025a037d3.woff2 +0 -0
- package/host-dist/872.bundle.js +1 -1
- package/host-dist/8cf68dfb15aca3e4.woff2 +0 -0
- package/host-dist/8fb1a96451cec840.woff2 +0 -0
- package/host-dist/916d3686010a8de2.woff2 +0 -0
- package/host-dist/92d1bbe48d4eec3e.woff2 +0 -0
- package/host-dist/93e25f31623ab732.woff2 +0 -0
- package/host-dist/95045d685be8648f.woff2 +0 -0
- package/host-dist/9545a3d1fceacf5c.woff2 +0 -0
- package/host-dist/96be080130d6f32c.woff2 +0 -0
- package/host-dist/9a881e2ac07d406b.woff2 +0 -0
- package/host-dist/ad20d13d2a924905.woff2 +0 -0
- package/host-dist/af2e87f5d61b5665.woff2 +0 -0
- package/host-dist/b19b14aaaeb386b9.woff2 +0 -0
- package/host-dist/b447814dc676cec1.woff2 +0 -0
- package/host-dist/b7b43acee81d05a8.woff2 +0 -0
- package/host-dist/bd427f25ac24d036.woff2 +0 -0
- package/host-dist/bundle.js +1509 -12
- package/host-dist/bundle.js.map +1 -1
- package/host-dist/c04551857776278f.woff2 +0 -0
- package/host-dist/c050c0dd427d93a6.woff2 +0 -0
- package/host-dist/cb2a73f0b29f1031.woff2 +0 -0
- package/host-dist/d36a2a2bb416f59e.woff2 +0 -0
- package/host-dist/d4df281bd2c98884.woff2 +0 -0
- package/host-dist/d869208648ca5469.woff2 +0 -0
- package/host-dist/db9d659a1acad1e2.woff2 +0 -0
- package/host-dist/ebeb24f1e7287d3a.woff2 +0 -0
- package/host-dist/ef6d3f52c547a6e3.woff2 +0 -0
- package/host-dist/f4646f71883e1c80.woff2 +0 -0
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ Precisa do [Node.js](https://nodejs.org) 20 ou mais novo.
|
|
|
12
12
|
npx vibeeditor-render@latest
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Na primeira vez ele baixa o navegador de render (uns 100 MB) e
|
|
15
|
+
Na primeira vez ele baixa o navegador de render (uns 100 MB) e, em segundo plano, o ffmpeg do tratamento de áudio
|
|
16
|
+
(uns 30 MB; ver Licença) e cria o atalho **Vibe Render** na área de
|
|
16
17
|
trabalho (Windows e Mac). Depois:
|
|
17
18
|
|
|
18
19
|
1. Abra [plat.vibeeditor.com.br/editor/render](https://plat.vibeeditor.com.br/editor/render) e clique em **Conectar**.
|
|
@@ -27,10 +28,20 @@ ele instala o Node.js, se faltar, e cria o atalho.
|
|
|
27
28
|
| `npx vibeeditor-render@latest` | abre o Vibe Render |
|
|
28
29
|
| `… --codigo ABCD-2345` | pareia sem perguntar |
|
|
29
30
|
| `… --pasta "C:\Videos"` | muda a pasta dos vídeos (fica guardada) |
|
|
31
|
+
| `… --voicestudio http://127.0.0.1:3900` | endereço do VoiceStudio deste computador (fica guardado) |
|
|
30
32
|
| `… desparear` | desliga este computador da conta |
|
|
31
33
|
| `… atalho` | (re)cria o atalho na área de trabalho e sai |
|
|
32
34
|
| `… --ajuda` | mostra os comandos |
|
|
33
35
|
|
|
36
|
+
### Voz clonada (VoiceStudio)
|
|
37
|
+
|
|
38
|
+
A página VoiceStudio do site gera a narração com a sua voz clonada pelo [VoiceStudio](https://github.com/debpalash/VoiceStudio),
|
|
39
|
+
um app open source que também roda no seu computador (API em `http://127.0.0.1:3900`). O navegador não alcança
|
|
40
|
+
esse app sozinho, então o Vibe Render repassa os pedidos (`/v1/voz/...`) para ele, só em endereço local
|
|
41
|
+
(`127.0.0.1`, `localhost` ou `[::1]`). O endereço muda pelo site, por `--voicestudio` ou pela variável
|
|
42
|
+
`VIBE_RENDER_VOICESTUDIO` (esta manda) e fica em `voicestudio.json` na pasta de dados. Os pesos do modelo padrão
|
|
43
|
+
do VoiceStudio (OmniVoice) são CC-BY-NC: uso pessoal, não comercial.
|
|
44
|
+
|
|
34
45
|
## Segurança
|
|
35
46
|
|
|
36
47
|
- O servidor escuta só em `127.0.0.1` (portas 47817 a 47820) e confere o cabeçalho `Host` (barra DNS rebinding).
|
|
@@ -51,11 +62,12 @@ npm install
|
|
|
51
62
|
npm test # núcleo, segurança, CLI
|
|
52
63
|
npm start # gera host-dist/ se faltar e abre o Vibe Render
|
|
53
64
|
npm run benchmark # renderiza scripts/amostra.tsx pelo servidor local, nos 2 presets
|
|
65
|
+
node scripts/paridade-etapa1.mjs --hv <hiveverse-editor> # Edição Simples × editor local (docs/M2-MOTOR-ETAPA1.md)
|
|
54
66
|
npm pack # o pacote como vai para o npm (prepack gera o host-dist)
|
|
55
67
|
```
|
|
56
68
|
|
|
57
69
|
- `core/`: Node puro. Servidor local (`servidor.js`), fila (`jobs.js`), cache por sha256 (`cache.js`),
|
|
58
|
-
pareamento (`pareamento.js`), segurança (`seguranca.js`).
|
|
70
|
+
pareamento (`pareamento.js`), segurança (`seguranca.js`), transcrição (`transcricao.js`), VoiceStudio (`voz.js`).
|
|
59
71
|
- `host/`: projeto Remotion com a composição `VibeHost`, que avalia o código gerado dentro do Chromium.
|
|
60
72
|
Vira `host-dist/` no build.
|
|
61
73
|
- `cli/`: o comando `vibeeditor-render`.
|
|
@@ -67,3 +79,8 @@ npm pack # o pacote como vai para o npm (prepack gera o host-dist)
|
|
|
67
79
|
O código deste pacote é MIT (veja `LICENSE`). Ele usa o [Remotion](https://www.remotion.dev/license), que tem
|
|
68
80
|
licença própria: grátis para pessoas, organizações sem fins lucrativos e empresas de até 3 pessoas; acima
|
|
69
81
|
disso, a empresa precisa de uma licença do Remotion.
|
|
82
|
+
|
|
83
|
+
O tratamento de áudio usa o ffmpeg 6.1.1 estático do projeto [ffmpeg-static](https://github.com/eugeneware/ffmpeg-static)
|
|
84
|
+
(release `b6.1.1`), baixado na primeira vez para `<pasta de dados>/ffmpeg/6.1.1/` com sha256 conferido. Esse binário é
|
|
85
|
+
**GPL-3.0** (a licença fica ao lado, em `ffmpeg.LICENSE`) e roda como programa separado; ele não faz parte deste pacote.
|
|
86
|
+
Para usar outro ffmpeg completo: `VIBE_FFMPEG=<caminho do executável>`.
|
package/cli/sistema.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Integração com o sistema, para ninguém precisar abrir o Vibe Render na mão a cada vez que liga o computador:
|
|
2
|
+
// · início automático: sobe junto com o login (Windows: pasta Inicializar; Mac: LaunchAgent; Linux: autostart),
|
|
3
|
+
// minimizado e sem perguntar nada — o pareamento já fica salvo na pasta de dados;
|
|
4
|
+
// · link vibe-render://abrir: o botão "Abrir o Vibe Render" do site chama este link e o navegador pergunta
|
|
5
|
+
// "Abrir Vibe Render?" (Windows: registro do usuário; Linux: x-scheme-handler; Mac: um app pequeno em
|
|
6
|
+
// ~/Applications feito com o osacompile).
|
|
7
|
+
// Tudo fica na conta do usuário (nada de administrador) e cada passo que falhar só é pulado.
|
|
8
|
+
"use strict";
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const os = require("node:os");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const {spawnSync} = require("node:child_process");
|
|
13
|
+
|
|
14
|
+
const PROTOCOLO = "vibe-render";
|
|
15
|
+
const COMANDO = "npx -y vibeeditor-render@latest --sem-atalho --segundo-plano";
|
|
16
|
+
|
|
17
|
+
const rodar = (cmd, args) => spawnSync(cmd, args, {encoding: "utf8", windowsHide: true});
|
|
18
|
+
|
|
19
|
+
/** pasta especial do Windows (Startup, Desktop…), já com OneDrive e nome traduzido */
|
|
20
|
+
function pastaWindows(qual) {
|
|
21
|
+
const r = rodar("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `[Environment]::GetFolderPath('${qual}')`]);
|
|
22
|
+
const p = String(r.stdout || "").trim();
|
|
23
|
+
return r.status === 0 && p ? p : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** onde fica o arquivo do início automático em cada sistema */
|
|
27
|
+
function arquivoInicio(plataforma = process.platform, casa = os.homedir(), env = process.env) {
|
|
28
|
+
if (plataforma === "win32") {
|
|
29
|
+
const startup = pastaWindows("Startup") || path.join(env.APPDATA || path.join(casa, "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
30
|
+
return path.join(startup, "Vibe Render.cmd");
|
|
31
|
+
}
|
|
32
|
+
if (plataforma === "darwin") return path.join(casa, "Library", "LaunchAgents", "br.com.vibeeditor.render.plist");
|
|
33
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(casa, ".config"), "autostart", "vibeeditor-render.desktop");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** conteúdo do arquivo do início automático (minimizado, sem pausa no fim) */
|
|
37
|
+
function conteudoInicio(plataforma = process.platform, dirDados = "") {
|
|
38
|
+
if (plataforma === "win32") return `@echo off\r\nstart "Vibe Render" /min cmd /c "${COMANDO}"\r\n`;
|
|
39
|
+
if (plataforma === "darwin") {
|
|
40
|
+
const log = path.join(dirDados, "inicio.log");
|
|
41
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
42
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
43
|
+
<plist version="1.0">
|
|
44
|
+
<dict>
|
|
45
|
+
<key>Label</key><string>br.com.vibeeditor.render</string>
|
|
46
|
+
<key>ProgramArguments</key>
|
|
47
|
+
<array><string>/bin/bash</string><string>-lc</string><string>${COMANDO}</string></array>
|
|
48
|
+
<key>RunAtLoad</key><true/>
|
|
49
|
+
<key>StandardOutPath</key><string>${log}</string>
|
|
50
|
+
<key>StandardErrorPath</key><string>${log}</string>
|
|
51
|
+
</dict>
|
|
52
|
+
</plist>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
return `[Desktop Entry]
|
|
56
|
+
Type=Application
|
|
57
|
+
Name=Vibe Render
|
|
58
|
+
Comment=Exporta os vídeos do Editor IA neste computador
|
|
59
|
+
Exec=bash -lc "${COMANDO}"
|
|
60
|
+
Terminal=false
|
|
61
|
+
X-GNOME-Autostart-enabled=true
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ligarInicio({dirDados}) {
|
|
66
|
+
const arq = arquivoInicio();
|
|
67
|
+
fs.mkdirSync(path.dirname(arq), {recursive: true});
|
|
68
|
+
fs.writeFileSync(arq, conteudoInicio(process.platform, dirDados), {mode: 0o644});
|
|
69
|
+
return arq;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function desligarInicio() {
|
|
73
|
+
const arq = arquivoInicio();
|
|
74
|
+
const tinha = fs.existsSync(arq);
|
|
75
|
+
fs.rmSync(arq, {force: true});
|
|
76
|
+
return tinha;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const inicioLigado = () => fs.existsSync(arquivoInicio());
|
|
80
|
+
|
|
81
|
+
/** o link vibe-render://abrir chama este iniciador (Windows/Linux); abre visível, mas sem segurar a janela */
|
|
82
|
+
function registrarProtocolo({dirDados}) {
|
|
83
|
+
if (process.platform === "win32") {
|
|
84
|
+
const iniciador = path.join(dirDados, "abrir.cmd");
|
|
85
|
+
fs.writeFileSync(iniciador, `@echo off\r\nstart "Vibe Render" cmd /c "npx -y vibeeditor-render@latest --sem-atalho"\r\n`);
|
|
86
|
+
const chave = `HKCU\\Software\\Classes\\${PROTOCOLO}`;
|
|
87
|
+
const passos = [
|
|
88
|
+
["add", chave, "/ve", "/d", "URL:Vibe Render", "/f"],
|
|
89
|
+
["add", chave, "/v", "URL Protocol", "/d", "", "/f"],
|
|
90
|
+
["add", `${chave}\\shell\\open\\command`, "/ve", "/d", `"${iniciador}" "%1"`, "/f"],
|
|
91
|
+
];
|
|
92
|
+
for (const p of passos) {
|
|
93
|
+
const r = rodar("reg.exe", p);
|
|
94
|
+
if (r.status !== 0) throw new Error(`não consegui registrar o link (${String(r.stderr || r.stdout).trim()})`);
|
|
95
|
+
}
|
|
96
|
+
return `${PROTOCOLO}:// → ${iniciador}`;
|
|
97
|
+
}
|
|
98
|
+
if (process.platform === "darwin") {
|
|
99
|
+
// app de AppleScript que recebe o link e sobe o Vibe Render em segundo plano
|
|
100
|
+
const app = path.join(os.homedir(), "Applications", "Vibe Render.app");
|
|
101
|
+
fs.mkdirSync(path.dirname(app), {recursive: true});
|
|
102
|
+
const log = path.join(dirDados, "abrir.log");
|
|
103
|
+
const script = [
|
|
104
|
+
"on open location u",
|
|
105
|
+
` do shell script "/bin/bash -lc 'nohup ${COMANDO.replace("--segundo-plano", "")} > \\"${log}\\" 2>&1 &'"`,
|
|
106
|
+
"end open location",
|
|
107
|
+
"on run",
|
|
108
|
+
` do shell script "/bin/bash -lc 'nohup ${COMANDO.replace("--segundo-plano", "")} > \\"${log}\\" 2>&1 &'"`,
|
|
109
|
+
"end run",
|
|
110
|
+
];
|
|
111
|
+
fs.rmSync(app, {recursive: true, force: true});
|
|
112
|
+
const r = rodar("osacompile", ["-o", app, ...script.flatMap((l) => ["-e", l])]);
|
|
113
|
+
if (r.status !== 0) throw new Error(`osacompile falhou (${String(r.stderr).trim()})`);
|
|
114
|
+
const plist = path.join(app, "Contents", "Info.plist");
|
|
115
|
+
for (const a of [
|
|
116
|
+
"Add :CFBundleIdentifier string br.com.vibeeditor.render.link",
|
|
117
|
+
"Add :CFBundleURLTypes array", "Add :CFBundleURLTypes:0 dict",
|
|
118
|
+
"Add :CFBundleURLTypes:0:CFBundleURLName string Vibe Render",
|
|
119
|
+
"Add :CFBundleURLTypes:0:CFBundleURLSchemes array",
|
|
120
|
+
`Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string ${PROTOCOLO}`,
|
|
121
|
+
]) rodar("/usr/libexec/PlistBuddy", ["-c", a, plist]);
|
|
122
|
+
rodar("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister", ["-f", app]);
|
|
123
|
+
return `${PROTOCOLO}:// → ${app}`;
|
|
124
|
+
}
|
|
125
|
+
const apps = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "applications");
|
|
126
|
+
fs.mkdirSync(apps, {recursive: true});
|
|
127
|
+
const desk = path.join(apps, "vibeeditor-render-link.desktop");
|
|
128
|
+
fs.writeFileSync(desk, `[Desktop Entry]
|
|
129
|
+
Type=Application
|
|
130
|
+
Name=Vibe Render
|
|
131
|
+
Exec=bash -lc "npx -y vibeeditor-render@latest --sem-atalho" %u
|
|
132
|
+
Terminal=true
|
|
133
|
+
NoDisplay=true
|
|
134
|
+
MimeType=x-scheme-handler/${PROTOCOLO};
|
|
135
|
+
`);
|
|
136
|
+
rodar("xdg-mime", ["default", "vibeeditor-render-link.desktop", `x-scheme-handler/${PROTOCOLO}`]);
|
|
137
|
+
return `${PROTOCOLO}:// → ${desk}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** liga tudo de uma vez; devolve o que deu certo e o que foi pulado (com o motivo) */
|
|
141
|
+
function integrar({dirDados, inicio = true}) {
|
|
142
|
+
const feito = [], pulado = [];
|
|
143
|
+
try {
|
|
144
|
+
feito.push(`link ${registrarProtocolo({dirDados})}`);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
pulado.push(`link vibe-render:// (${e.message})`);
|
|
147
|
+
}
|
|
148
|
+
if (inicio) {
|
|
149
|
+
try {
|
|
150
|
+
feito.push(`início automático em ${ligarInicio({dirDados})}`);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
pulado.push(`início automático (${e.message})`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {feito, pulado};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {integrar, ligarInicio, desligarInicio, inicioLigado, registrarProtocolo, arquivoInicio, conteudoInicio, PROTOCOLO, COMANDO};
|
package/cli/vibeeditor-render.js
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
// vibeeditor-render abre (na primeira vez baixa o navegador de render e pede o código)
|
|
7
7
|
// vibeeditor-render --codigo X pareia com o código do site sem perguntar
|
|
8
8
|
// vibeeditor-render --pasta <dir> muda a pasta onde os vídeos são salvos (fica guardada)
|
|
9
|
-
// vibeeditor-render
|
|
10
|
-
// vibeeditor-render
|
|
9
|
+
// vibeeditor-render --voicestudio <url> endereço do VoiceStudio deste computador (fica guardado)
|
|
10
|
+
// vibeeditor-render desparear desliga este computador da conta
|
|
11
|
+
// vibeeditor-render atalho (re)cria o atalho "Vibe Render", o link vibe-render:// e o início automático
|
|
12
|
+
// vibeeditor-render sem-inicio-automatico não abre mais junto com o computador
|
|
13
|
+
// vibeeditor-render --segundo-plano como o início automático chama: minimizado, sem perguntar nada
|
|
11
14
|
"use strict";
|
|
12
15
|
const fs = require("node:fs");
|
|
13
16
|
const os = require("node:os");
|
|
@@ -19,6 +22,8 @@ const PACOTE = require("../package.json");
|
|
|
19
22
|
const VERSAO = PACOTE.version;
|
|
20
23
|
const RAIZ = path.join(__dirname, "..");
|
|
21
24
|
const SITE = "https://plat.vibeeditor.com.br/editor/render";
|
|
25
|
+
const sistema = require("./sistema");
|
|
26
|
+
const VERSAO_INTEGRACAO = 1; // sobe quando o que sistema.integrar() faz mudar
|
|
22
27
|
|
|
23
28
|
// ------------------------------------------------------------------ terminal
|
|
24
29
|
|
|
@@ -35,8 +40,10 @@ ${negrito("Vibe Render")} ${cinza(`v${VERSAO}`)} · exporta os vídeos do Editor
|
|
|
35
40
|
${azul("npx vibeeditor-render@latest")} abre o Vibe Render
|
|
36
41
|
${azul("npx vibeeditor-render@latest --codigo ABCD-2345")} pareia sem perguntar
|
|
37
42
|
${azul("npx vibeeditor-render@latest --pasta \"C:\\Videos\"")} muda a pasta dos vídeos
|
|
43
|
+
${azul("npx vibeeditor-render@latest --voicestudio http://127.0.0.1:3900")} endereço do VoiceStudio (voz clonada)
|
|
38
44
|
${azul("npx vibeeditor-render@latest desparear")} desliga este computador da sua conta
|
|
39
|
-
${azul("npx vibeeditor-render@latest atalho")}
|
|
45
|
+
${azul("npx vibeeditor-render@latest atalho")} atalho, botão "Abrir" do site e início junto com o computador
|
|
46
|
+
${azul("npx vibeeditor-render@latest sem-inicio-automatico")} não abre mais junto com o computador
|
|
40
47
|
|
|
41
48
|
Deixe a janela aberta enquanto exporta. Para fechar: Ctrl+C.
|
|
42
49
|
Ajuda: ${SITE}
|
|
@@ -44,14 +51,17 @@ Ajuda: ${SITE}
|
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
function lerArgs(argv) {
|
|
47
|
-
const a = {comando: null, codigo: null, pasta: null, semAtalho: false};
|
|
54
|
+
const a = {comando: null, codigo: null, pasta: null, voicestudio: null, semAtalho: false, segundoPlano: false};
|
|
48
55
|
for (let i = 0; i < argv.length; i++) {
|
|
49
56
|
const v = argv[i];
|
|
50
57
|
if (v === "--codigo" || v === "-c") a.codigo = argv[++i] || "";
|
|
51
58
|
else if (v.startsWith("--codigo=")) a.codigo = v.slice(9);
|
|
52
59
|
else if (v === "--pasta") a.pasta = argv[++i] || "";
|
|
53
60
|
else if (v.startsWith("--pasta=")) a.pasta = v.slice(8);
|
|
61
|
+
else if (v === "--voicestudio") a.voicestudio = argv[++i] || "";
|
|
62
|
+
else if (v.startsWith("--voicestudio=")) a.voicestudio = v.slice(14);
|
|
54
63
|
else if (v === "--sem-atalho") a.semAtalho = true;
|
|
64
|
+
else if (v === "--segundo-plano") a.segundoPlano = true;
|
|
55
65
|
else if (v === "--versao" || v === "-v" || v === "--version") a.comando = "versao";
|
|
56
66
|
else if (v === "--ajuda" || v === "-h" || v === "--help") a.comando = "ajuda";
|
|
57
67
|
else if (!v.startsWith("-") && !a.comando) a.comando = v;
|
|
@@ -200,7 +210,7 @@ async function principal() {
|
|
|
200
210
|
const args = lerArgs(process.argv.slice(2));
|
|
201
211
|
if (args.comando === "versao") return dizer(VERSAO);
|
|
202
212
|
if (args.comando === "ajuda") return ajuda();
|
|
203
|
-
if (args.comando && !["desparear", "atalho"].includes(args.comando)) throw new Error(`não conheço o comando "${args.comando}". Veja: npx vibeeditor-render --ajuda`);
|
|
213
|
+
if (args.comando && !["desparear", "atalho", "sem-inicio-automatico"].includes(args.comando)) throw new Error(`não conheço o comando "${args.comando}". Veja: npx vibeeditor-render --ajuda`);
|
|
204
214
|
|
|
205
215
|
const [maior] = process.versions.node.split(".").map(Number);
|
|
206
216
|
if (maior < 20) throw new Error(`o Vibe Render precisa do Node.js 20 ou mais novo (este computador tem ${process.version}). Atualize em https://nodejs.org`);
|
|
@@ -224,11 +234,25 @@ async function principal() {
|
|
|
224
234
|
}
|
|
225
235
|
if (args.comando === "atalho") {
|
|
226
236
|
const a = criarAtalho();
|
|
227
|
-
|
|
237
|
+
dizer(a ? `${verde("✓")} Atalho criado: ${a}` : "Atalho na área de trabalho só no Windows e no Mac.");
|
|
238
|
+
config = {...config, semInicio: false};
|
|
239
|
+
const r = sistema.integrar({dirDados: dados});
|
|
240
|
+
for (const f of r.feito) dizer(`${verde("✓")} ${f}`);
|
|
241
|
+
for (const p of r.pulado) dizer(`${ambar("!")} Pulei: ${p}`);
|
|
242
|
+
config = {...config, atalhoCriado: true, integrado: VERSAO_INTEGRACAO}; gravarConfig();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (args.comando === "sem-inicio-automatico") {
|
|
246
|
+
const tinha = sistema.desligarInicio();
|
|
247
|
+
config = {...config, semInicio: true, integrado: VERSAO_INTEGRACAO}; gravarConfig();
|
|
248
|
+
return dizer(tinha ? `${verde("✓")} O Vibe Render não abre mais junto com o computador. Para voltar: npx vibeeditor-render@latest atalho` : "O início automático já estava desligado.");
|
|
228
249
|
}
|
|
229
250
|
if (args.pasta) {
|
|
230
251
|
config = {...config, dirSaida: path.resolve(args.pasta)}; gravarConfig();
|
|
231
252
|
}
|
|
253
|
+
if (args.voicestudio !== null && !require("../core/voz").urlValida(args.voicestudio)) {
|
|
254
|
+
throw new Error(`--voicestudio precisa ser o endereço do VoiceStudio neste computador, como http://127.0.0.1:3900 (veio "${args.voicestudio}")`);
|
|
255
|
+
}
|
|
232
256
|
|
|
233
257
|
dizer(`\n${negrito(azul("▶ Vibe Render"))} ${cinza(`v${VERSAO}`)} ${cinza("· exporta os vídeos do Editor IA neste computador")}\n`);
|
|
234
258
|
versaoNova().then((v) => v && dizer(`${ambar("!")} Saiu a versão ${v}. Feche e abra de novo com ${azul("npx vibeeditor-render@latest")}.`));
|
|
@@ -236,7 +260,7 @@ async function principal() {
|
|
|
236
260
|
const {PORTAS} = require("../core/config");
|
|
237
261
|
const outra = await jaAberto(PORTAS);
|
|
238
262
|
if (outra) {
|
|
239
|
-
dizer(`${ambar("!")} O Vibe Render já está aberto em outra janela (porta ${outra}). Use aquela.`);
|
|
263
|
+
if (!args.segundoPlano) dizer(`${ambar("!")} O Vibe Render já está aberto em outra janela (porta ${outra}). Use aquela.`);
|
|
240
264
|
return;
|
|
241
265
|
}
|
|
242
266
|
|
|
@@ -252,9 +276,18 @@ async function principal() {
|
|
|
252
276
|
const nucleo = await criarNucleo({
|
|
253
277
|
versao: VERSAO, dirDados: dados, dirSaida: () => config.dirSaida, hostDir, navegador: null, binarios: null,
|
|
254
278
|
armazenamento, nomeAparelho: os.hostname(), licenseKey: config.licenseKey || process.env.REMOTION_LICENSE_KEY || null,
|
|
255
|
-
mostrarArquivo, log,
|
|
279
|
+
mostrarArquivo, log, voiceStudio: args.voicestudio || null,
|
|
256
280
|
});
|
|
257
281
|
|
|
282
|
+
// link vibe-render:// (botão "Abrir" do site) e início junto com o computador: uma vez por versão da
|
|
283
|
+
// integração, também para quem já abria pelo atalho antigo (que passa --sem-atalho)
|
|
284
|
+
if (config.integrado !== VERSAO_INTEGRACAO) {
|
|
285
|
+
const r = sistema.integrar({dirDados: dados, inicio: !config.semInicio});
|
|
286
|
+
if (r.feito.length) dizer(`${verde("✓")} Daqui para a frente o Vibe Render abre sozinho junto com o computador, e o site consegue abri-lo pelo botão. ${cinza("(para desligar: npx vibeeditor-render@latest sem-inicio-automatico)")}`);
|
|
287
|
+
for (const p of r.pulado) log("integração pulada:", p);
|
|
288
|
+
config = {...config, integrado: VERSAO_INTEGRACAO}; gravarConfig();
|
|
289
|
+
}
|
|
290
|
+
|
|
258
291
|
// atalho na área de trabalho, uma vez (quem abriu pelo atalho passa --sem-atalho)
|
|
259
292
|
if (!args.semAtalho && !config.atalhoCriado) {
|
|
260
293
|
try {
|
|
@@ -266,6 +299,15 @@ async function principal() {
|
|
|
266
299
|
|
|
267
300
|
dizer(`${verde("✓")} Pronto e escutando só neste computador ${cinza(`(127.0.0.1:${nucleo.porta})`)}.`);
|
|
268
301
|
dizer(` Vídeos vão para: ${config.dirSaida}`);
|
|
302
|
+
if (args.voicestudio) dizer(` VoiceStudio: ${require("../core/voz").urlValida(args.voicestudio)}`);
|
|
303
|
+
|
|
304
|
+
// ffmpeg completo do tratamento de áudio: baixa já em segundo plano (a 1ª exportação não espera por ele)
|
|
305
|
+
const ffmpeg = require("../core/ffmpeg");
|
|
306
|
+
if (!ffmpeg.acharFfmpegCompleto({dirDados: dados})) {
|
|
307
|
+
ffmpeg.garantirFfmpeg({dirDados: dados})
|
|
308
|
+
.then((f) => f.origem === "baixado" && dizer(`${verde("✓")} Tratamento de áudio pronto ${cinza(`(ffmpeg ${ffmpeg.VERSAO})`)}.`))
|
|
309
|
+
.catch((e) => dizer(`${ambar("!")} Não consegui baixar o ffmpeg do tratamento de áudio (${e.message}). Tento de novo na exportação.`));
|
|
310
|
+
}
|
|
269
311
|
|
|
270
312
|
// pareamento
|
|
271
313
|
let rl = null;
|
package/core/audio.js
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
// Tratamento do áudio da fala com ffmpeg, a mesma cadeia do editor local
|
|
2
|
-
// passa-alta → redução de ruído (afftdn, opcional) → loudnorm em 2 passadas (−14 LUFS, linear) →
|
|
3
|
-
// limitador de pico com 4× de reamostragem (≤ −1,5 dBTP).
|
|
1
|
+
// Tratamento do áudio da fala com ffmpeg, a mesma cadeia do editor local.
|
|
4
2
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// tratarFala — a cadeia do hv/montar.py para a linha do tempo editada: corta os trechos mantidos (fade de 8 ms
|
|
4
|
+
// nas emendas), iguala o nível de voz por trecho, passa-alta, (afftdn), limitador de folga e loudnorm
|
|
5
|
+
// em 2 passadas (linear; se o ganho estourasse o pico: ganho fixo + limitador). WAV 48 kHz.
|
|
6
|
+
// limitarPico — o último passo de todo render (hv/render.limitar_pico): pico real ≤ máximo no ARQUIVO FINAL.
|
|
7
|
+
// tratarAudio — cadeia genérica (arquivo inteiro, sem cortes), para áudio avulso.
|
|
8
|
+
//
|
|
9
|
+
// O ffmpeg que vem com o @remotion/renderer é mínimo (sem highpass/afftdn/alimiter/ebur128): o ffmpeg completo vem
|
|
10
|
+
// de core/ffmpeg.js (VIBE_FFMPEG, baixado na pasta de dados, ou o do PATH em desenvolvimento).
|
|
8
11
|
const {spawn} = require("node:child_process");
|
|
9
12
|
const fs = require("node:fs");
|
|
13
|
+
const os = require("node:os");
|
|
10
14
|
const path = require("node:path");
|
|
15
|
+
const crypto = require("node:crypto");
|
|
16
|
+
const {acharFfmpegCompleto, versaoPrincipal} = require("./ffmpeg");
|
|
11
17
|
|
|
12
18
|
const ALVO = {lufs: -14, tp: -1.5, lra: 11};
|
|
19
|
+
const FPS = 30;
|
|
13
20
|
|
|
14
|
-
|
|
21
|
+
/** ffmpeg sem baixar nada: VIBE_FFMPEG, `recursos/ffmpeg-audio`, o baixado na pasta de dados, ou o do PATH */
|
|
22
|
+
function acharFfmpeg({recursos, dirDados, env = process.env} = {}) {
|
|
15
23
|
const exe = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
|
|
16
|
-
const
|
|
17
|
-
|
|
24
|
+
const achado = acharFfmpegCompleto({dirDados, env});
|
|
25
|
+
if (achado) return achado.caminho;
|
|
26
|
+
const r = recursos && path.join(recursos, "ffmpeg-audio", exe);
|
|
27
|
+
if (r && fs.existsSync(r)) return r;
|
|
18
28
|
return exe; // PATH (desenvolvimento)
|
|
19
29
|
}
|
|
20
30
|
|
|
@@ -48,11 +58,11 @@ async function medir(ffmpeg, entrada, opc) {
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
/**
|
|
51
|
-
* Trata o áudio de `entrada` (vídeo ou áudio) e grava WAV 48 kHz em `saida`.
|
|
52
|
-
* opc: {ruido: bool (afftdn), recursos, sinal: AbortSignal}
|
|
61
|
+
* Trata o áudio de `entrada` (vídeo ou áudio) inteiro e grava WAV 48 kHz em `saida`.
|
|
62
|
+
* opc: {ruido: bool (afftdn), ffmpeg, recursos, dirDados, sinal: AbortSignal}
|
|
53
63
|
*/
|
|
54
64
|
async function tratarAudio(entrada, saida, opc = {}) {
|
|
55
|
-
const ffmpeg = acharFfmpeg(opc);
|
|
65
|
+
const ffmpeg = opc.ffmpeg || acharFfmpeg(opc);
|
|
56
66
|
const m = await medir(ffmpeg, entrada, opc);
|
|
57
67
|
const loud = `loudnorm=I=${ALVO.lufs}:TP=${ALVO.tp}:LRA=${ALVO.lra}:measured_I=${m.input_i}:measured_TP=${m.input_tp}:measured_LRA=${m.input_lra}:measured_thresh=${m.input_thresh}:offset=${m.target_offset}:linear=true`;
|
|
58
68
|
const limite = Math.pow(10, ALVO.tp / 20).toFixed(4); // −1,5 dB → 0,8414
|
|
@@ -61,13 +71,125 @@ async function tratarAudio(entrada, saida, opc = {}) {
|
|
|
61
71
|
return {medicaoAntes: m, ffmpeg};
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
// ---------------------------------------------------------------- fala da linha do tempo editada (montar.py)
|
|
75
|
+
|
|
76
|
+
// voz gerada (VoiceStudio): chiado de 1,5–8 kHz que engrossa ao longo da geração; afftdn com rastreio do ruído
|
|
77
|
+
const REDUCAO_VOZ_GERADA = "afftdn=nr=40:nf=-70:tn=1:gs=8";
|
|
78
|
+
|
|
79
|
+
/** número como o Python escreve num f-string sem formato (3.0 → "3.0"; o ffmpeg lê igual a "3") */
|
|
80
|
+
const py = (v) => (Number.isInteger(v) ? `${v}.0` : String(v));
|
|
81
|
+
|
|
82
|
+
/** grafo de áudio do montar._grafo_audio: trechos [[a, b]] já deslocados pelo -ss da leitura */
|
|
83
|
+
function grafoFala(trechos, spec, final, ganhos) {
|
|
84
|
+
const n = trechos.length;
|
|
85
|
+
const partes = n > 1 ? [`[0:a]asplit=${n}` + trechos.map((_, i) => `[s${i}]`).join("")] : [];
|
|
86
|
+
trechos.forEach(([a, b], i) => {
|
|
87
|
+
const src = n > 1 ? `[s${i}]` : "[0:a]";
|
|
88
|
+
const d = b - a;
|
|
89
|
+
const fade = Math.min(0.008, d / 4);
|
|
90
|
+
// ganho do trecho antes do limitador e do loudnorm (eles agem na mistura já nivelada)
|
|
91
|
+
const ganho = ganhos && ganhos[i] ? `,volume=${py(ganhos[i])}dB` : "";
|
|
92
|
+
partes.push(`${src}atrim=${a.toFixed(4)}:${b.toFixed(4)},asetpts=PTS-STARTPTS,`
|
|
93
|
+
+ `afade=t=in:d=${fade.toFixed(3)},afade=t=out:st=${(d - fade).toFixed(4)}:d=${fade.toFixed(3)}${ganho}[a${i}]`);
|
|
94
|
+
});
|
|
95
|
+
const entradas = trechos.map((_, i) => `[a${i}]`).join("");
|
|
96
|
+
let cadeia = `highpass=f=${spec.passa_alta_hz}`;
|
|
97
|
+
if (spec.voz_gerada) cadeia += "," + REDUCAO_VOZ_GERADA;
|
|
98
|
+
else if (spec.reducao_ruido_db) cadeia += `,afftdn=nr=${spec.reducao_ruido_db}:nf=-40`;
|
|
99
|
+
// limitador antes do loudnorm: sem folga de pico o modo linear vira dinâmico
|
|
100
|
+
cadeia += ",alimiter=limit=0.79:level=disabled";
|
|
101
|
+
partes.push(`${entradas}concat=n=${n}:v=0:a=1,${cadeia}${final}`);
|
|
102
|
+
return partes.join(";");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** roda um grafo grande pelo arquivo (o Windows limita a linha de comando) */
|
|
106
|
+
async function rodarGrafo(ffmpeg, versao, leitura, grafo, resto, opc) {
|
|
107
|
+
const arq = path.join(opc.dirTmp || os.tmpdir(), `vibe-grafo-${crypto.randomBytes(5).toString("hex")}.txt`);
|
|
108
|
+
fs.writeFileSync(arq, grafo, "utf8");
|
|
109
|
+
try {
|
|
110
|
+
const flag = versao >= 7 ? ["-/filter_complex", arq] : ["-filter_complex_script", arq];
|
|
111
|
+
return await rodar(ffmpeg, ["-nostats", ...leitura, ...flag, ...resto], opc);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.rmSync(arq, {force: true});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A fala da linha do tempo editada, tratada como no montar.py. spec (vem do job, já validado):
|
|
119
|
+
* trechos [[ini, fim]] no tempo do original, ganhos_db [] (um por trecho), lufs_alvo, pico_dbtp (alvo do loudnorm,
|
|
120
|
+
* já com a folga), passa_alta_hz, reducao_ruido_db, voz_gerada.
|
|
121
|
+
* opc: {ffmpeg, versao, sinal, dirTmp}. Grava WAV 48 kHz 16 bits em `saida`; devolve a medição e o modo usado.
|
|
122
|
+
*/
|
|
123
|
+
async function tratarFala(entrada, saida, spec, opc = {}) {
|
|
124
|
+
const ffmpeg = opc.ffmpeg || acharFfmpeg(opc);
|
|
125
|
+
const versao = opc.versao || (await versaoPrincipal(ffmpeg));
|
|
126
|
+
const alvo = spec.lufs_alvo, tp = spec.pico_dbtp;
|
|
127
|
+
// lê só o pedaço do original que interessa (amostra não decodifica o vídeo todo)
|
|
128
|
+
const t0 = spec.trechos[0][0], tN = spec.trechos[spec.trechos.length - 1][1];
|
|
129
|
+
const ofs = Math.max(0, Math.trunc((t0 - 1.0) * FPS) / FPS);
|
|
130
|
+
const leitura = ["-ss", ofs.toFixed(4), "-to", (tN + 0.5).toFixed(4), "-i", entrada];
|
|
131
|
+
const trechos = spec.trechos.map(([a, b]) => [a - ofs, b - ofs]);
|
|
132
|
+
|
|
133
|
+
const g1 = grafoFala(trechos, spec, `,loudnorm=I=${py(alvo)}:TP=${py(tp)}:LRA=11:print_format=json[aout]`, spec.ganhos_db);
|
|
134
|
+
const log = await rodarGrafo(ffmpeg, versao, leitura, g1, ["-map", "[aout]", "-f", "null", "-"], opc);
|
|
135
|
+
const achados = log.match(/\{[^{}]*"input_i"[^{}]*\}/g);
|
|
136
|
+
if (!achados) throw new Error("loudnorm não devolveu a medição");
|
|
137
|
+
const m = JSON.parse(achados[achados.length - 1]);
|
|
138
|
+
|
|
139
|
+
// loudnorm só é linear se o ganho não estourar o pico; senão vira compressor dinâmico (puxa pausas e chiado
|
|
140
|
+
// para cima). Nesse caso: ganho fixo + limitador.
|
|
141
|
+
const ganho = alvo - Number(m.input_i);
|
|
142
|
+
const linear = Number(m.input_tp) + ganho <= tp;
|
|
143
|
+
const ln = linear
|
|
144
|
+
? `,loudnorm=I=${py(alvo)}:TP=${py(tp)}:LRA=11:measured_I=${m.input_i}:measured_TP=${m.input_tp}:measured_LRA=${m.input_lra}`
|
|
145
|
+
+ `:measured_thresh=${m.input_thresh}:offset=${m.target_offset}:linear=true,aresample=48000[aout]`
|
|
146
|
+
: `,volume=${ganho.toFixed(2)}dB,alimiter=limit=${Math.pow(10, tp / 20).toFixed(4)}:attack=1:release=60:level=disabled,aresample=48000[aout]`;
|
|
147
|
+
const g2 = grafoFala(trechos, spec, ln, spec.ganhos_db);
|
|
148
|
+
fs.mkdirSync(path.dirname(saida), {recursive: true});
|
|
149
|
+
await rodarGrafo(ffmpeg, versao, leitura, g2, ["-y", "-v", "error", "-map", "[aout]", "-c:a", "pcm_s16le", saida], opc);
|
|
150
|
+
return {loudnorm_medido: m, normalizacao: linear ? "loudnorm linear" : `ganho ${ganho >= 0 ? "+" : ""}${ganho.toFixed(1)} dB + limitador`, ffmpeg};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------- pico real no arquivo final (render.limitar_pico)
|
|
154
|
+
|
|
155
|
+
/** mede loudness integrada, LRA e true peak de um arquivo */
|
|
65
156
|
async function medirLoudness(arquivo, opc = {}) {
|
|
66
|
-
const log = await rodar(acharFfmpeg(opc), ["-i", arquivo, "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"], opc);
|
|
157
|
+
const log = await rodar(opc.ffmpeg || acharFfmpeg(opc), ["-nostats", "-i", arquivo, "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"], opc);
|
|
67
158
|
const resumo = log.slice(log.lastIndexOf("Summary:"));
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
159
|
+
const num = (re) => {
|
|
160
|
+
const v = (resumo.match(re) || [])[1];
|
|
161
|
+
return v === undefined ? NaN : v === "-inf" ? -99 : Number(v);
|
|
162
|
+
};
|
|
163
|
+
return {lufs: num(/I:\s+(-?[\d.]+|-inf) LUFS/), lra: num(/LRA:\s+(-?[\d.]+|-inf) LU/), tp: num(/Peak:\s+(-?[\d.]+|-inf) dBFS/)};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Último passo de todo render: o limitador da montagem age antes das compressões AAC, e cada compressão cria
|
|
168
|
+
* picos novos. Aqui o áudio passa por um limitador com 4× de superamostragem, é recodificado (AAC 256k) e medido
|
|
169
|
+
* de novo — se ainda passar, aperta o limite e repete. O vídeo é copiado. Troca `video` no lugar.
|
|
170
|
+
*/
|
|
171
|
+
async function limitarPico(video, picoMaxDbtp, opc = {}) {
|
|
172
|
+
const ffmpeg = opc.ffmpeg || acharFfmpeg(opc);
|
|
173
|
+
// a medição do arquivo final também vai para a conferência da amostra (LUFS/LRA/pico, como o conferir.py)
|
|
174
|
+
const m0 = await medirLoudness(video, {...opc, ffmpeg});
|
|
175
|
+
const antes = m0.tp;
|
|
176
|
+
if (antes <= picoMaxDbtp - 0.2) return {pico_dbtp: antes, limitado: false, lufs: m0.lufs, lra: m0.lra};
|
|
177
|
+
const tmp = video.replace(/(\.\w+)$/, "_pico$1");
|
|
178
|
+
let limiteDb = picoMaxDbtp - 0.5, m = m0;
|
|
179
|
+
try {
|
|
180
|
+
for (let i = 0; i < 4; i++) {
|
|
181
|
+
await rodar(ffmpeg, ["-y", "-v", "error", "-i", video, "-map", "0", "-c:v", "copy",
|
|
182
|
+
"-af", `aresample=192000,alimiter=limit=${Math.pow(10, limiteDb / 20).toFixed(4)}:attack=1:release=60:level=disabled,aresample=48000`,
|
|
183
|
+
"-c:a", "aac", "-b:a", "256k", "-movflags", "+faststart", tmp], opc);
|
|
184
|
+
m = await medirLoudness(tmp, {...opc, ffmpeg});
|
|
185
|
+
if (m.tp <= picoMaxDbtp) break;
|
|
186
|
+
limiteDb -= (m.tp - picoMaxDbtp) + 0.3;
|
|
187
|
+
}
|
|
188
|
+
fs.renameSync(tmp, video);
|
|
189
|
+
} finally {
|
|
190
|
+
fs.rmSync(tmp, {force: true});
|
|
191
|
+
}
|
|
192
|
+
return {pico_dbtp: m.tp, limitado: true, antes_dbtp: antes, lufs: m.lufs, lra: m.lra};
|
|
71
193
|
}
|
|
72
194
|
|
|
73
|
-
module.exports = {tratarAudio, medirLoudness, acharFfmpeg, ALVO};
|
|
195
|
+
module.exports = {tratarAudio, tratarFala, grafoFala, limitarPico, medirLoudness, acharFfmpeg, ALVO, REDUCAO_VOZ_GERADA};
|
package/core/cache.js
CHANGED
|
@@ -45,6 +45,26 @@ function criarCache(dir, {limiteBytes}) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** põe no cache um arquivo gerado aqui (ex.: o áudio tratado), endereçado pelo sha256 dele; devolve o sha */
|
|
49
|
+
async function adotar(arquivo) {
|
|
50
|
+
const hash = crypto.createHash("sha256");
|
|
51
|
+
await pipeline(fs.createReadStream(arquivo), new Transform({transform(c, _e, cb) {
|
|
52
|
+
hash.update(c); cb();
|
|
53
|
+
}}));
|
|
54
|
+
const sha = hash.digest("hex");
|
|
55
|
+
const destino = caminho(sha);
|
|
56
|
+
if (fs.existsSync(destino)) fs.rmSync(arquivo, {force: true});
|
|
57
|
+
else {
|
|
58
|
+
try {
|
|
59
|
+
fs.renameSync(arquivo, destino);
|
|
60
|
+
} catch {
|
|
61
|
+
fs.copyFileSync(arquivo, destino); fs.rmSync(arquivo, {force: true});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
tocar(sha);
|
|
65
|
+
return sha;
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
/** marca uso (o limpador tira primeiro o que está há mais tempo sem uso) */
|
|
49
69
|
function tocar(sha) {
|
|
50
70
|
const t = new Date();
|
|
@@ -68,7 +88,7 @@ function criarCache(dir, {limiteBytes}) {
|
|
|
68
88
|
return total;
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
return {dir, caminho, existe, guardar, tocar, limpar};
|
|
91
|
+
return {dir, caminho, existe, guardar, adotar, tocar, limpar};
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
module.exports = {criarCache};
|
package/core/config.js
CHANGED
|
@@ -25,6 +25,11 @@ const PRESETS = {
|
|
|
25
25
|
rascunho: {ladoCurto: 720, crf: 23, x264Preset: "veryfast", jpegQuality: 80},
|
|
26
26
|
// lado curto em 1080, qualidade de entrega
|
|
27
27
|
final: {ladoCurto: 1080, crf: 18, x264Preset: "medium", jpegQuality: 92},
|
|
28
|
+
// 2K e 4K: a composição continua no tamanho do layout (1080 de lado curto) e o Chromium renderiza com escala
|
|
29
|
+
// maior, como o --scale do editor local: texto, legenda e motions saem nítidos e o vídeo do apresentador vem
|
|
30
|
+
// do original (4K do iPhone fica 4K de verdade). Por isso estes dois podem AUMENTAR a escala.
|
|
31
|
+
"2k": {ladoCurto: 1440, crf: 18, x264Preset: "medium", jpegQuality: 92, aumenta: true},
|
|
32
|
+
"4k": {ladoCurto: 2160, crf: 18, x264Preset: "medium", jpegQuality: 92, aumenta: true},
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
const STATUS_INTERVALO = 10 * 60000; // reconferir o pareamento com o servidor
|