@praeko/studio 0.0.0-stage → 0.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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/LICENSES.md +38 -0
- package/README.md +42 -2
- package/dist/praeko-studio.mjs +12603 -0
- package/dist/voz-config.mjs +81 -0
- package/dist/voz-conta.mjs +319 -0
- package/dist/voz-saldo.mjs +178 -0
- package/dist/voz-vozes.mjs +247 -0
- package/docs/episodio-yaml.md +57 -0
- package/docs/hyperframes.md +79 -0
- package/docs/longo-yaml.md +78 -0
- package/docs/publicar.md +22 -0
- package/docs/voz.md +22 -0
- package/modelos/avatar.png +0 -0
- package/modelos/sfx/ding.wav +0 -0
- package/modelos/sfx/pop.wav +0 -0
- package/modelos/sfx/thump.wav +0 -0
- package/modelos/sfx/whoosh.wav +0 -0
- package/package.json +59 -4
|
@@ -0,0 +1,247 @@
|
|
|
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/provedores/voicestudio.mjs
|
|
171
|
+
var BASE_PADRAO = "http://127.0.0.1:3900";
|
|
172
|
+
var dormir = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
173
|
+
function prepararTexto(texto) {
|
|
174
|
+
return texto.replace(/\[[^\]]*\]/g, "").replace(/\bIA\b/g, "intelig\xEAncia artificial").replace(/\s*\n\s*/g, " ").replace(/ {2,}/g, " ").trim();
|
|
175
|
+
}
|
|
176
|
+
function criarVoiceStudio({ base = BASE_PADRAO, esperar = dormir } = {}) {
|
|
177
|
+
return {
|
|
178
|
+
nome: "voicestudio",
|
|
179
|
+
base,
|
|
180
|
+
/** { ok, device, versao } ou { ok: false, motivo } */
|
|
181
|
+
async disponivel() {
|
|
182
|
+
try {
|
|
183
|
+
const r = await fetch(`${base}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
184
|
+
if (!r.ok) return { ok: false, motivo: `HTTP ${r.status}` };
|
|
185
|
+
const d = await r.json();
|
|
186
|
+
return { ok: d.status === "ok", device: d.device, versao: d.version };
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return { ok: false, motivo: `sem resposta em ${base} (abra o VoiceStudio)` };
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
/** Perfis de voz: [{ id, name, language }] */
|
|
192
|
+
async perfis() {
|
|
193
|
+
const r = await fetch(`${base}/profiles`, { signal: AbortSignal.timeout(1e4) });
|
|
194
|
+
const d = await r.json();
|
|
195
|
+
return (Array.isArray(d) ? d : d.profiles || d.data || []).map((p) => ({ id: p.id, name: p.name, language: p.language }));
|
|
196
|
+
},
|
|
197
|
+
/** `{ texto, id, modelo, idioma, ajustes, tentativas, intervalo }` → Buffer (wav). 429 = ocupado: espera e repete. */
|
|
198
|
+
async gerar({ texto, id, modelo = "omnivoice", idioma = "pt-BR", ajustes = {}, tentativas = 15, intervalo = 6e4, limite = 18e5 }) {
|
|
199
|
+
const corpo = JSON.stringify({ model: modelo, input: prepararTexto(texto), voice: id, language: idioma, response_format: "wav", ...ajustes });
|
|
200
|
+
for (let i = 0; i < tentativas; i++) {
|
|
201
|
+
const r = await fetch(`${base}/v1/audio/speech`, { method: "POST", headers: { "Content-Type": "application/json" }, body: corpo, signal: AbortSignal.timeout(limite) });
|
|
202
|
+
if (r.status === 429) {
|
|
203
|
+
await esperar(intervalo);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!r.ok) {
|
|
207
|
+
const e = new Error(`VoiceStudio: HTTP ${r.status} ${(await r.text()).slice(0, 200)}`);
|
|
208
|
+
e.statusCode = r.status;
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
return Buffer.from(await r.arrayBuffer());
|
|
212
|
+
}
|
|
213
|
+
throw new Error("VoiceStudio ocupado (429) depois de v\xE1rias tentativas");
|
|
214
|
+
},
|
|
215
|
+
/** Transcrição com tempo por palavra (faster-whisper do próprio VoiceStudio). → [{ t, a, b }] */
|
|
216
|
+
async transcrever({ audio, bytes, idioma = "pt" }) {
|
|
217
|
+
const f = new FormData();
|
|
218
|
+
f.append("file", new Blob([bytes]), audio.split("/").pop());
|
|
219
|
+
f.append("model", "whisper-1");
|
|
220
|
+
f.append("language", idioma);
|
|
221
|
+
f.append("response_format", "verbose_json");
|
|
222
|
+
f.append("timestamp_granularities[]", "word");
|
|
223
|
+
const r = await fetch(`${base}/v1/audio/transcriptions`, { method: "POST", body: f, signal: AbortSignal.timeout(6e5) });
|
|
224
|
+
if (!r.ok) throw new Error(`VoiceStudio (transcri\xE7\xE3o): HTTP ${r.status} ${(await r.text()).slice(0, 200)}`);
|
|
225
|
+
const d = await r.json();
|
|
226
|
+
return { palavras: (d.words || []).map((w) => ({ t: w.word.trim(), a: +w.start.toFixed(2), b: +w.end.toFixed(2) })), texto: d.text };
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ../voz/src/alinhamento.mjs
|
|
232
|
+
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(" "));
|
|
233
|
+
|
|
234
|
+
// ../voz/bin/vozes.mjs
|
|
235
|
+
var cfg = carregarConfig();
|
|
236
|
+
var vs = criarVoiceStudio({ base: cfg.voicestudio?.base });
|
|
237
|
+
var st = await vs.disponivel();
|
|
238
|
+
var perfis = st.ok ? await vs.perfis() : [];
|
|
239
|
+
console.log(`VoiceStudio: ${st.ok ? `no ar (${st.versao}, ${st.device})` : st.motivo}`);
|
|
240
|
+
for (const [nome, v] of Object.entries(cfg.vozes)) {
|
|
241
|
+
let nota = "";
|
|
242
|
+
if (v.provedor === "voicestudio") nota = !st.ok ? "indispon\xEDvel" : perfis.find((p) => p.id === v.id) ? `perfil "${perfis.find((p) => p.id === v.id).name}" ok` : "PERFIL N\xC3O ENCONTRADO";
|
|
243
|
+
else nota = `contas: ${Object.keys(v.ids || {}).join(", ")}`;
|
|
244
|
+
console.log(` ${nome.padEnd(14)} ${v.provedor.padEnd(12)} ${v.modelo || ""} \xB7 alinhador ${v.alinhador || "scribe"} \xB7 ${nota}`);
|
|
245
|
+
}
|
|
246
|
+
console.log("Saldos (ElevenLabs):");
|
|
247
|
+
for (const s of await saldos({ config: cfg })) console.log(` ${s.conta.padEnd(10)} ${s.erro || `${s.restantes} restantes de ${s.limite}`}`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Episódio em YAML
|
|
2
|
+
|
|
3
|
+
`episodios/<slug>/episodio.yaml` descreve o Short; `praeko studio compilar <slug>` (ou `render`/`capa`, que compilam sozinhos) confere e gera `episodio.spec.json`, que o `EpisodioDecl` do `@praeko/studio-kit` lê. O `episodio.tsx` é fixo (criado por `praeko studio novo`). O que o YAML não cobre vai em `cenas.tsx` e é chamado por `componente:`.
|
|
4
|
+
|
|
5
|
+
## Topo
|
|
6
|
+
```yaml
|
|
7
|
+
id: ComentarioTimestamp # nome da Composition (padrão: o de episodio.json)
|
|
8
|
+
duracao: 60.2 # s, vídeo inteiro
|
|
9
|
+
legenda: 80 # px da legenda palavra a palavra
|
|
10
|
+
voz: audio/comentario-timestamp.mp3
|
|
11
|
+
fundo: ["linhas que flutuam no fundo", "…"]
|
|
12
|
+
golpes: [{ em: 31.5, tipo: flash }, { em: 44.9, tipo: zoom }] # shake | zoom | flash | tudo
|
|
13
|
+
fechamento: { inicio: 52.4, cta: ARTIGO NO DEVLOG }
|
|
14
|
+
capa: { id: CapaComentario, componente: CapaComentario } # TSX em cenas.tsx; ou titulo/cor/glow/tam/itens
|
|
15
|
+
cenas: [ … ]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Tempos por frase (acompanham a voz)
|
|
19
|
+
Em `duracao`, `fechamento.inicio`, `golpes[].em` e no `inicio`/`fim` das cenas vale: número (s) · `"frase"` (início da frase na voz) · `{ quando: "frase", nth: 1, depois: 20, mais: -0.1 }` · `{ voz: 2.5 }` (2,5 s depois do fim da voz; negativo = antes). Resolvido no `praeko studio compilar` com o `palavras.json` atual, então basta recompilar (o `render` já faz) depois de uma voz nova ou de um `praeko studio corte --refazer`. Exemplo: `duracao: { voz: 2.5 }`, `fechamento: { inicio: { voz: -2 }, cta: … }`, `inicio: { quando: "Agora um bug real", mais: -0.1 }`.
|
|
20
|
+
|
|
21
|
+
## Cena
|
|
22
|
+
```yaml
|
|
23
|
+
- inicio: 7.05 # s; a cena vai até o inicio da próxima (ou `fim:`, ou o fechamento)
|
|
24
|
+
fim: 52.35 # opcional; muda só a saída do painel
|
|
25
|
+
titulo: FUI CONFERIR # faixa do PainelD (layout padrão: painel)
|
|
26
|
+
layout: painel | livre # livre = itens soltos na tela (posição absoluta, vídeos)
|
|
27
|
+
rotulo: FUI CONFERIR # etiqueta curta da cena
|
|
28
|
+
sons: [{ at: 0.2, f: pop }] # s locais; pop | ding | thump | whoosh
|
|
29
|
+
itens: [ … ] # ou `componente: NomeEmCenas` (um dos dois)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Itens
|
|
33
|
+
Todo item tem `tipo`. **Quando aparece:** `quando: "frase"` (ou `[frase, n]` para a n-ésima ocorrência, a partir de 0) liga ao instante da voz em que a frase começa; `deslocar: 0.4` soma segundos; `em: 1.2` é em segundos desde o início da cena. Frase não encontrada na voz é erro. Posição absoluta: `pos: { esquerda, direita, topo, base, largura, altura }`. Cores: nomes do tema (`destaque texto suave ok erro info extra caixa`) ou `#hex`. **Dentro de `{ … }`, texto com vírgula vai entre aspas.**
|
|
34
|
+
|
|
35
|
+
| tipo | campos |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `texto` | `texto` (`\n` quebra), `tamanho`, `cor`, `fonte` (`padrao`, `mono`; omitido herda), `alinhar: esquerda`, `altura`, `peso`, `pop`, `semFade`, `semQuebra` |
|
|
38
|
+
| `big` | `texto`, `tamanho`, `cor`, `mono` |
|
|
39
|
+
| `caixa` | `texto`, `tamanho`, `cor`, `mono`, `apagada`, `tracejada` |
|
|
40
|
+
| `linha` | `partes: [{texto, mono, cor, peso}]`, `tamanho`, `cor` |
|
|
41
|
+
| `balao` | `texto`, `cor`, `autor` |
|
|
42
|
+
| `codigo` | `titulo`, `cor`, `linhas: [{s, cor, marca}]` |
|
|
43
|
+
| `cartao` | `linhas: [[{texto, cor, peso}]]`, `tamanho`, `cor` (entra deslizando) |
|
|
44
|
+
| `barra` | `altura`, `raio`, `gradiente: [cor, "cor 60%", …]`, `borda: false`, `preenche: {de, ate, inicio, duracao}`, `brilhoAoFim`, `semFade` |
|
|
45
|
+
| `barras` | `linhas: [{rotulo, valor, texto}]`, `max`, `larguraMax`, `larguraRotulo`, `passo`, `gap`, `cor` |
|
|
46
|
+
| `janela` | `src`, `duracaoClipe`, `de`, `reg: [x, y, w, h]`, `tag`, `pos` (esquerda, topo, largura) |
|
|
47
|
+
| `celular` | `src`, `duracaoClipe`, `de`, `pos` |
|
|
48
|
+
| `pilha` / `fila` | `itens`, `gap`, `alinhar` (`centro`, `inicio`, `fim`, `esticar`); fila: `centrar: false`, `espalhar: true` |
|
|
49
|
+
|
|
50
|
+
## O que o compilador confere
|
|
51
|
+
Erros (não grava o spec): campo obrigatório ausente, `tipo` ou cor desconhecidos, chave desconhecida (inclui a vírgula sem aspas), cenas fora de ordem, frase que não está em `palavras.json`. Avisos (estimativa, não prova): âncora fora da janela da cena, itens que somam mais altura do que o painel (920 px) ou largura maior que 844 px, texto que deve quebrar, itens com posição absoluta que se cruzam, duração com menos de 1 s depois do fim da voz. Sempre olhe também `praeko studio folha <slug>`.
|
|
52
|
+
|
|
53
|
+
## Equivalência com TSX
|
|
54
|
+
`comentario-timestamp` (7 cenas) e `raide-ep1` (6 cenas, vídeos e barras animadas) foram portados do TSX para este formato: o primeiro renderiza **idêntico** (PSNR inf), o segundo dentro do ruído do `OffthreadVideo` (média 54,2 dB, mínimo 47,4). A conferência achou um bug do original do raide: a âncora `"dano"` da cena 5 pegava a primeira ocorrência (18,98 s) e a frase aparecia no início da cena.
|
|
55
|
+
|
|
56
|
+
## Motor de render
|
|
57
|
+
`motor: hyperframes` (ou `remotion`) no topo do YAML escolhe quem renderiza este episódio; `praeko studio render --motor …` tem a palavra final. Cenas com `componente:` só renderizam no Remotion. Ver [`hyperframes.md`](hyperframes.md).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# HyperFrames como segundo motor de render
|
|
2
|
+
|
|
3
|
+
O Praeko Studio renderiza com o **Remotion** (padrão) ou com o **HyperFrames** (HTML + GSAP, render por captura de quadros). O `episodio.yaml`, a voz, a legenda, a capa e o pacote de publicação são os mesmos; só muda quem desenha os quadros.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
praeko studio render <slug> --motor hyperframes # out/<slug>.hf.mp4 (não sobrescreve o do Remotion)
|
|
7
|
+
praeko studio prever <slug> --motor hyperframes # abre o preview do HyperFrames no projeto gerado
|
|
8
|
+
praeko studio compilar <slug> --motor hyperframes # avisa as cenas que usam `componente:` (TSX)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Qual motor um episódio usa (a primeira que existir): `--motor` na linha de comando → `motor: hyperframes` no topo do `episodio.yaml` → `"motor": "hyperframes"` no `marca.json` → `remotion`. Um canal novo pode usar `"motor": "hyperframes"` na marca e nunca tocar no Remotion (a capa continua sendo um `still` do Remotion: `praeko studio capa`).
|
|
12
|
+
|
|
13
|
+
## O que acontece
|
|
14
|
+
|
|
15
|
+
1. Compila o `episodio.yaml` (`episodio.spec.json`), como sempre.
|
|
16
|
+
2. `@praeko/studio-hyperframes` gera um projeto em `<projeto>/.hf/<slug>/` (já no `.gitignore` do `praeko studio init`): `index.html`, uma sub-composição por cena em `compositions/`, `assets/` com **só** o que o episódio usa (copiado do `publico` da marca).
|
|
17
|
+
3. `npx hyperframes@0.9.13 check --json`: erros param o render; avisos viram uma linha de resumo (código × quantidade).
|
|
18
|
+
4. `npx hyperframes@0.9.13 render --output out/<slug>.hf.mp4`.
|
|
19
|
+
|
|
20
|
+
O projeto gerado é descartável: não edite `.hf/`, edite o YAML e rode de novo. Para editar visualmente, abra `.hf/<slug>` no app do HyperFrames; o que você mexer lá some na próxima geração.
|
|
21
|
+
|
|
22
|
+
## Short (1080×1920)
|
|
23
|
+
|
|
24
|
+
Os itens do YAML (`texto`, `big`, `caixa`, `linha`, `balao`, `codigo`, `janela`, `celular`, `barra`, `barras`, `pilha`, `fila`, `cartao`) viram HTML com as mesmas medidas do kit do Remotion: painel de 70 px de margem, rótulo, legenda palavra a palavra (mesmo agrupamento de 3 palavras), barra de progresso, golpes (tremor, zoom, clarão), fundo de terminal de duas colunas, sons (whoosh, pop por cena, ding no fecho) e fechamento com avatar, handle e CTA. `quando` resolve para o tempo local da cena como no Remotion.
|
|
25
|
+
|
|
26
|
+
Diferenças conhecidas:
|
|
27
|
+
- **Fontes:** só as embutidas do HyperFrames resolvem pelo nome. Arial Black vira Montserrat 900 e Menlo vira JetBrains Mono (largura um pouco diferente; texto que quebra em uma pode quebrar na outra). Com `"hyperframes": { "fontesLocais": true }` no `marca.json` a fonte da marca é carregada do sistema (`local()`), com a embutida de reserva: paridade exata, mas só nesta máquina.
|
|
28
|
+
- **Vídeos gravados de tela ou de app** (`janela`, `celular`): o trecho `[de, duracaoClipe]` repete até o fim da cena (vários `<video>` em sequência, no lugar do `Loop`).
|
|
29
|
+
- **Ponto vermelho** da etiqueta da janela é fixo (no Remotion pisca).
|
|
30
|
+
- **Áudio** sai uns 3 dB mais alto (média −16,8 dB contra −19,8 dB no mesmo Short): conferir com `praeko studio checar` antes de publicar.
|
|
31
|
+
- **Zona segura vertical:** as posições do YAML valem como estão (o rótulo fica em y=70, a legenda em 190 px da base). O `check` do HyperFrames não reclama, mas as bordas dos apps cobrem o topo.
|
|
32
|
+
|
|
33
|
+
## Longo (1920×1080)
|
|
34
|
+
|
|
35
|
+
Reaproveita o plano do `episodio.yaml` (`formato: longo`: `codigo`, `cartao`, `chip`, `txt`, `video`, `celular`, `barra`, `contador`, `fluxo`) e acrescenta uma edição própria do motor:
|
|
36
|
+
- barra de capítulos no topo, com o título do capítulo corrente;
|
|
37
|
+
- transição entre capítulos (varredura horizontal, vertical, íris e escurecimento, em rodízio) com o número e o nome do capítulo, no silêncio entre os blocos (`blocos.json`) e com whoosh;
|
|
38
|
+
- número do capítulo ao fundo, brilho de fundo que muda de cor por capítulo;
|
|
39
|
+
- legenda palavra a palavra (a palavra falada fica dourada);
|
|
40
|
+
- tela final com `fecho:` (texto, link, imagem).
|
|
41
|
+
|
|
42
|
+
Mascote opcional, só neste motor: no `episodio.yaml` do longo,
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
mascote:
|
|
46
|
+
- { quando: "cinco radiantes voltaram", gato: chocado } # gatos/gato-chocado.png
|
|
47
|
+
- { quando: "a trava so fazia", gato: cafe, mais: 0.5 }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
o gato aparece no canto inferior direito por ~3 s na cena em que a frase é falada.
|
|
51
|
+
|
|
52
|
+
Linhas de código: `hl: [frase-de-início, frase-de-fim]` acende a linha entre as duas frases (como no Remotion).
|
|
53
|
+
|
|
54
|
+
## Limites
|
|
55
|
+
|
|
56
|
+
- **`componente:` (TSX) não existe no HyperFrames.** Cena (ou capa) feita em `cenas.tsx` precisa virar itens no YAML ou o episódio fica no Remotion. O gerador falha com o slug, o número, o título e o componente da cena; `praeko studio compilar --motor hyperframes` avisa antes. A capa de TSX não conta: ela sempre sai do Remotion.
|
|
57
|
+
- Os tempos do `check` são de um instante amostrado: avisos de contraste em barras ainda zeradas e de tela vazia no quadro 0 são esperados (o quadro 0 de um Short é só o fundo; a capa é a miniatura).
|
|
58
|
+
- O HyperFrames busca o GSAP no CDN (jsdelivr) e as fontes embutidas na primeira vez: precisa de internet no primeiro render.
|
|
59
|
+
- Versão fixa: `hyperframes@0.9.13` (`VERSAO_HF` em `packages/hyperframes/src/comum.mjs`). Subir a versão = rodar os testes e comparar quadros.
|
|
60
|
+
|
|
61
|
+
## Remotion × HyperFrames (medido em 04/11/2026, M4 de 10 núcleos)
|
|
62
|
+
|
|
63
|
+
| Episódio | Remotion (GL angle) | HyperFrames (render) | HyperFrames (com geração e check) |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| Short `heranca-corte-cofre` (53 s) | 26 s | 32 s | 38 s |
|
|
66
|
+
| Short `raide-ep1` (42 s, janelas, celular, barras) | 43 s | 40 s | 54 s |
|
|
67
|
+
| Longo `heranca-longo` (7:47; saída 38 MB contra 56 MB do Remotion) | 7:12 (GL padrão) · 7:53 (GL angle) | 3:29 | 3:54 |
|
|
68
|
+
|
|
69
|
+
Observações: o `angle` só ajuda nos Shorts (blur, sombras em 1080×1920); no longo ele não ganhou nada (7:12 com o padrão, 7:53 com `angle`; em 1200 quadros, 46 s contra 44 s), então vale medir antes de trocar. O Remotion só ficou rápido nos Shorts depois de `Config.setChromiumOpenGlRenderer("angle")` no `remotion.config.ts` (antes: 102 s para o mesmo Short; PSNR de 48 dB entre os dois renderizadores). O HyperFrames paga ~6 s de compilação por render e ~6 s de `check`. Concorrência do Remotion (5 ou 10) não mudou nada.
|
|
70
|
+
|
|
71
|
+
## Quando escolher cada um
|
|
72
|
+
|
|
73
|
+
| Situação | Motor |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Episódio com cena em TSX (`componente:`) ou capa de peça própria | Remotion |
|
|
76
|
+
| Short de rotina do canal atual | Remotion (mesmo tempo, mesma aparência de sempre) |
|
|
77
|
+
| Longo, ou qualquer vídeo em que a edição (transições, capítulos, mascote) pesa | HyperFrames |
|
|
78
|
+
| Canal novo, sem TSX legado | HyperFrames como padrão (`"motor": "hyperframes"` na marca) |
|
|
79
|
+
| Quer mexer no resultado em editor visual | HyperFrames (abre `.hf/<slug>` no app) |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Longo em YAML (16:9, 1920×1080)
|
|
2
|
+
|
|
3
|
+
`episodio.yaml` com `formato: longo`. Tempo é **global** (segundos desde o início do vídeo). `praeko studio compilar <slug>` valida e gera `episodio.spec.json`; `praeko studio render <slug>` renderiza (compila antes).
|
|
4
|
+
|
|
5
|
+
## Roteiro em blocos
|
|
6
|
+
```
|
|
7
|
+
## 1 · O GANCHO
|
|
8
|
+
[curious] texto falado…
|
|
9
|
+
|
|
10
|
+
## 2 · O PROBLEMA
|
|
11
|
+
…
|
|
12
|
+
```
|
|
13
|
+
`praeko studio voz <slug> [--qualidade rascunho|final|local]` gera um áudio por bloco em `voz-blocos/` (nome com hash do texto, voz e pronúncia: só o bloco editado é regerado, o que poupa crédito do ElevenLabs), junta com 0,6 s de pausa em `voz.mp3` e escreve `palavras.json`, `palavras.ts` e `blocos.json` (`[{n, titulo, a, b}]`). Blocos antigos ficam no cache.
|
|
14
|
+
|
|
15
|
+
## Topo e cenas
|
|
16
|
+
```yaml
|
|
17
|
+
formato: longo
|
|
18
|
+
id: BancaLongo
|
|
19
|
+
duracao: 334
|
|
20
|
+
voz: audio/banca-longo-voz.mp3
|
|
21
|
+
cenas:
|
|
22
|
+
- { titulo: O GANCHO, inicio: 0, componente: S1 } # cena em TSX (cenas.tsx exporta S1: React.FC<{t:number}>)
|
|
23
|
+
- titulo: O PROBLEMA
|
|
24
|
+
inicio: 31.0 # s; ou { bloco: 2 } (usa blocos.json); `n` opcional (padrão: posição)
|
|
25
|
+
itens: [ … ]
|
|
26
|
+
```
|
|
27
|
+
Cada cena vai do seu `inicio` ao da próxima (a última, até `duracao`); entra e sai com fade e mostra "n · TÍTULO" no canto.
|
|
28
|
+
|
|
29
|
+
## Tempo (`quando`, `hl`)
|
|
30
|
+
`número` (s) · `"frase"` (início da frase na voz) · `{ quando: "frase", nth: 1, depois: 83, mais: 0.6 }` (`nth` = ocorrência, `depois` = ignora antes de N s, `mais` = soma segundos). Frase que não está em `palavras.json` é erro.
|
|
31
|
+
|
|
32
|
+
## Itens (todos com `pos: { esquerda, topo, largura, altura }` em px, exceto `texto`)
|
|
33
|
+
| tipo | campos |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `codigo` | `quando`, `titulo`, `tamanho`, `linhas: [{ s, hl: [tempo, tempo], vermelho }]` (a linha acende entre os dois tempos) |
|
|
36
|
+
| `cartao` | `quando`, `cor`, `itens: [texto…]` (entra deslizando) |
|
|
37
|
+
| `texto` | só dentro de `cartao`: `tamanho`, `cor`, `espacar`, `quando` (fade), `texto` ou `partes` |
|
|
38
|
+
| `chip` | `quando`, `cor`, `tamanho`, `texto` |
|
|
39
|
+
| `txt` | `quando`, `tamanho`, `cor`, `alinhar: centro`, `mono`, `texto` ou `partes: [{ texto, cor, mono }]` |
|
|
40
|
+
|
|
41
|
+
Cores: nomes do tema (`destaque texto suave ok erro info extra caixa`) ou `#hex`. **Texto com vírgula dentro de `{ … }` vai entre aspas** (o compilador acusa chave desconhecida).
|
|
42
|
+
|
|
43
|
+
## Avisos do compilador (estimativa)
|
|
44
|
+
Item que passa da borda direita ou invade a legenda (abaixo de ~960 px), itens de posição absoluta que se sobrepõem, item que entra fora da janela da cena, duração com menos de 1 s depois do fim da voz.
|
|
45
|
+
|
|
46
|
+
## Tela final (opcional)
|
|
47
|
+
```yaml
|
|
48
|
+
duracao: { voz: 6 } # a tela final precisa de uns 5 s depois da voz
|
|
49
|
+
fecho: { inicio: { quando: "última frase", depois: 480 }, texto: "Obrigado por assistir", link: meusite.com.br }
|
|
50
|
+
```
|
|
51
|
+
Mostra avatar, `texto` (a assinatura), o `handle` do tema e o `link`, sobre as cenas, de `inicio` até o fim (a legenda continua). `inicio` aceita número, "frase" ou `{ voz: N }`; `texto`, `link` e `imagem` (arquivo em `public/`, ex.: `gatos/gato-feliz.png`; padrão: o avatar do tema) são opcionais.
|
|
52
|
+
|
|
53
|
+
## Capa (YouTube 16:9 e Instagram vertical)
|
|
54
|
+
```yaml
|
|
55
|
+
capa:
|
|
56
|
+
id: CapaBancaLongo # padrão: Capa<id>; a vertical é <id>Vertical
|
|
57
|
+
titulo: ["DOIS CLIQUES", "UM ITEM"] # 2 linhas; a 2ª sai em destaque (≤ ~16 letras cada na 16:9)
|
|
58
|
+
gato: chocado # gatos/gato-<nome>.png (opcional)
|
|
59
|
+
codigo: { titulo: social.mjs · marketBuy, linhas: [{ s: "UPDATE market ..." }, { s: "AND sold_to IS NULL", hl: true }] } # até 3 linhas
|
|
60
|
+
chips: [{ texto: "ok ✔", cor: ok }, { texto: vendido, cor: erro }]
|
|
61
|
+
```
|
|
62
|
+
`praeko studio capa <slug>` gera `out/<slug>-capa-1280x720.png` e `out/<slug>-capa-vertical-1080x1920.png` (a vertical usa a `Moldura` das capas dos Shorts, dentro da grade 3:4 do Instagram). A capa da banca bate com a publicada a 58,4 dB.
|
|
63
|
+
|
|
64
|
+
## Pacote de publicação do longo
|
|
65
|
+
`publicacao.json` com `"formato": "longo"` (campos como no Short, mais `yt.comentarioFixo` e `arquivos.capaVertical`). `praeko studio pacote <slug>` monta o `textos.md`: título e alternativas, descrição **com capítulos**, comentário fixo, legenda do Instagram e a conferência. Os capítulos saem das cenas (`inicio` + título em minúsculas) e valem as regras do YouTube: começar em 0:00, 3 ou mais, 10 s cada um (senão é ERRO). Para outros textos, escreva `capitulos: [{ t, titulo }]` no `publicacao.json`. O UTM do YouTube usa `utm_medium=long`. No longo da banca os 9 capítulos saíram iguais aos publicados à mão.
|
|
66
|
+
|
|
67
|
+
## Itens de mídia e animação (longo)
|
|
68
|
+
Todos com `quando` e `pos`:
|
|
69
|
+
| tipo | campos |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `video` | `src` (em `public/`, 16:9), `duracaoClipe`, `de` (s do clipe), `reg: [x, y, w, h]` (recorte em 1920×1080), `tag`, `pos: { esquerda, topo, largura }` (altura sai do recorte) |
|
|
72
|
+
| `celular` | `src` (vertical 1080×1920), `duracaoClipe`, `de`, `pos: { esquerda, topo, largura }` |
|
|
73
|
+
| `barra` | `valor` (0–1), `duracao`, `cor`, `rotulo`, `texto` (dentro da barra), `altura`, `pos` |
|
|
74
|
+
| `contador` | `de`, `ate`, `duracao`, `casas`, `prefixo`, `sufixo`, `tamanho`, `cor`, `mono`, `alinhar: centro`, `pos` |
|
|
75
|
+
| `fluxo` | `nos: [{ texto (\n quebra), cor, quando }]`, `tamanho`, `cor`, `pos: { esquerda, topo, largura }`: caixas ligadas por setas que entram uma a uma |
|
|
76
|
+
|
|
77
|
+
## Motor e mascote (HyperFrames)
|
|
78
|
+
`motor: hyperframes` no topo escolhe o motor de render deste longo (padrão: o da marca, depois `remotion`). `mascote: [{ quando: "frase", gato: chocado, mais: 0.5 }]` faz o gato `gatos/gato-chocado.png` aparecer no canto por ~3 s; só o HyperFrames desenha o mascote (o Remotion ignora). Ver [`hyperframes.md`](hyperframes.md).
|
package/docs/publicar.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Publicar pelo Praeko
|
|
2
|
+
|
|
3
|
+
O Studio **não publica nada sozinho**. `praeko studio pacote <slug>` gera `textos.md` e confere limites, hashtags e a declaração de IA; `praeko studio agendar` entrega vídeo, capa e textos ao Praeko, que agenda nas redes conectadas ao seu workspace.
|
|
4
|
+
|
|
5
|
+
## Pela CLI (`--via praeko`)
|
|
6
|
+
```sh
|
|
7
|
+
echo $CHAVE | praeko studio agendar --via praeko --chave # chave praeko_… (escopo write); a mesma da `praeko` CLI
|
|
8
|
+
praeko studio agendar --canais --via praeko # lista os canais conectados
|
|
9
|
+
praeko studio agendar <slug> --quando "06/11 17:30" --via praeko # SIMULA (só lê os canais)
|
|
10
|
+
praeko studio agendar <slug> --quando "06/11 17:30" --via praeko --enviar # envia o vídeo e agenda de verdade
|
|
11
|
+
praeko studio agendar <slug> --status | --cancelar | --limpar
|
|
12
|
+
```
|
|
13
|
+
`marca.json` → `publicacao.provedor: "praeko"` torna o Praeko o padrão (o `init` já grava assim). `publicacao.praeko.canais` fixa a conta quando há mais de uma por rede. YouTube sobe privado e é agendado; TikTok vai como rascunho (`publicacao.praeko.tiktok.modo`); o horário mínimo é 5 minutos à frente.
|
|
14
|
+
|
|
15
|
+
## Pelo MCP do Praeko (agente com shell)
|
|
16
|
+
O arquivo do vídeo nunca passa pelo MCP. Em um passo: `create_media_upload { type: "video", name, mime, size }` (`size` em bytes: `wc -c < arquivo`) devolve o `curl` pronto; envie o arquivo; depois `complete_media_upload { mediaId }` até o estado `ready`. Então `create_post`/`schedule_post` em **dois passos**: a 1ª chamada devolve só a **prévia** e um `confirmation_code`; **mostre a prévia inteira à pessoa**, espere o sim e repita a chamada com os mesmos argumentos mais `confirm`. O agente nunca confirma sozinho. Veja a documentação de MCP em praeko.app/docs.
|
|
17
|
+
|
|
18
|
+
## Declaração de IA (conferida pelo `checar` e pelo `pacote`)
|
|
19
|
+
Preencha `"ia": { "sintetico": true|false }` no `publicacao.json`. Voz sintética realista, ou imagem/vídeo gerado que pareça real, precisa do rótulo de conteúdo alterado/sintético: YouTube (campo `containsSyntheticMedia`, enviado quando o Studio publica direto), "AI info" no Instagram e o rótulo de IA no TikTok. Pelo caminho do Praeko, **marque o rótulo no app de cada rede antes de publicar** (a API pública do Praeko ainda não leva esse campo). Rascunhar roteiro com IA ou clonar a **sua própria** voz para narrar não exigem o rótulo no YouTube, mas as regras mudam: confira a política vigente de cada rede.
|
|
20
|
+
|
|
21
|
+
## Avançado
|
|
22
|
+
`postar` (publicação direta com credenciais suas de cada rede), `fila` e `conta <rede>` seguem no pacote para quem precisa; `fila instalar` usa o launchd e só funciona no macOS. O provedor Buffer (`--via buffer`) é legado.
|
package/docs/voz.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Voz no Praeko Studio
|
|
2
|
+
|
|
3
|
+
`praeko studio voz <slug> --qualidade rascunho|final|local` gera `voz.mp3` e os tempos por palavra (`palavras.json`/`palavras.ts`).
|
|
4
|
+
|
|
5
|
+
| Qualidade | O que faz | Custo |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `rascunho` | voz **estimada e muda** (~190 palavras/min), sem rede | grátis |
|
|
8
|
+
| `final` | ElevenLabs (TTS) + Scribe (tempos por palavra), conta e chave **suas** | caracteres do seu plano |
|
|
9
|
+
| `local` | VoiceStudio (servidor local compatível, `http://127.0.0.1:3900` por padrão) | grátis, precisa estar rodando |
|
|
10
|
+
|
|
11
|
+
## Configurar a sua voz
|
|
12
|
+
Camadas de `vozes.json` (a de cima vence): **projeto** (ao lado do `marca.json`) > **usuário** (`~/.config/praeko/studio/vozes.json`, ou `PRAEKO_STUDIO_CONFIG`) > padrão do pacote (sem ids).
|
|
13
|
+
1. Guarde a chave: `echo $CHAVE | praeko studio voz config chave elevenlabs/principal` (cofre do sistema no macOS; arquivo `~/.config/praeko/segredos.json` com permissão 600 em Linux; no Windows prefira a variável `ELEVENLABS_API_KEY`).
|
|
14
|
+
2. Cadastre a voz: `praeko studio voz conta adicionar principal --voz minha-voz --voz-id <id da voz>` (grava o id no `vozes.json` do projeto) ou defina `ELEVENLABS_VOICE_ID`.
|
|
15
|
+
3. No `marca.json`, `"voz": "minha-voz"` vale para `voz` sem `--qualidade`; com `--qualidade final` vale a voz de `qualidade.final`. O padrão do `init` é `rascunho-mudo`, para nunca gastar sem querer.
|
|
16
|
+
Mais de uma conta (failover por saldo): `conta adicionar <outra>`; o roteador pula a conta sem saldo ou com chave inválida.
|
|
17
|
+
|
|
18
|
+
## Regras
|
|
19
|
+
- Use **somente a sua voz** ou uma que você tem autorização para usar (termos do ElevenLabs).
|
|
20
|
+
- Voz sintética realista exige o rótulo de IA nas plataformas: preencha `"ia": { "sintetico": true }` no `publicacao.json`.
|
|
21
|
+
- Chave nunca em arquivo do projeto, em log ou em chat com agente. Ordem de leitura: variável de ambiente, cofre do macOS, arquivo de segredos.
|
|
22
|
+
- O modelo OmniVoice do VoiceStudio é CC-BY-NC (não comercial): não use em conteúdo monetizado.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praeko/studio",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Praeko Studio: vídeos curtos (9:16) e longos (16:9) a partir de um roteiro, para você operar com o seu próprio agente de código (Claude Code, Codex, Gemini CLI, Cursor, opencode). Voz, legenda palavra a palavra, cenas, capa, textos e publicação pelo Praeko.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"praeko",
|
|
9
|
+
"video",
|
|
10
|
+
"shorts",
|
|
11
|
+
"remotion",
|
|
12
|
+
"captions",
|
|
13
|
+
"studio",
|
|
14
|
+
"cli",
|
|
15
|
+
"agent",
|
|
16
|
+
"shorts"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://praeko.app/docs",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://praeko.app/docs"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/Praeko/praeko-js.git",
|
|
25
|
+
"directory": "studio/packages/cli"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"praeko-studio": "dist/praeko-studio.mjs"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"modelos",
|
|
33
|
+
"docs",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"LICENSES.md"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node ../../../scripts/build-studio.mjs cli",
|
|
45
|
+
"prepack": "npm run build",
|
|
46
|
+
"test": "node --test test/*.test.mjs"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@elevenlabs/elevenlabs-js": "^2.75.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@praeko/studio-hyperframes": "*",
|
|
53
|
+
"@praeko/studio-publicar": "*",
|
|
54
|
+
"@praeko/studio-revisar": "*",
|
|
55
|
+
"@praeko/studio-voz": "*",
|
|
56
|
+
"yaml": "^2.9.1"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22"
|
|
60
|
+
}
|
|
61
|
+
}
|