@praeko/studio 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -9
- package/LICENSES.md +28 -30
- package/README.md +23 -22
- package/dist/praeko-studio.mjs +4751 -3621
- package/dist/voice-account.mjs +295 -0
- package/dist/voice-balance.mjs +152 -0
- package/dist/{voz-config.mjs → voice-config.mjs} +38 -35
- package/dist/voice-voices.mjs +220 -0
- package/docs/episode-yaml.md +57 -0
- package/docs/hyperframes.md +49 -49
- package/docs/long-yaml.md +78 -0
- package/docs/publish.md +22 -0
- package/docs/voice.md +22 -0
- package/package.json +8 -7
- package/dist/voz-conta.mjs +0 -319
- package/dist/voz-saldo.mjs +0 -178
- package/dist/voz-vozes.mjs +0 -247
- package/docs/episodio-yaml.md +0 -57
- package/docs/longo-yaml.md +0 -78
- package/docs/publicar.md +0 -22
- package/docs/voz.md +0 -22
- /package/{modelos → templates}/avatar.png +0 -0
- /package/{modelos → templates}/sfx/ding.wav +0 -0
- /package/{modelos → templates}/sfx/pop.wav +0 -0
- /package/{modelos → templates}/sfx/thump.wav +0 -0
- /package/{modelos → templates}/sfx/whoosh.wav +0 -0
package/dist/voz-conta.mjs
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
3
|
-
|
|
4
|
-
// ../voz/bin/conta.mjs
|
|
5
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
|
-
import { dirname as dirname2 } from "node:path";
|
|
7
|
-
import { createInterface } from "node:readline";
|
|
8
|
-
|
|
9
|
-
// ../voz/src/index.mjs
|
|
10
|
-
import { ElevenLabsClient as ElevenLabsClient2 } from "@elevenlabs/elevenlabs-js";
|
|
11
|
-
|
|
12
|
-
// ../voz/src/config.mjs
|
|
13
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { execFileSync } from "node:child_process";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
|
-
import { join, dirname, resolve } from "node:path";
|
|
17
|
-
|
|
18
|
-
// ../voz/src/padrao.mjs
|
|
19
|
-
var VOZES_PADRAO = {
|
|
20
|
-
"contas": {
|
|
21
|
-
"elevenlabs": [
|
|
22
|
-
"principal"
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
"vozes": {
|
|
26
|
-
"rascunho-mudo": {
|
|
27
|
-
"provedor": "estimado",
|
|
28
|
-
"palavrasPorMin": 190
|
|
29
|
-
},
|
|
30
|
-
"minha-voz": {
|
|
31
|
-
"provedor": "elevenlabs",
|
|
32
|
-
"modelo": "eleven_v4",
|
|
33
|
-
"ids": {},
|
|
34
|
-
"alinhador": "scribe"
|
|
35
|
-
},
|
|
36
|
-
"voz-local": {
|
|
37
|
-
"provedor": "voicestudio",
|
|
38
|
-
"modelo": "",
|
|
39
|
-
"id": "",
|
|
40
|
-
"idioma": "pt-BR",
|
|
41
|
-
"ajustes": {},
|
|
42
|
-
"alinhador": "voicestudio"
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
"estrategia": {
|
|
46
|
-
"elevenlabs": "failover-saldo"
|
|
47
|
-
},
|
|
48
|
-
"qualidade": {
|
|
49
|
-
"rascunho": "rascunho-mudo",
|
|
50
|
-
"local": "voz-local",
|
|
51
|
-
"final": "minha-voz"
|
|
52
|
-
},
|
|
53
|
-
"voicestudio": {
|
|
54
|
-
"base": "http://127.0.0.1:3900"
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// ../voz/src/config.mjs
|
|
59
|
-
var pastaConfigUsuario = () => process.env.PRAEKO_STUDIO_CONFIG || join(homedir(), ".config", "praeko", "studio");
|
|
60
|
-
var arquivoVozesUsuario = () => join(pastaConfigUsuario(), "vozes.json");
|
|
61
|
-
var arquivoSegredos = () => process.env.PRAEKO_SEGREDOS || join(homedir(), ".config", "praeko", "segredos.json");
|
|
62
|
-
var SERVICO_CHAVEIRO = "praeko";
|
|
63
|
-
var chaveiroAtivo = () => process.platform === "darwin" && !process.env.PRAEKO_SEM_CHAVEIRO;
|
|
64
|
-
function raizDoProjeto(de = process.cwd()) {
|
|
65
|
-
let d = resolve(de);
|
|
66
|
-
for (; ; ) {
|
|
67
|
-
if (existsSync(join(d, "marca.json"))) return d;
|
|
68
|
-
const p = dirname(d);
|
|
69
|
-
if (p === d) return null;
|
|
70
|
-
d = p;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
var arquivoVozesProjeto = (de) => {
|
|
74
|
-
const r = raizDoProjeto(de);
|
|
75
|
-
return r ? join(r, "vozes.json") : null;
|
|
76
|
-
};
|
|
77
|
-
var arquivoVozesEscrita = (de) => arquivoVozesProjeto(de) || arquivoVozesUsuario();
|
|
78
|
-
var lerJson = (a2) => {
|
|
79
|
-
try {
|
|
80
|
-
return JSON.parse(readFileSync(a2, "utf8"));
|
|
81
|
-
} catch (e) {
|
|
82
|
-
throw new Error(`${a2}: JSON inv\xE1lido (${e.message})`);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
var mesclar = (base, extra) => ({
|
|
86
|
-
...base,
|
|
87
|
-
...extra,
|
|
88
|
-
contas: { ...base.contas, ...extra.contas },
|
|
89
|
-
vozes: { ...base.vozes, ...extra.vozes },
|
|
90
|
-
estrategia: { ...base.estrategia, ...extra.estrategia },
|
|
91
|
-
qualidade: { ...base.qualidade, ...extra.qualidade },
|
|
92
|
-
voicestudio: { ...base.voicestudio, ...extra.voicestudio }
|
|
93
|
-
});
|
|
94
|
-
function carregarConfig(arquivo) {
|
|
95
|
-
if (arquivo) return mesclar({ contas: {}, vozes: {}, estrategia: {} }, lerJson(arquivo));
|
|
96
|
-
let c = structuredClone(VOZES_PADRAO);
|
|
97
|
-
for (const a2 of [arquivoVozesUsuario(), arquivoVozesProjeto()]) if (a2 && existsSync(a2)) c = mesclar(c, lerJson(a2));
|
|
98
|
-
return c;
|
|
99
|
-
}
|
|
100
|
-
var envDe = (ref) => [`PRAEKO_STUDIO_KEY_${ref.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`, ...ref === "elevenlabs/principal" ? ["ELEVENLABS_API_KEY"] : [], ...ref === "praeko/principal" ? ["PRAEKO_KEY"] : []];
|
|
101
|
-
var canonica = (ref) => ref === "praeko/principal" ? "praeko/chave" : ref;
|
|
102
|
-
var lerSegredos = () => {
|
|
103
|
-
try {
|
|
104
|
-
return existsSync(arquivoSegredos()) ? JSON.parse(readFileSync(arquivoSegredos(), "utf8")) : {};
|
|
105
|
-
} catch {
|
|
106
|
-
return {};
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
function obterChave(ref) {
|
|
110
|
-
for (const n of envDe(ref)) if (process.env[n]) return process.env[n];
|
|
111
|
-
const refs = [.../* @__PURE__ */ new Set([canonica(ref), ref])];
|
|
112
|
-
if (chaveiroAtivo()) for (const r of refs) {
|
|
113
|
-
try {
|
|
114
|
-
const k = execFileSync("security", ["find-generic-password", "-s", SERVICO_CHAVEIRO, "-a", r, "-w"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
115
|
-
if (k) return k;
|
|
116
|
-
} catch {
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const seg = lerSegredos();
|
|
120
|
-
for (const r of refs) if (seg[r]) return seg[r];
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
function guardarChave(ref, chave) {
|
|
124
|
-
ref = canonica(ref);
|
|
125
|
-
if (chaveiroAtivo()) {
|
|
126
|
-
execFileSync("security", ["add-generic-password", "-U", "-s", SERVICO_CHAVEIRO, "-a", ref, "-w", chave], { stdio: ["ignore", "ignore", "pipe"] });
|
|
127
|
-
return "chaveiro";
|
|
128
|
-
}
|
|
129
|
-
const f = arquivoSegredos();
|
|
130
|
-
mkdirSync(dirname(f), { recursive: true });
|
|
131
|
-
const s = lerSegredos();
|
|
132
|
-
s[ref] = chave;
|
|
133
|
-
writeFileSync(f, JSON.stringify(s, null, 2) + "\n", { mode: 384 });
|
|
134
|
-
if (process.platform !== "win32") chmodSync(f, 384);
|
|
135
|
-
return "arquivo";
|
|
136
|
-
}
|
|
137
|
-
var mascarar = (k) => k ? `${k.slice(0, 3)}\u2026${k.slice(-2)}` : "(sem chave)";
|
|
138
|
-
|
|
139
|
-
// ../voz/src/provedores/elevenlabs.mjs
|
|
140
|
-
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
|
141
|
-
function criarElevenLabs({ chave }) {
|
|
142
|
-
const client = new ElevenLabsClient({ apiKey: chave });
|
|
143
|
-
return {
|
|
144
|
-
nome: "elevenlabs",
|
|
145
|
-
/** `{ texto, id, modelo, ajustes }` → Buffer de mp3 */
|
|
146
|
-
async gerar({ texto, id, modelo, ajustes = {} }) {
|
|
147
|
-
const audio = await client.textToSpeech.convert(id, {
|
|
148
|
-
text: texto,
|
|
149
|
-
modelId: modelo || "eleven_v4",
|
|
150
|
-
languageCode: "pt",
|
|
151
|
-
outputFormat: "mp3_44100_128",
|
|
152
|
-
voiceSettings: { stability: ajustes.stability ?? 0.5, similarityBoost: ajustes.similarity ?? 0.75, style: ajustes.style ?? 0, useSpeakerBoost: true, speed: ajustes.speed ?? 1 }
|
|
153
|
-
});
|
|
154
|
-
const chunks = [];
|
|
155
|
-
for await (const c of audio) chunks.push(c);
|
|
156
|
-
return Buffer.concat(chunks);
|
|
157
|
-
},
|
|
158
|
-
/** `{ usados, limite, restantes, plano, reinicia }` (caracteres) */
|
|
159
|
-
async saldo() {
|
|
160
|
-
const r = await fetch("https://api.elevenlabs.io/v1/user/subscription", { headers: { "xi-api-key": chave } });
|
|
161
|
-
if (!r.ok) {
|
|
162
|
-
const e = new Error(`saldo: HTTP ${r.status}`);
|
|
163
|
-
e.statusCode = r.status;
|
|
164
|
-
throw e;
|
|
165
|
-
}
|
|
166
|
-
const d = await r.json();
|
|
167
|
-
return { usados: d.character_count, limite: d.character_limit, restantes: d.character_limit - d.character_count, plano: d.tier, reinicia: d.next_character_count_reset_unix ? new Date(d.next_character_count_reset_unix * 1e3) : null };
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ../voz/src/roteador.mjs
|
|
173
|
-
async function saldos({ config = carregarConfig(), criar } = {}) {
|
|
174
|
-
const fabrica = criar || ((conta, chave) => criarElevenLabs({ chave }));
|
|
175
|
-
const out = [];
|
|
176
|
-
for (const conta of config.contas?.elevenlabs || []) {
|
|
177
|
-
const chave = obterChave(`elevenlabs/${conta}`);
|
|
178
|
-
if (!chave) {
|
|
179
|
-
out.push({ conta, erro: "sem chave" });
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
out.push({ conta, ...await fabrica(conta, chave).saldo() });
|
|
184
|
-
} catch (e) {
|
|
185
|
-
out.push({ conta, erro: e.message });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return out;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ../voz/src/alinhamento.mjs
|
|
192
|
-
var NUM = new Set("zero um uma dois duas tr\xEAs tres quatro cinco seis sete oito nove dez onze doze treze catorze quatorze quinze dezesseis dezessete dezoito dezenove vinte trinta quarenta cinquenta sessenta setenta oitenta noventa cem cento duzentos duzentas trezentos trezentas quatrocentos quatrocentas quinhentos quinhentas seiscentos seiscentas setecentos setecentas oitocentos oitocentas novecentos novecentas mil milh\xE3o milh\xF5es bilh\xE3o bilh\xF5es meio meia primeiro primeira".split(" "));
|
|
193
|
-
|
|
194
|
-
// ../voz/bin/conta.mjs
|
|
195
|
-
var a = process.argv.slice(2);
|
|
196
|
-
var cmd = a[0];
|
|
197
|
-
var nome = a[1];
|
|
198
|
-
var opt = (k) => {
|
|
199
|
-
const i = a.indexOf("--" + k);
|
|
200
|
-
return i >= 0 ? a[i + 1] : void 0;
|
|
201
|
-
};
|
|
202
|
-
var fmt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
203
|
-
async function lerChave() {
|
|
204
|
-
if (opt("de-env")) return process.env[opt("de-env")];
|
|
205
|
-
if (!process.stdin.isTTY) return await new Promise((ok) => {
|
|
206
|
-
let s = "";
|
|
207
|
-
process.stdin.on("data", (d) => s += d);
|
|
208
|
-
process.stdin.on("end", () => ok(s));
|
|
209
|
-
});
|
|
210
|
-
return await new Promise((ok) => {
|
|
211
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
212
|
-
rl._writeToOutput = (s) => {
|
|
213
|
-
if (s.includes("\n") || s.startsWith("Chave")) process.stdout.write(s);
|
|
214
|
-
};
|
|
215
|
-
rl.question(`Chave de elevenlabs/${nome}: `, (k) => {
|
|
216
|
-
rl.close();
|
|
217
|
-
console.log();
|
|
218
|
-
ok(k);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
async function api(caminho, chave) {
|
|
223
|
-
const r = await fetch(`https://api.elevenlabs.io${caminho}`, { headers: { "xi-api-key": chave }, signal: AbortSignal.timeout(2e4) });
|
|
224
|
-
if (!r.ok) {
|
|
225
|
-
let msg = "";
|
|
226
|
-
try {
|
|
227
|
-
const d = await r.json();
|
|
228
|
-
msg = d?.detail?.message || d?.detail?.status || JSON.stringify(d).slice(0, 160);
|
|
229
|
-
} catch {
|
|
230
|
-
}
|
|
231
|
-
const e = new Error(`${caminho}: HTTP ${r.status}${msg ? ` \u2014 ${msg}` : ""}`);
|
|
232
|
-
e.status = r.status;
|
|
233
|
-
throw e;
|
|
234
|
-
}
|
|
235
|
-
return r.json();
|
|
236
|
-
}
|
|
237
|
-
if (cmd === "listar") {
|
|
238
|
-
const cfg = carregarConfig();
|
|
239
|
-
const s = await saldos({ config: cfg });
|
|
240
|
-
for (const c of s) console.log(`${c.conta.padEnd(12)} ${c.erro || `${fmt(c.restantes)} restantes de ${fmt(c.limite)} (${c.plano}), reinicia ${c.reinicia?.toLocaleDateString("pt-BR")}`}`);
|
|
241
|
-
const usados = /* @__PURE__ */ new Map();
|
|
242
|
-
for (const c of s.filter((x) => !x.erro)) {
|
|
243
|
-
const k = `${c.usados}|${c.limite}|${c.reinicia?.getTime()}`;
|
|
244
|
-
usados.set(k, [...usados.get(k) || [], c.conta]);
|
|
245
|
-
}
|
|
246
|
-
for (const [, l] of usados) if (l.length > 1) console.log(`ATEN\xC7\xC3O: ${l.join(" e ")} mostram o MESMO saldo: provavelmente s\xE3o o mesmo workspace/plano (os cr\xE9ditos s\xE3o compartilhados).`);
|
|
247
|
-
} else if (cmd === "adicionar" && nome) {
|
|
248
|
-
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(nome)) {
|
|
249
|
-
console.error("nome: use letras, n\xFAmeros, - ou _");
|
|
250
|
-
process.exit(2);
|
|
251
|
-
}
|
|
252
|
-
const chave = (await lerChave() || "").trim();
|
|
253
|
-
if (!chave) {
|
|
254
|
-
console.error("chave vazia");
|
|
255
|
-
process.exit(1);
|
|
256
|
-
}
|
|
257
|
-
const apelido = opt("voz") || "minha-voz";
|
|
258
|
-
const cfg = carregarConfig();
|
|
259
|
-
if (!cfg.vozes[apelido]) {
|
|
260
|
-
console.error(`voz "${apelido}" n\xE3o existe em vozes.json`);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
}
|
|
263
|
-
let sub = null, vozes = null, erroSub = null, erroVoz = null;
|
|
264
|
-
try {
|
|
265
|
-
sub = await api("/v1/user/subscription", chave);
|
|
266
|
-
} catch (e) {
|
|
267
|
-
erroSub = e;
|
|
268
|
-
}
|
|
269
|
-
try {
|
|
270
|
-
vozes = (await api("/v1/voices", chave)).voices || [];
|
|
271
|
-
} catch (e) {
|
|
272
|
-
erroVoz = e;
|
|
273
|
-
}
|
|
274
|
-
if (erroSub && erroVoz) {
|
|
275
|
-
console.error(`A ElevenLabs recusou a chave (${mascarar(chave)}):
|
|
276
|
-
${erroSub.message}
|
|
277
|
-
${erroVoz.message}
|
|
278
|
-
Confira: (1) a chave foi copiada inteira, sem espa\xE7os; (2) foi criada no workspace certo; (3) se for restrita, tem as permiss\xF5es Voices (leitura), User (leitura), Text to Speech e Speech to Text.`);
|
|
279
|
-
process.exit(1);
|
|
280
|
-
}
|
|
281
|
-
if (sub) {
|
|
282
|
-
console.log(`conta ${nome}: plano ${sub.tier}, ${fmt(sub.character_limit - sub.character_count)} restantes de ${fmt(sub.character_limit)}, reinicia ${new Date(sub.next_character_count_reset_unix * 1e3).toLocaleDateString("pt-BR")} (chave ${mascarar(chave)})`);
|
|
283
|
-
for (const c of await saldos({ config: cfg })) if (!c.erro && c.usados === sub.character_count && c.limite === sub.character_limit) console.log(`ATEN\xC7\xC3O: o saldo \xE9 id\xEAntico ao da conta "${c.conta}": pode ser o MESMO workspace (cr\xE9ditos compartilhados), e o failover n\xE3o ajudaria.`);
|
|
284
|
-
} else {
|
|
285
|
-
console.log(`AVISO: a chave n\xE3o consegue ler o saldo (${erroSub.message}). Ela vale para gerar voz, mas o failover por saldo n\xE3o funciona nessa conta: ative a permiss\xE3o "User" (leitura) na chave para o Praeko Studio ver os cr\xE9ditos.`);
|
|
286
|
-
}
|
|
287
|
-
vozes = vozes || [];
|
|
288
|
-
const clones = vozes.filter((v) => ["cloned", "professional"].includes(v.category));
|
|
289
|
-
console.log(`${vozes.length} vozes nessa chave; clones: ${clones.length ? clones.map((v) => `${v.name} (${v.voice_id})`).join(", ") : "nenhum"}${erroVoz ? ` (lista de vozes indispon\xEDvel: ${erroVoz.message})` : ""}`);
|
|
290
|
-
let id = opt("voz-id");
|
|
291
|
-
if (!id) {
|
|
292
|
-
const cand = clones.filter((v) => new RegExp(apelido, "i").test(v.name));
|
|
293
|
-
if (cand.length === 1) id = cand[0].voice_id;
|
|
294
|
-
else {
|
|
295
|
-
console.error(cand.length ? `v\xE1rios clones parecem "${apelido}": repita com --voz-id <id>` : `nenhum clone chamado "${apelido}" nessa chave: crie o clone nesse workspace/conta ou repita com --voz-id <id>`);
|
|
296
|
-
process.exit(1);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
console.log(`voz "${apelido}" nessa conta: ${id}`);
|
|
300
|
-
if (opt("simular") !== void 0 || a.includes("--simular")) {
|
|
301
|
-
console.log("(simula\xE7\xE3o: nada foi guardado)");
|
|
302
|
-
process.exit(0);
|
|
303
|
-
}
|
|
304
|
-
const onde = guardarChave(`elevenlabs/${nome}`, chave);
|
|
305
|
-
const destino = arquivoVozesEscrita();
|
|
306
|
-
mkdirSync2(dirname2(destino), { recursive: true });
|
|
307
|
-
const raw = existsSync2(destino) ? JSON.parse(readFileSync2(destino, "utf8")) : {};
|
|
308
|
-
raw.contas ||= {};
|
|
309
|
-
raw.contas.elevenlabs ||= [];
|
|
310
|
-
if (!raw.contas.elevenlabs.includes(nome)) raw.contas.elevenlabs.push(nome);
|
|
311
|
-
raw.vozes ||= {};
|
|
312
|
-
raw.vozes[apelido] = { ...cfg.vozes[apelido], ...raw.vozes[apelido] || {} };
|
|
313
|
-
raw.vozes[apelido].ids = { ...raw.vozes[apelido].ids || {}, [nome]: id };
|
|
314
|
-
writeFileSync2(destino, JSON.stringify(raw, null, 2) + "\n");
|
|
315
|
-
console.log(`pronto: chave guardada (${onde}, elevenlabs/${nome}) e id em ${destino}. Ordem de failover: ${raw.contas.elevenlabs.join(" \u2192 ")}`);
|
|
316
|
-
} else {
|
|
317
|
-
console.error("uso: conta.mjs listar | conta.mjs adicionar <nome> [--de-env VAR] [--voz minha-voz] [--voz-id ID] [--simular]");
|
|
318
|
-
process.exit(2);
|
|
319
|
-
}
|
package/dist/voz-saldo.mjs
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
|
|
3
|
-
|
|
4
|
-
// ../voz/src/index.mjs
|
|
5
|
-
import { ElevenLabsClient as ElevenLabsClient2 } from "@elevenlabs/elevenlabs-js";
|
|
6
|
-
|
|
7
|
-
// ../voz/src/config.mjs
|
|
8
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { execFileSync } from "node:child_process";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { join, dirname, resolve } from "node:path";
|
|
12
|
-
|
|
13
|
-
// ../voz/src/padrao.mjs
|
|
14
|
-
var VOZES_PADRAO = {
|
|
15
|
-
"contas": {
|
|
16
|
-
"elevenlabs": [
|
|
17
|
-
"principal"
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
|
-
"vozes": {
|
|
21
|
-
"rascunho-mudo": {
|
|
22
|
-
"provedor": "estimado",
|
|
23
|
-
"palavrasPorMin": 190
|
|
24
|
-
},
|
|
25
|
-
"minha-voz": {
|
|
26
|
-
"provedor": "elevenlabs",
|
|
27
|
-
"modelo": "eleven_v4",
|
|
28
|
-
"ids": {},
|
|
29
|
-
"alinhador": "scribe"
|
|
30
|
-
},
|
|
31
|
-
"voz-local": {
|
|
32
|
-
"provedor": "voicestudio",
|
|
33
|
-
"modelo": "",
|
|
34
|
-
"id": "",
|
|
35
|
-
"idioma": "pt-BR",
|
|
36
|
-
"ajustes": {},
|
|
37
|
-
"alinhador": "voicestudio"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
"estrategia": {
|
|
41
|
-
"elevenlabs": "failover-saldo"
|
|
42
|
-
},
|
|
43
|
-
"qualidade": {
|
|
44
|
-
"rascunho": "rascunho-mudo",
|
|
45
|
-
"local": "voz-local",
|
|
46
|
-
"final": "minha-voz"
|
|
47
|
-
},
|
|
48
|
-
"voicestudio": {
|
|
49
|
-
"base": "http://127.0.0.1:3900"
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// ../voz/src/config.mjs
|
|
54
|
-
var pastaConfigUsuario = () => process.env.PRAEKO_STUDIO_CONFIG || join(homedir(), ".config", "praeko", "studio");
|
|
55
|
-
var arquivoVozesUsuario = () => join(pastaConfigUsuario(), "vozes.json");
|
|
56
|
-
var arquivoSegredos = () => process.env.PRAEKO_SEGREDOS || join(homedir(), ".config", "praeko", "segredos.json");
|
|
57
|
-
var SERVICO_CHAVEIRO = "praeko";
|
|
58
|
-
var chaveiroAtivo = () => process.platform === "darwin" && !process.env.PRAEKO_SEM_CHAVEIRO;
|
|
59
|
-
function raizDoProjeto(de = process.cwd()) {
|
|
60
|
-
let d = resolve(de);
|
|
61
|
-
for (; ; ) {
|
|
62
|
-
if (existsSync(join(d, "marca.json"))) return d;
|
|
63
|
-
const p = dirname(d);
|
|
64
|
-
if (p === d) return null;
|
|
65
|
-
d = p;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
var arquivoVozesProjeto = (de) => {
|
|
69
|
-
const r = raizDoProjeto(de);
|
|
70
|
-
return r ? join(r, "vozes.json") : null;
|
|
71
|
-
};
|
|
72
|
-
var lerJson = (a) => {
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(readFileSync(a, "utf8"));
|
|
75
|
-
} catch (e) {
|
|
76
|
-
throw new Error(`${a}: JSON inv\xE1lido (${e.message})`);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
var mesclar = (base, extra) => ({
|
|
80
|
-
...base,
|
|
81
|
-
...extra,
|
|
82
|
-
contas: { ...base.contas, ...extra.contas },
|
|
83
|
-
vozes: { ...base.vozes, ...extra.vozes },
|
|
84
|
-
estrategia: { ...base.estrategia, ...extra.estrategia },
|
|
85
|
-
qualidade: { ...base.qualidade, ...extra.qualidade },
|
|
86
|
-
voicestudio: { ...base.voicestudio, ...extra.voicestudio }
|
|
87
|
-
});
|
|
88
|
-
function carregarConfig(arquivo) {
|
|
89
|
-
if (arquivo) return mesclar({ contas: {}, vozes: {}, estrategia: {} }, lerJson(arquivo));
|
|
90
|
-
let c = structuredClone(VOZES_PADRAO);
|
|
91
|
-
for (const a of [arquivoVozesUsuario(), arquivoVozesProjeto()]) if (a && existsSync(a)) c = mesclar(c, lerJson(a));
|
|
92
|
-
return c;
|
|
93
|
-
}
|
|
94
|
-
var envDe = (ref) => [`PRAEKO_STUDIO_KEY_${ref.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`, ...ref === "elevenlabs/principal" ? ["ELEVENLABS_API_KEY"] : [], ...ref === "praeko/principal" ? ["PRAEKO_KEY"] : []];
|
|
95
|
-
var canonica = (ref) => ref === "praeko/principal" ? "praeko/chave" : ref;
|
|
96
|
-
var lerSegredos = () => {
|
|
97
|
-
try {
|
|
98
|
-
return existsSync(arquivoSegredos()) ? JSON.parse(readFileSync(arquivoSegredos(), "utf8")) : {};
|
|
99
|
-
} catch {
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
function obterChave(ref) {
|
|
104
|
-
for (const n of envDe(ref)) if (process.env[n]) return process.env[n];
|
|
105
|
-
const refs = [.../* @__PURE__ */ new Set([canonica(ref), ref])];
|
|
106
|
-
if (chaveiroAtivo()) for (const r of refs) {
|
|
107
|
-
try {
|
|
108
|
-
const k = execFileSync("security", ["find-generic-password", "-s", SERVICO_CHAVEIRO, "-a", r, "-w"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
109
|
-
if (k) return k;
|
|
110
|
-
} catch {
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const seg = lerSegredos();
|
|
114
|
-
for (const r of refs) if (seg[r]) return seg[r];
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ../voz/src/provedores/elevenlabs.mjs
|
|
119
|
-
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
|
|
120
|
-
function criarElevenLabs({ chave }) {
|
|
121
|
-
const client = new ElevenLabsClient({ apiKey: chave });
|
|
122
|
-
return {
|
|
123
|
-
nome: "elevenlabs",
|
|
124
|
-
/** `{ texto, id, modelo, ajustes }` → Buffer de mp3 */
|
|
125
|
-
async gerar({ texto, id, modelo, ajustes = {} }) {
|
|
126
|
-
const audio = await client.textToSpeech.convert(id, {
|
|
127
|
-
text: texto,
|
|
128
|
-
modelId: modelo || "eleven_v4",
|
|
129
|
-
languageCode: "pt",
|
|
130
|
-
outputFormat: "mp3_44100_128",
|
|
131
|
-
voiceSettings: { stability: ajustes.stability ?? 0.5, similarityBoost: ajustes.similarity ?? 0.75, style: ajustes.style ?? 0, useSpeakerBoost: true, speed: ajustes.speed ?? 1 }
|
|
132
|
-
});
|
|
133
|
-
const chunks = [];
|
|
134
|
-
for await (const c of audio) chunks.push(c);
|
|
135
|
-
return Buffer.concat(chunks);
|
|
136
|
-
},
|
|
137
|
-
/** `{ usados, limite, restantes, plano, reinicia }` (caracteres) */
|
|
138
|
-
async saldo() {
|
|
139
|
-
const r = await fetch("https://api.elevenlabs.io/v1/user/subscription", { headers: { "xi-api-key": chave } });
|
|
140
|
-
if (!r.ok) {
|
|
141
|
-
const e = new Error(`saldo: HTTP ${r.status}`);
|
|
142
|
-
e.statusCode = r.status;
|
|
143
|
-
throw e;
|
|
144
|
-
}
|
|
145
|
-
const d = await r.json();
|
|
146
|
-
return { usados: d.character_count, limite: d.character_limit, restantes: d.character_limit - d.character_count, plano: d.tier, reinicia: d.next_character_count_reset_unix ? new Date(d.next_character_count_reset_unix * 1e3) : null };
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ../voz/src/roteador.mjs
|
|
152
|
-
async function saldos({ config = carregarConfig(), criar } = {}) {
|
|
153
|
-
const fabrica = criar || ((conta, chave) => criarElevenLabs({ chave }));
|
|
154
|
-
const out = [];
|
|
155
|
-
for (const conta of config.contas?.elevenlabs || []) {
|
|
156
|
-
const chave = obterChave(`elevenlabs/${conta}`);
|
|
157
|
-
if (!chave) {
|
|
158
|
-
out.push({ conta, erro: "sem chave" });
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
out.push({ conta, ...await fabrica(conta, chave).saldo() });
|
|
163
|
-
} catch (e) {
|
|
164
|
-
out.push({ conta, erro: e.message });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return out;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ../voz/src/alinhamento.mjs
|
|
171
|
-
var NUM = new Set("zero um uma dois duas tr\xEAs tres quatro cinco seis sete oito nove dez onze doze treze catorze quatorze quinze dezesseis dezessete dezoito dezenove vinte trinta quarenta cinquenta sessenta setenta oitenta noventa cem cento duzentos duzentas trezentos trezentas quatrocentos quatrocentas quinhentos quinhentas seiscentos seiscentas setecentos setecentas oitocentos oitocentas novecentos novecentas mil milh\xE3o milh\xF5es bilh\xE3o bilh\xF5es meio meia primeiro primeira".split(" "));
|
|
172
|
-
|
|
173
|
-
// ../voz/bin/saldo.mjs
|
|
174
|
-
var fmt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
|
175
|
-
for (const s of await saldos()) {
|
|
176
|
-
if (s.erro) console.log(`${s.conta.padEnd(10)} ${s.erro}`);
|
|
177
|
-
else console.log(`${s.conta.padEnd(10)} ${fmt(s.restantes)} restantes de ${fmt(s.limite)} (${s.plano})${s.reinicia ? ", reinicia em " + s.reinicia.toLocaleDateString("pt-BR") : ""}`);
|
|
178
|
-
}
|