vibeeditor-render 0.3.1 → 0.3.3

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.
@@ -182,6 +182,35 @@ async function versaoNova() {
182
182
  return null;
183
183
  }
184
184
 
185
+ /**
186
+ * Atualização ao abrir: se saiu versão nova no npm, troca por ela na hora (npx com o número exato, então a cópia
187
+ * guardada do npx não segura a antiga) e esta fica só esperando a nova fechar. Não roda em desenvolvimento (com o
188
+ * código-fonte do host ao lado) nem duas vezes seguidas (VIBE_RENDER_ATUALIZADO), e nunca trava a abertura:
189
+ * sem internet ou com o npm fora, segue na versão atual.
190
+ */
191
+ async function atualizarSeTiver(argv) {
192
+ if (process.env.VIBE_RENDER_ATUALIZADO || process.env.VIBE_RENDER_SEM_ATUALIZAR) return false;
193
+ if (fs.existsSync(path.join(RAIZ, "host", "build.mjs"))) return false;
194
+ const nova = await versaoNova();
195
+ if (!nova) return false;
196
+ dizer(`${azul("›")} Saiu a versão ${negrito(nova)} (esta é a ${VERSAO}). Atualizando e abrindo a nova…`);
197
+ // no Windows o npx é um .cmd e precisa do shell: argumento com espaço (--pasta "C:\Meus Vídeos") vai entre aspas
198
+ const args = process.platform === "win32" ? argv.map((a) => (/[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)) : argv;
199
+ const filho = spawn("npx", ["-y", `${PACOTE.name}@${nova}`, ...args], {
200
+ stdio: "inherit", shell: process.platform === "win32", env: {...process.env, VIBE_RENDER_ATUALIZADO: "1"},
201
+ });
202
+ // o Ctrl+C chega nos dois: quem cuida é a versão nova; esta só repassa o código de saída
203
+ process.on("SIGINT", () => {});
204
+ await new Promise((ok) => {
205
+ filho.on("error", (e) => {
206
+ dizer(`${ambar("!")} Não consegui atualizar (${e.message}). Sigo na ${VERSAO}.`);
207
+ ok(false);
208
+ });
209
+ filho.on("exit", (code) => process.exit(code == null ? 0 : code));
210
+ });
211
+ return false;
212
+ }
213
+
185
214
  /** "1.10.0" é mais nova que "1.9.3"? (só números; pré-lançamentos não entram) */
186
215
  function maisNova(a, b) {
187
216
  const x = String(a).split(".").map(Number), y = String(b).split(".").map(Number);
@@ -255,7 +284,8 @@ async function principal() {
255
284
  }
256
285
 
257
286
  dizer(`\n${negrito(azul("▶ Vibe Render"))} ${cinza(`v${VERSAO}`)} ${cinza("· exporta os vídeos do Editor IA neste computador")}\n`);
258
- versaoNova().then((v) => v && dizer(`${ambar("!")} Saiu a versão ${v}. Feche e abra de novo com ${azul("npx vibeeditor-render@latest")}.`));
287
+ // versão nova no npm: abre direto nela (esta só espera); sem internet, segue nesta
288
+ await atualizarSeTiver(process.argv.slice(2));
259
289
 
260
290
  const {PORTAS} = require("../core/config");
261
291
  const outra = await jaAberto(PORTAS);
package/core/nucleo.js CHANGED
@@ -28,6 +28,7 @@ async function criarNucleo(opc) {
28
28
  servidor = criarServidor({versao: opc.versao, origens: origensPermitidas(env), cache, jobs, transcricoes, voz, pareamento, mostrarArquivo: opc.mostrarArquivo, log: opc.log});
29
29
  const porta = await servidor.iniciar(opc.portas);
30
30
  pareamento.conferir();
31
+ voz.aoIniciar(); // abre o VoiceStudio junto, se a pessoa pediu (Configurações → VoiceStudio)
31
32
  const relogio = setInterval(() => pareamento.conferir(), STATUS_INTERVALO);
32
33
  relogio.unref();
33
34
  return {
package/core/servidor.js CHANGED
@@ -116,7 +116,7 @@ function criarServidor(opc) {
116
116
  const p = opc.pareamento.visao();
117
117
  return json(200, {app: "vibe-render", versao: opc.versao, pareado: p.pareado, dispositivoId: p.dispositivoId, email: p.email,
118
118
  plataforma: process.platform, porta, acessoPlataforma: p.plataforma,
119
- recursos: [...(opc.transcricoes ? ["transcricao"] : []), ...(opc.voz ? ["voz"] : [])]}, cors);
119
+ recursos: [...(opc.transcricoes ? ["transcricao"] : []), ...(opc.voz ? ["voz", "voz-abrir"] : [])]}, cors);
120
120
  }
121
121
 
122
122
  // daqui para baixo: sessão válida deste aparelho
package/core/voz.js CHANGED
@@ -8,10 +8,14 @@
8
8
  // POST /v1/voz/perfis multipart → POST /profiles (clonar: name, kind, ref_audio, ref_text, language)
9
9
  // POST /v1/voz/perfis/<id>/excluir → DELETE /profiles/<id> (a voz que já não existe conta como excluída)
10
10
  // POST /v1/voz/gerar multipart → POST /generate (text, profile_id ou ref_audio… → WAV)
11
+ // POST /v1/voz/abrir abre o app do VoiceStudio instalado neste computador (o site espera ele subir)
12
+ // POST /v1/voz/preferencias {abrirJunto} abre o VoiceStudio junto com o Vibe Render (que sobe com o computador)
11
13
  // Nada de IA aqui: gerar voz é determinístico. O formato das respostas do VoiceStudio muda entre versões, então
12
14
  // o status vai cru e quem interpreta é o site.
13
15
  const fs = require("node:fs");
16
+ const os = require("node:os");
14
17
  const path = require("node:path");
18
+ const {spawn} = require("node:child_process");
15
19
 
16
20
  const URL_PADRAO = "http://127.0.0.1:3900";
17
21
  const HOSTS_LOCAIS = new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -35,6 +39,43 @@ function urlValida(bruta) {
35
39
 
36
40
  const erro = (status, mensagem, extra = {}) => Object.assign(new Error(mensagem), {status, ...extra});
37
41
 
42
+ /** onde o instalador do VoiceStudio costuma pôr o app (Electron), em cada sistema */
43
+ function lugaresDoApp(plataforma = process.platform, env = process.env, casa = os.homedir()) {
44
+ if (plataforma === "win32") {
45
+ const local = env.LOCALAPPDATA || path.join(casa, "AppData", "Local");
46
+ return [path.join(local, "Programs", "VoiceStudio", "VoiceStudio.exe"),
47
+ ...[env.ProgramFiles, env["ProgramFiles(x86)"]].filter(Boolean).map((p) => path.join(p, "VoiceStudio", "VoiceStudio.exe"))];
48
+ }
49
+ if (plataforma === "darwin") return ["/Applications/VoiceStudio.app", path.join(casa, "Applications", "VoiceStudio.app")];
50
+ const appimages = [path.join(casa, "Applications"), path.join(casa, "Downloads")].flatMap((d) => {
51
+ try {
52
+ return fs.readdirSync(d).filter((f) => /^voicestudio.*\.appimage$/i.test(f)).map((f) => path.join(d, f));
53
+ } catch {
54
+ return [];
55
+ }
56
+ });
57
+ return [...appimages, "/opt/VoiceStudio/voicestudio", "/usr/bin/voicestudio", "/usr/local/bin/voicestudio"];
58
+ }
59
+
60
+ /** só abre um arquivo que existe e se chama VoiceStudio: o site manda o pedido, nunca o caminho de outro programa */
61
+ function appValido(caminho) {
62
+ if (!caminho || !path.isAbsolute(caminho) || !/voicestudio/i.test(path.basename(caminho))) return false;
63
+ try {
64
+ return fs.existsSync(caminho);
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /** abre o app sem prender o Vibe Render a ele (fecha um, o outro continua) */
71
+ function abrirApp(caminho, plataforma = process.platform) {
72
+ const filho = plataforma === "darwin"
73
+ ? spawn("open", ["-a", caminho], {detached: true, stdio: "ignore"})
74
+ : spawn(caminho, [], {detached: true, stdio: "ignore", windowsHide: false, cwd: path.dirname(caminho)});
75
+ filho.on("error", () => {});
76
+ filho.unref();
77
+ }
78
+
38
79
  /** lê o corpo inteiro até `max` bytes (413 acima disso) */
39
80
  function lerCorpo(req, max) {
40
81
  return new Promise((ok, falha) => {
@@ -76,13 +117,20 @@ function criarVoz(opc) {
76
117
  const log = opc.log || (() => {});
77
118
  const arq = path.join(opc.dirDados, ARQUIVO);
78
119
 
79
- const salva = () => {
120
+ // voicestudio.json: {url, abrirJunto, app} — o endereço, se abre junto com o Vibe Render e onde está o app
121
+ const lerArq = () => {
80
122
  try {
81
- return urlValida(JSON.parse(fs.readFileSync(arq, "utf8")).url);
123
+ const d = JSON.parse(fs.readFileSync(arq, "utf8"));
124
+ return d && typeof d === "object" ? d : {};
82
125
  } catch {
83
- return null;
126
+ return {};
84
127
  }
85
128
  };
129
+ const gravarArq = (patch) => {
130
+ fs.mkdirSync(opc.dirDados, {recursive: true});
131
+ fs.writeFileSync(arq, JSON.stringify({...lerArq(), ...patch}, null, 2));
132
+ };
133
+ const salva = () => urlValida(lerArq().url);
86
134
  /** a variável de ambiente manda (fixo); senão o endereço guardado; senão o padrão do VoiceStudio */
87
135
  function config() {
88
136
  const daEnv = urlValida(env.VIBE_RENDER_VOICESTUDIO);
@@ -93,12 +141,40 @@ function criarVoz(opc) {
93
141
  function definir(bruta) {
94
142
  const url = urlValida(bruta);
95
143
  if (!url) throw erro(400, "Endereço inválido: use o do VoiceStudio neste computador, como http://127.0.0.1:3900");
96
- fs.mkdirSync(opc.dirDados, {recursive: true});
97
- fs.writeFileSync(arq, JSON.stringify({url}, null, 2));
144
+ gravarArq({url});
98
145
  return config();
99
146
  }
100
147
  if (opc.urlInicial) definir(opc.urlInicial);
101
148
 
149
+ /** o app instalado: o caminho guardado (se ainda existe) ou o primeiro lugar padrão onde ele está */
150
+ function acharApp() {
151
+ const guardado = lerArq().app;
152
+ if (appValido(guardado)) return guardado;
153
+ return (opc.lugares || lugaresDoApp(process.platform, env)).find(appValido) || null;
154
+ }
155
+ const preferencias = () => ({abrirJunto: lerArq().abrirJunto === true, app: acharApp()});
156
+
157
+ /** abre o VoiceStudio se estiver fechado; o site espera ele subir conferindo o status */
158
+ async function abrir() {
159
+ const st = await status();
160
+ if (st.online) return {jaAberto: true, app: acharApp()};
161
+ const app = acharApp();
162
+ if (!app) throw erro(404, "Não encontrei o VoiceStudio instalado neste computador. Instale uma vez (github.com/debpalash/VoiceStudio) e tente de novo.", {semApp: true});
163
+ (opc.abrirApp || abrirApp)(app);
164
+ gravarArq({app});
165
+ log("voz", `abrindo o VoiceStudio: ${app}`);
166
+ return {abrindo: true, app};
167
+ }
168
+
169
+ /** chamado quando o Vibe Render sobe: abre o VoiceStudio junto, se a pessoa pediu */
170
+ async function aoIniciar() {
171
+ if (!preferencias().abrirJunto) return null;
172
+ return abrir().catch((e) => {
173
+ log("voz", `não abri o VoiceStudio junto: ${e.message}`);
174
+ return null;
175
+ });
176
+ }
177
+
102
178
  /** um pedido ao VoiceStudio; erro com status 503 (fechado), 504 (demorou) ou o status dele */
103
179
  async function chamar(caminho, {metodo = "GET", corpo, tipo, timeout = 8000, sinal} = {}) {
104
180
  const {url} = config();
@@ -177,7 +253,19 @@ function criarVoz(opc) {
177
253
  async function rota(req, res, resto, {json, cors}) {
178
254
  const [a, b, c] = resto;
179
255
  try {
180
- if (a === "config" && !b && req.method === "GET") return json(200, config(), cors), true;
256
+ if (a === "config" && !b && req.method === "GET") return json(200, {...config(), ...preferencias()}, cors), true;
257
+ if (a === "abrir" && !b && req.method === "POST") return json(200, await abrir(), cors), true;
258
+ if (a === "preferencias" && !b && req.method === "POST") {
259
+ let d;
260
+ try {
261
+ d = JSON.parse((await lerCorpo(req, 4096)).toString("utf8") || "{}");
262
+ } catch (e) {
263
+ throw e.status ? e : erro(400, "JSON inválido");
264
+ }
265
+ if (typeof (d && d.abrirJunto) !== "boolean") throw erro(400, "abrirJunto precisa ser true ou false");
266
+ gravarArq({abrirJunto: d.abrirJunto});
267
+ return json(200, preferencias(), cors), true;
268
+ }
181
269
  if (a === "config" && !b && req.method === "POST") {
182
270
  let d;
183
271
  try {
@@ -215,13 +303,13 @@ function criarVoz(opc) {
215
303
  if (e.status === 499) return res.destroy(), true;
216
304
  // 401/403 do VoiceStudio não podem parecer sessão inválida do Vibe Render (o site renovaria a sessão)
217
305
  const st = e.status === 401 || e.status === 403 ? 502 : e.status || 500;
218
- json(st, {erro: e.status ? e.message : "erro interno", ...(e.offline ? {offline: true} : {}), ...(e.doVoiceStudio ? {voiceStudio: e.status} : {})}, cors);
306
+ json(st, {erro: e.status ? e.message : "erro interno", ...(e.offline ? {offline: true} : {}), ...(e.semApp ? {semApp: true} : {}), ...(e.doVoiceStudio ? {voiceStudio: e.status} : {})}, cors);
219
307
  if (!e.status) log("erro", e.message);
220
308
  return true;
221
309
  }
222
310
  }
223
311
 
224
- return {config, definir, status, chamar, rota};
312
+ return {config, definir, status, chamar, rota, abrir, aoIniciar, preferencias, acharApp};
225
313
  }
226
314
 
227
- module.exports = {criarVoz, urlValida, URL_PADRAO, LIMITE_CORPO};
315
+ module.exports = {criarVoz, urlValida, lugaresDoApp, appValido, URL_PADRAO, LIMITE_CORPO};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeeditor-render",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Vibe Render: exporta os vídeos do Editor IA (vibeeditor.com.br) no seu computador, com Remotion + Chrome Headless Shell.",
5
5
  "keywords": [
6
6
  "vibeeditor",