@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.
@@ -1,247 +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/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}`}`);
@@ -1,57 +0,0 @@
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).
@@ -1,78 +0,0 @@
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 DELETED
@@ -1,22 +0,0 @@
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 DELETED
@@ -1,22 +0,0 @@
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.
File without changes
File without changes
File without changes
File without changes
File without changes