primocode 10.12.0 → 10.13.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/README.md +10 -2
- package/lib/estudio/assinaturas/gerativo.json +1 -1
- package/lib/estudio/camadas.js +4 -0
- package/lib/estudio/diretor.js +2 -1
- package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
- package/lib/estudio/gerativo.js +44 -15
- package/lib/estudio/index.js +36 -3
- package/lib/estudio/kit/gerativo.jsx +15 -0
- package/lib/estudio/kokoro.js +3 -1
- package/lib/estudio/remotion.js +6 -4
- package/lib/estudio/revisor.js +61 -0
- package/lib/estudio/voicestudio.js +81 -0
- package/lib/video/auto.js +18 -3
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +33 -51
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.
|
|
1
|
+
# PrimoCode v10.13.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.13.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -17,6 +17,14 @@ está no limite de pedidos, o próximo atende. Você não vê o erro, e não pag
|
|
|
17
17
|
Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
|
|
18
18
|
veja `/mascot` e `/name`.
|
|
19
19
|
|
|
20
|
+
## O que mudou na 10.13
|
|
21
|
+
|
|
22
|
+
- **Motion pelas referências:** o título enche a tela (rótulo nunca abaixo de 2% da altura), a cena dura de 1 a 2 s, o texto entra com ataque rápido e freio forte, e a frase sai inteira. As saídas que espalhavam letras saíram. O círculo de caneta na palavra-chave ganha o ✓. Sem quadro morto entre as cenas. A conta está em `docs/planos/2026-10-15-motion-pelas-referencias.md`.
|
|
23
|
+
- **Revisão do texto:** antes do render, o PrimoCode avisa texto pequeno demais para o celular e contraste abaixo de 3:1.
|
|
24
|
+
- **Camadas sem legenda dobrada:** a cena escrita pela IA não repete a fala por cima da legenda.
|
|
25
|
+
- **Voz do VoiceStudio:** com o app aberto no Mac, ele narra primeiro. Fechado, a narração segue para o Gemini (`PRIMOCODE_SEM_VOICESTUDIO=1` desliga).
|
|
26
|
+
- **Edição de vídeo:** num vídeo sem pedido falado, a edição não para mais na legenda. O agente corta, dá zoom e cor.
|
|
27
|
+
|
|
20
28
|
## O que mudou na 10.12
|
|
21
29
|
|
|
22
30
|
- **Vídeo em camadas:** você grava só a fala e o PrimoCode monta o resto. O rosto vira cartão, bolinha no canto, some e volta, com mola, e o motion ocupa o espaço que sobra. A legenda vai palavra por palavra, com a palavra forte em serif itálico. O áudio nunca pula. Use `estudio_video acao:"camadas"`: sem batidas, ele propõe pela transcrição.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"origem": "o vídeo que o Joel aprovou em 08/10/2026 (teste-e2e-sono, o gerativo do PR #113, 30,3 s), medido",
|
|
5
5
|
"descricao": "Batida curta e seca, pouco texto por linha, numa identidade só (2 chãos, 1 acento, 2 fontes): o fundo troca na virada de seção, a primeira por corte seco. A palavra gigante entra em ~1 de cada 3 batidas. O som é desenhado da semente e fica mais raro que no fluido.",
|
|
6
6
|
"faixas": {
|
|
7
|
-
"batida_mediana_s": { "min":
|
|
7
|
+
"batida_mediana_s": { "min": 1, "max": 3, "medido": 3.18, "revisto": "15/10/2026: as referências do Joel (\"motion devem sair assim\") cortam a cada 1 a 2 s; a faixa saiu de 2–4,5 para 1–3 s.", "diz": "cada batida dura de 1 a 3 s, o normal é ~2 s" },
|
|
8
8
|
"palavras_por_frase_max": { "max": 5, "medido": 3, "diz": "no máximo 3 palavras por linha" },
|
|
9
9
|
"palavras_por_frase_mediana": { "max": 3, "medido": 2, "diz": "o normal é 1 a 2 palavras por linha" },
|
|
10
10
|
"cortes_secos": { "min": 1, "medido": 5, "diz": "o fundo troca por corte seco (5 das 9 trocas)" },
|
package/lib/estudio/camadas.js
CHANGED
|
@@ -299,6 +299,10 @@ function montar({ src, duracao, palavras = [], batidas, largura = 1080, altura =
|
|
|
299
299
|
tipo: 'componente', src: b.cena,
|
|
300
300
|
dados: {
|
|
301
301
|
largura: area.w, altura: area.h, duracao: r3(dur - entra),
|
|
302
|
+
/* 15/10: a cena escrevia a fala, e a legenda do PrimoCode vinha
|
|
303
|
+
por cima ("provou / provou o bolo"). A fala é para SINCRONIA. */
|
|
304
|
+
legendaNoQuadro: true,
|
|
305
|
+
regra: 'a legenda da fala já está no quadro: não escreva a fala; use as palavras só para o tempo, e desenhe a ideia',
|
|
302
306
|
fala: ws.map((p) => p.texto).join(' '),
|
|
303
307
|
palavras: ws.map((p) => ({ texto: p.texto, t: r3(p.de - b.de - entra) })),
|
|
304
308
|
destaque: (Array.isArray(b.destaque) ? b.destaque : b.destaque ? [b.destaque] : []),
|
package/lib/estudio/diretor.js
CHANGED
|
@@ -110,7 +110,8 @@ function vozPara({ tom = 'neutro', idioma = 'pt-BR', genero } = {}) {
|
|
|
110
110
|
/** Quantas cenas cabem, e de que tamanho, num vídeo desta duração. */
|
|
111
111
|
function ritmoPara(segundos) {
|
|
112
112
|
const s = Math.max(5, Number(segundos) || 30);
|
|
113
|
-
|
|
113
|
+
// (15/10) As referências cortam a cada 1 a 2 s; o PrimoCode pedia 2,6 a 5 s e saía lento.
|
|
114
|
+
const media = s <= 20 ? 1.8 : s <= 45 ? 2.2 : s <= 90 ? 2.6 : 3;
|
|
114
115
|
return { cenas: Math.max(3, Math.round(s / media)), duracaoMedia: media,
|
|
115
116
|
palavrasPorCena: Math.round(media * 2.6) }; // ~155 palavras/minuto de narração
|
|
116
117
|
}
|
|
@@ -16,7 +16,8 @@ export default function TerminalDigitando({ el, t }) {
|
|
|
16
16
|
const c = d.cores || {};
|
|
17
17
|
const f = d.fontes || {};
|
|
18
18
|
const u = t - (d.atraso || 0);
|
|
19
|
-
|
|
19
|
+
// O prompt "›" é da janela: a linha que já vem com "> " ou "$ " saía com ele dobrado ("› > faz um site", 15/10).
|
|
20
|
+
const linhas = (Array.isArray(d.linhas) && d.linhas.length ? d.linhas : [d.titulo || d.fala || 'faz o vídeo pra mim']).map((x) => String(x).replace(/^\s*[>›$#]\s+/, '')).slice(0, 6);
|
|
20
21
|
const vel = d.velocidade || 26;
|
|
21
22
|
const k = mola(u);
|
|
22
23
|
const largura = Math.min(W * 0.96, H * 1.5);
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -510,7 +510,9 @@ function tituloDaFala(fala) {
|
|
|
510
510
|
|
|
511
511
|
function conteudoDe(p, marca = {}) {
|
|
512
512
|
const titulo = p.titulo || p.texto || tituloDaFala(p.fala);
|
|
513
|
-
|
|
513
|
+
// O item que repete o título ("Sem troco" / "sem troco", 15/10) sai: o quadro dizia a mesma coisa duas vezes.
|
|
514
|
+
const igual = (a, b) => tokens(a).join(' ') === tokens(b).join(' ');
|
|
515
|
+
const itens = itensDe(p).filter((it) => !igual(it, titulo));
|
|
514
516
|
if (p.tipo === 'numero' && numeroDe(p.valor)) return { tipo: 'numero', valor: String(p.valor), rotulo: p.rotulo || titulo, sub: p.sub || '' };
|
|
515
517
|
if (itens.length >= 2) return { tipo: 'lista', titulo: p.tipo === 'cores' ? titulo : titulo, itens: itens.slice(0, 5), numerada: p.tipo === 'passos' };
|
|
516
518
|
if (p.tipo === 'contraste' && (p.nao || p.sim)) return { tipo: 'contraste', nao: p.nao || '', sim: p.sim || titulo };
|
|
@@ -528,7 +530,7 @@ const textoDoConteudo = (c) => [c.titulo, c.sub, c.valor, c.rotulo, c.nao, c.sim
|
|
|
528
530
|
const COMPOSICOES = ['gigante', 'pilha', 'divisao', 'contador', 'cascata', 'parede', 'citacao', 'marca', 'palavras', 'produto'];
|
|
529
531
|
const COMPORTAMENTOS = ['carimbo', 'mascara', 'onda', 'decodifica', 'peso', 'largura', 'tracking', 'desfoque', 'flap', 'pontos', 'contorno', 'monta'];
|
|
530
532
|
const SAIDAS = ['pontos', 'evapora', 'atravessa', 'desliza', 'comprime', 'explode', 'chicote', 'inunda', 'corte'];
|
|
531
|
-
const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta'];
|
|
533
|
+
const ACENTOS = ['ponto', 'sublinha', 'anel', 'risco', 'barra', 'bala', 'marca', 'moldura', 'seta', 'visto'];
|
|
532
534
|
|
|
533
535
|
function pesosDe(c, { curto, palavras, primeira, ultima, tem3d, pergunta, exclama, numeroNoTitulo }) {
|
|
534
536
|
switch (c.tipo) {
|
|
@@ -1055,13 +1057,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1055
1057
|
const M = Math.round(W * (deitado ? 0.085 : 0.095));
|
|
1056
1058
|
const larguraUtil = W - 2 * M;
|
|
1057
1059
|
const base = Math.min(W, H) / 1080;
|
|
1058
|
-
|
|
1060
|
+
/* (15/10, as referências) O rótulo de 24–30 px (1,6% da altura no 9:16)
|
|
1061
|
+
não se lia no celular ("NO COADOR", "pra você, de graça"). O rótulo
|
|
1062
|
+
fica em ~2% da altura, e a segunda voz (o sub, a frase de apoio) em
|
|
1063
|
+
3–4%, como a anotação das referências. */
|
|
1064
|
+
const tamRotulo = Math.round(Math.max((deitado ? 24 : 30) * base, H * (deitado ? 0.026 : 0.02)));
|
|
1059
1065
|
const tem3d = !!roteiro.modelo3d;
|
|
1060
1066
|
const ws = temposDasPartes(partes, tempos);
|
|
1061
1067
|
|
|
1062
1068
|
// ── os limites de cada parte na linha do tempo ────────────────────────
|
|
1063
1069
|
const inicio = [];
|
|
1064
|
-
|
|
1070
|
+
// 0,9 s (eram 1,4): nas referências a cena dura 1 a 2 s; com fala rápida a parte segue a voz.
|
|
1071
|
+
partes.forEach((_, i) => inicio.push(i === 0 ? 0 : r3(Math.max(inicio[i - 1] + 0.9, ws[i].de - 0.12))));
|
|
1065
1072
|
const ultimaFala = ws[ws.length - 1];
|
|
1066
1073
|
const duracao = r1(Math.max(inicio[inicio.length - 1] + 2.0, (ultimaFala.ate || 0) + 1.15));
|
|
1067
1074
|
const fimParte = partes.map((_, i) => (i < partes.length - 1 ? inicio[i + 1] : duracao));
|
|
@@ -1544,14 +1551,18 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1544
1551
|
resto); no fim, cada gesto vira o seu par limpo. */
|
|
1545
1552
|
/* O substituto é um limpo que não aparece nas 3 batidas de cada lado: a
|
|
1546
1553
|
passagem e o gesto da tese não voltam antes de 3 batidas. */
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1554
|
+
/* (15/10, as referências) Os pontos, o evapora e o comprime também
|
|
1555
|
+
espalhavam as letras ("dad|o", "· p o n d e", parecia glitch). Nas
|
|
1556
|
+
referências a frase sai inteira: desliza, atravessa ou corta seco. */
|
|
1557
|
+
const TORTOS = ['decodifica', 'flap', 'onda', 'monta', 'pontos'];
|
|
1558
|
+
const GESTOS_LIMPOS = ['mascara', 'desfoque', 'tracking', 'contorno', 'carimbo'];
|
|
1559
|
+
const SAIDAS_LIMPAS = ['desliza', 'atravessa', 'corte'];
|
|
1560
|
+
const SAIDAS_SUJAS = ['explode', 'chicote', 'pontos', 'evapora', 'comprime'];
|
|
1550
1561
|
const vizinhas = (i) => batidas.filter((_, j) => j !== i && Math.abs(j - i) <= 3);
|
|
1551
1562
|
const principal = (b) => (b.linhas || []).find((l) => (l.papel === 'tese' || l.papel === 'numero') && !l.marca);
|
|
1552
1563
|
const comGesto = batidas.filter(principal);
|
|
1553
1564
|
batidas.forEach((b, i) => {
|
|
1554
|
-
if (
|
|
1565
|
+
if (SAIDAS_SUJAS.includes(b.saida)) {
|
|
1555
1566
|
const perto = vizinhas(i).map((x) => x.saida);
|
|
1556
1567
|
b.saida = SAIDAS_LIMPAS.find((x) => !perto.includes(x)) || 'corte';
|
|
1557
1568
|
if (batidas[i + 1]) batidas[i + 1].entrada = b.saida;
|
|
@@ -1626,6 +1637,11 @@ function molduraDe(roteiro, E, { W, H, deitado, marca, secoes }) {
|
|
|
1626
1637
|
const pesoDe = (fonte) => (VARIAVEIS[fonte] && VARIAVEIS[fonte].eixos.wght) || [400, 400];
|
|
1627
1638
|
|
|
1628
1639
|
// ── as composições: de conteúdo + contexto, uma batida ─────────────────
|
|
1640
|
+
/** A segunda voz (sub, a frase de apoio): 3–4% da altura, como a anotação das referências. */
|
|
1641
|
+
function tamSegunda({ H, deitado, tamRotulo }) {
|
|
1642
|
+
return Math.round(Math.max(tamRotulo, H * (deitado ? 0.042 : 0.03)));
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1629
1645
|
function compor(comp, br, ctx) {
|
|
1630
1646
|
const { s, W, H, M, deitado, base, larguraUtil, tamRotulo, fontes } = ctx;
|
|
1631
1647
|
const c = br.conteudo;
|
|
@@ -1739,6 +1755,8 @@ function compor(comp, br, ctx) {
|
|
|
1739
1755
|
else {
|
|
1740
1756
|
const lt = comTese || teses[teses.length - 1];
|
|
1741
1757
|
b.acento.push({ forma, alvo: { linha: lt.id, palavra: comTese ? lt.palavras.findIndex((p) => p.papel === 'tese') : null } });
|
|
1758
|
+
// O círculo de caneta na tese ganha o ✓ (15/10, as referências), sem gastar sorteio.
|
|
1759
|
+
if (forma === 'anel' && comTese) b.acento.push({ forma: 'visto', alvo: { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') } });
|
|
1742
1760
|
}
|
|
1743
1761
|
}
|
|
1744
1762
|
// O ThreeCanvas só aceita pixel inteiro; ele ocupa a altura toda, para o chão não cortar.
|
|
@@ -1759,7 +1777,13 @@ function compor(comp, br, ctx) {
|
|
|
1759
1777
|
}
|
|
1760
1778
|
for (const t of fit.linhas) { const l = linha(t, 'apoio', { tamanho: fit.tamanho, eixos: { wght: 560 } }); misturarPesos(l, { maiorTese: 1.14 }); }
|
|
1761
1779
|
const lt = b.linhas.find((l) => l.palavras.some((p) => p.papel === 'tese'));
|
|
1762
|
-
if (lt) {
|
|
1780
|
+
if (lt) {
|
|
1781
|
+
tese(lt);
|
|
1782
|
+
const alvo = { linha: lt.id, palavra: lt.palavras.findIndex((p) => p.papel === 'tese') };
|
|
1783
|
+
const forma = s.chance(0.6) ? 'sublinha' : 'anel';
|
|
1784
|
+
b.acento.push({ forma, alvo });
|
|
1785
|
+
if (forma === 'anel') b.acento.push({ forma: 'visto', alvo: { ...alvo } });
|
|
1786
|
+
}
|
|
1763
1787
|
else pontoFinal(b.linhas[b.linhas.length - 1]);
|
|
1764
1788
|
break;
|
|
1765
1789
|
}
|
|
@@ -1774,7 +1798,7 @@ function compor(comp, br, ctx) {
|
|
|
1774
1798
|
const l = linha(c.valor, 'numero', { fonte, tamanho: tam, eixos });
|
|
1775
1799
|
const tRot = Math.max(30 * base, Math.min(tam * 0.22, 64 * base));
|
|
1776
1800
|
if (c.rotulo) linha(c.rotulo, 'apoio', { tamanho: tRot, eixos: { wght: 600 }, fonte: fontes.display });
|
|
1777
|
-
if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho:
|
|
1801
|
+
if (c.sub) linha(c.sub, 'rotulo', { fonte: fontes.mono, tamanho: tamSegunda(ctx), tracking: 0.02, eixos: { wght: 400 } });
|
|
1778
1802
|
b.contador = { linha: l.id, ...n, dur: 0.9 };
|
|
1779
1803
|
b.acento.push({ forma: c.valor.length <= 4 ? 'anel' : 'sublinha', alvo: { linha: l.id } });
|
|
1780
1804
|
break;
|
|
@@ -1844,7 +1868,7 @@ function compor(comp, br, ctx) {
|
|
|
1844
1868
|
const tam = tamanhoQueCabe(nome, lg, (deitado ? 0.3 : 0.16) * H, { fonte, wght: 800 });
|
|
1845
1869
|
b.bloco = { x: W / 2 + tam * 0.3, y: H * 0.5, alinha: 'centro', vert: 'meio', largura: lg, gap: 0.2 };
|
|
1846
1870
|
linha(nome, 'tese', { fonte, tamanho: tam, eixos: { wght: Math.min(800, pesoDe(fonte)[1]) }, comportamento: 'contorno' }).marca = true;
|
|
1847
|
-
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho:
|
|
1871
|
+
if (c.sub || c.link) linha(c.sub || c.link, 'rotulo', { fonte: fontes.mono, tamanho: tamSegunda(ctx), tracking: 0.06, eixos: { wght: 400 } });
|
|
1848
1872
|
b.acento.push({ forma: 'ponto', alvo: { linha: b.linhas[0].id, antes: true } });
|
|
1849
1873
|
break;
|
|
1850
1874
|
}
|
|
@@ -1952,9 +1976,10 @@ function tempoDaBatida(b, palavras, ctx) {
|
|
|
1952
1976
|
l.t = r3(Math.max(i === 0 ? b.de + 0.1 : t + REGRAS.stagger[0] * Q, falado != null ? falado : (i === 0 ? b.de + 0.12 : t + passo)));
|
|
1953
1977
|
t = l.t;
|
|
1954
1978
|
});
|
|
1955
|
-
/* Quadro morto não: a primeira coisa da batida entra em até 0,
|
|
1956
|
-
que a voz ainda não a tenha dito (o resto segue a voz).
|
|
1957
|
-
|
|
1979
|
+
/* Quadro morto não: a primeira coisa da batida entra em até 0,12 s, mesmo
|
|
1980
|
+
que a voz ainda não a tenha dito (o resto segue a voz). Eram 0,35 s, e
|
|
1981
|
+
com a saída da anterior o teste de 15/10 tinha 23% de quadro vazio. */
|
|
1982
|
+
if (ordem[0] && ordem[0].t > b.de + 0.12) ordem[0].t = r3(b.de + 0.12);
|
|
1958
1983
|
for (let i = 1; i < ordem.length; i++) if (ordem[i].t < ordem[i - 1].t + REGRAS.stagger[0] * Q) ordem[i].t = r3(ordem[i - 1].t + REGRAS.stagger[0] * Q);
|
|
1959
1984
|
// Nada entra tarde demais para ser lido: de trás para frente, com o stagger mínimo.
|
|
1960
1985
|
for (let i = ordem.length - 1; i >= 0; i--) {
|
|
@@ -1985,7 +2010,11 @@ function tempoDaBatida(b, palavras, ctx) {
|
|
|
1985
2010
|
let tw = l.t;
|
|
1986
2011
|
l.palavras.forEach((p, j) => {
|
|
1987
2012
|
const alvo = qs[j] != null ? qs[j] : (j === 0 ? l.t : tw + 2 * Q);
|
|
1988
|
-
|
|
2013
|
+
/* Quadro morto não, também na palavra (15/10): a voz pode dizer
|
|
2014
|
+
outra coisa antes ("Aponta a câmera, paga, e o pão…"); a primeira
|
|
2015
|
+
palavra da batida entra com a linha, as outras seguem a voz. */
|
|
2016
|
+
const abre = j === 0 && l === ordem[0];
|
|
2017
|
+
p.t = r3(abre ? l.t : Math.min(Math.max(j === 0 ? l.t : tw + 2 * Q, alvo), Math.max(l.t, fim)));
|
|
1989
2018
|
tw = p.t;
|
|
1990
2019
|
});
|
|
1991
2020
|
}
|
package/lib/estudio/index.js
CHANGED
|
@@ -65,6 +65,7 @@ const gerativo = require('./gerativo.js');
|
|
|
65
65
|
const fluido = require('./fluido.js');
|
|
66
66
|
const vox = require('./vox.js');
|
|
67
67
|
const kokoro = require('./kokoro.js');
|
|
68
|
+
const voicestudio = require('./voicestudio.js');
|
|
68
69
|
const lancamento = require('./lancamento.js');
|
|
69
70
|
const glifos = require('./glifos.js');
|
|
70
71
|
const personagem = require('./personagem.js');
|
|
@@ -887,7 +888,7 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
887
888
|
ok: true, proposta: camadas.propor(palavras, m.segundos), duracao: m.segundos,
|
|
888
889
|
comoUsar: 'é uma SUGESTÃO pela transcrição. Decida você: mude as formas, junte ou separe batidas, '
|
|
889
890
|
+ 'escreva a cena de motion de cada ideia (componentes/x.jsx, recebe {el, t, fps}; el.dados traz largura/altura do espaço livre, '
|
|
890
|
-
+ 'palavras com t, destaque, cores e fontes) ou use um efeito de fábrica pelo nome (estudio_midia acao:"efeitos"), '
|
|
891
|
+
+ 'palavras com t, destaque, cores e fontes; a LEGENDA da fala já está no quadro, então a cena NÃO escreve a fala: desenha a ideia) ou use um efeito de fábrica pelo nome (estudio_midia acao:"efeitos"), '
|
|
891
892
|
+ 'e chame de novo com batidas:[{de, ate, rosto, cena?, destaque?, canto?, dados?}].',
|
|
892
893
|
};
|
|
893
894
|
}
|
|
@@ -1177,6 +1178,35 @@ async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
|
1177
1178
|
}
|
|
1178
1179
|
}
|
|
1179
1180
|
|
|
1181
|
+
/* A VOZ DO VOICESTUDIO (15/10): o app local fala o roteiro inteiro, e o resto
|
|
1182
|
+
é o caminho do Gemini — volume de Reels, a transcrição para achar as partes.
|
|
1183
|
+
Fechado ou falhando, devolve ok:false e a cadeia segue para o Gemini. */
|
|
1184
|
+
async function narrarComVoiceStudio(lista, mp3, { voz } = {}) {
|
|
1185
|
+
if (process.env.PRIMOCODE_SEM_VOICESTUDIO) return { ok: false, indisponivel: true, error: 'voicestudio desligado' };
|
|
1186
|
+
try {
|
|
1187
|
+
const texto = lista.map((x) => x.texto).join('\n\n');
|
|
1188
|
+
// A voz do Gemini ("Kore", "Charon") não existe lá: só um perfil do próprio app.
|
|
1189
|
+
const perfil = /^vs:/i.test(String(voz || '')) ? String(voz).slice(3) : undefined;
|
|
1190
|
+
const r = await voicestudio.falar(texto, { voz: perfil });
|
|
1191
|
+
if (!r.ok) return r;
|
|
1192
|
+
const bruto = mp3.replace(/\.mp3$/, '-vs.mp3');
|
|
1193
|
+
fs.writeFileSync(bruto, r.audio);
|
|
1194
|
+
const c = await video.rodar(video.ffmpeg(), ['-y', '-i', bruto, '-af', 'loudnorm=I=-14:TP=-1.5:LRA=9', '-c:a', 'libmp3lame', '-q:a', '3', mp3], { timeout: 120000 });
|
|
1195
|
+
try { fs.unlinkSync(bruto); } catch { /* fica */ }
|
|
1196
|
+
if (!c.ok) return { ok: false, error: 'voicestudio: não consegui converter a voz' };
|
|
1197
|
+
const med = await video.medir(mp3);
|
|
1198
|
+
const chars = lista.reduce((n, x) => n + x.texto.length, 0);
|
|
1199
|
+
if (med.segundos < chars / 22) return { ok: false, error: `voicestudio: a voz veio cortada (${med.segundos.toFixed(1)} s para ${chars} caracteres)` };
|
|
1200
|
+
const cfg = require('../config').loadConfig();
|
|
1201
|
+
const tr = await require('../api').requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
1202
|
+
const palavras = (tr && tr.palavras) || [];
|
|
1203
|
+
if (!palavras.length) return { ok: false, error: 'voicestudio: sem a transcrição da voz para achar as partes' };
|
|
1204
|
+
return { ok: true, voz: `voicestudio ${perfil || 'default'}`, segundos: med.segundos, partes: alinharPartes(lista, palavras, med.segundos) };
|
|
1205
|
+
} catch (e) {
|
|
1206
|
+
return { ok: false, error: 'voicestudio: ' + e.message };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1180
1210
|
/* Onde estão os rostos em cada foto (studio/rostos.py, pelo uv). Guardado no
|
|
1181
1211
|
projeto: a mesma foto não é analisada duas vezes. */
|
|
1182
1212
|
async function acharRostos(p, srcs) {
|
|
@@ -1270,7 +1300,9 @@ async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
|
1270
1300
|
if (fs.existsSync(cm) && fs.existsSync(cj)) { fs.copyFileSync(cm, mp3); return { ok: true, ...JSON.parse(fs.readFileSync(cj, 'utf8')) }; }
|
|
1271
1301
|
// A voz local pedida (voz: 'kokoro'): direto nela, sem gastar a cota do Gemini.
|
|
1272
1302
|
if (kokoro.querKokoro(voz)) return kokoro.narrar(lista, mp3, { voz, uv: acharUv() });
|
|
1273
|
-
|
|
1303
|
+
// O VoiceStudio local primeiro (15/10); fechado ou falhando, o Gemini.
|
|
1304
|
+
let v = await narrarComVoiceStudio(lista, mp3, { voz });
|
|
1305
|
+
if (!v.ok) v = await narrarComGemini(lista, mp3, { voz, direcao });
|
|
1274
1306
|
if (v.ok) { try { fs.mkdirSync(dir, { recursive: true }); fs.copyFileSync(mp3, cm); fs.writeFileSync(cj, JSON.stringify({ voz: v.voz, segundos: v.segundos, partes: v.partes })); } catch { /* sem cache */ } }
|
|
1275
1307
|
return v;
|
|
1276
1308
|
}
|
|
@@ -1289,6 +1321,7 @@ async function produzirNecessidade(p, roteiro, pedido, provedores) {
|
|
|
1289
1321
|
direção dele), o Kokoro local e o Edge. A falha de uma passa para a próxima. */
|
|
1290
1322
|
function provedoresDeVoz(lista, mp3, arq, voz, vozEdge, { soEdge = false, gemini, escolha } = {}) {
|
|
1291
1323
|
const todos = {
|
|
1324
|
+
voicestudio: () => narrarComVoiceStudio(lista, mp3, { voz }),
|
|
1292
1325
|
gemini: () => gemini(),
|
|
1293
1326
|
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
1294
1327
|
edge: () => {
|
|
@@ -1307,7 +1340,7 @@ function provedoresDeVoz(lista, mp3, arq, voz, vozEdge, { soEdge = false, gemini
|
|
|
1307
1340
|
/* A VOZ DE RESERVA, quando o Gemini falha: o Kokoro (local, sem cota) e
|
|
1308
1341
|
depois o Edge. A falha de uma passa para a próxima, nunca derruba. */
|
|
1309
1342
|
function narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge = false, semKokoro = false } = {}) {
|
|
1310
|
-
const passos = kokoro.ordem(voz, process.env, { soEdge }).filter((n) => n !== 'gemini' && !(semKokoro && n === 'kokoro'));
|
|
1343
|
+
const passos = kokoro.ordem(voz, process.env, { soEdge }).filter((n) => n !== 'gemini' && n !== 'voicestudio' && !(semKokoro && n === 'kokoro'));
|
|
1311
1344
|
return kokoro.corrente(passos, {
|
|
1312
1345
|
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
1313
1346
|
edge: () => rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz', kokoro.vozNoEdge(voz, vozEdge)], { timeout: 10 * 60000 }),
|
|
@@ -295,6 +295,21 @@ function geometria(a, b, L, P) {
|
|
|
295
295
|
for (let i = 0; i < N; i++) { const q = -2.3 + (i / (N - 1)) * Math.PI * 2 * 1.1; const e = 1 + (i / N) * 0.07; pts.push([ccx + Math.cos(q) * rx * e, ccy + Math.sin(q) * ry * e]); }
|
|
296
296
|
return { pts, larg: Math.max(4 * base, cx.s * 0.03), alfa: 1, frente: true, cx: ccx, cy: ccy };
|
|
297
297
|
}
|
|
298
|
+
if (f === 'visto') {
|
|
299
|
+
/* O ✓ de caneta (15/10, as referências): depois do círculo, ao lado da
|
|
300
|
+
palavra, em dois traços; se não cabe à direita, encosta na margem. */
|
|
301
|
+
const pw = alvoPalavra();
|
|
302
|
+
const s = cx.s, cy0 = cx.y + cx.h * 0.5;
|
|
303
|
+
const A = [pw.x + pw.w + s * 0.16, cy0], B = [pw.x + pw.w + s * 0.27, cy0 + s * 0.2], C = [pw.x + pw.w + s * 0.52, cy0 - s * 0.3];
|
|
304
|
+
const dx = Math.min(0, P.largura - 24 * base - C[0]);
|
|
305
|
+
const q = 0.3;
|
|
306
|
+
for (let i = 0; i < N; i++) {
|
|
307
|
+
const k = i / (N - 1);
|
|
308
|
+
const [p0, p1, u] = k < q ? [A, B, k / q] : [B, C, (k - q) / (1 - q)];
|
|
309
|
+
pts.push([lerp(p0[0], p1[0], u) + dx, lerp(p0[1], p1[1], u)]);
|
|
310
|
+
}
|
|
311
|
+
return { pts, larg: Math.max(5 * base, s * 0.045), alfa: 1, frente: true, tampa: 'round', cx: B[0] + dx, cy: B[1] };
|
|
312
|
+
}
|
|
298
313
|
if (f === 'barra') {
|
|
299
314
|
// O vão da barra é o respiro do vídeo (margem.js): colada, ela riscava a primeira letra.
|
|
300
315
|
// A deriva da vida abre as letras até 3% da linha para fora: a barra recua junto.
|
package/lib/estudio/kokoro.js
CHANGED
|
@@ -94,7 +94,9 @@ async function narrar(lista, mp3, { voz, uv, espeak = acharEspeak(), executar =
|
|
|
94
94
|
roteiro pula as outras. */
|
|
95
95
|
function ordem(voz, env = process.env, { soEdge = false } = {}) {
|
|
96
96
|
if (soEdge) return ['edge'];
|
|
97
|
-
|
|
97
|
+
if (querKokoro(voz, env)) return ['kokoro', 'edge'];
|
|
98
|
+
// O VoiceStudio local vem primeiro (15/10); fechado, a cadeia segue para o Gemini na hora.
|
|
99
|
+
return env.PRIMOCODE_SEM_VOICESTUDIO ? ['gemini', 'edge', 'kokoro'] : ['voicestudio', 'gemini', 'edge', 'kokoro'];
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/* A voz do Gemini no Edge: o narrar.py só conhece antonio, francisca e
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -108,9 +108,9 @@ import { KIT, CURVAS_KIT, Filtros } from './kit';
|
|
|
108
108
|
import { COMPONENTES } from './componentes';
|
|
109
109
|
|
|
110
110
|
/* ── SUAVIZAÇÃO ──────────────────────────────────────────────────────────
|
|
111
|
-
* Cada curva é uma função de 0..1 em 0..1. "
|
|
112
|
-
* movimento linear
|
|
113
|
-
*
|
|
111
|
+
* Cada curva é uma função de 0..1 em 0..1. A "editorial" é o padrão (15/10):
|
|
112
|
+
* movimento linear denuncia máquina, e o entra-e-sai devagar da "suave" lia
|
|
113
|
+
* como fade; as referências atacam rápido e desaceleram forte. */
|
|
114
114
|
const CURVAS = {
|
|
115
115
|
linear: (t) => t,
|
|
116
116
|
suave: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
|
|
@@ -143,7 +143,9 @@ function valorEm(trilha, t, padrao) {
|
|
|
143
143
|
const a = trilha[i], b = trilha[i + 1];
|
|
144
144
|
if (t >= a.t && t <= b.t) {
|
|
145
145
|
const bruto = b.t === a.t ? 1 : (t - a.t) / (b.t - a.t);
|
|
146
|
-
|
|
146
|
+
/* (15/10, as referências) Sem curva dita, a editorial: ataque rápido e
|
|
147
|
+
desaceleração forte. A "suave" (entra e sai devagar) lia como fade. */
|
|
148
|
+
const curva = CURVAS[b.e || a.e || 'editorial'] || CURVAS.editorial;
|
|
147
149
|
const k = curva(bruto);
|
|
148
150
|
/* Texto no lugar do número: é COR ("#ff6a00") ou CAMINHO (o "d" de um
|
|
149
151
|
SVG). A cor mistura; o caminho se deforma (morfar.js): a bolinha
|
package/lib/estudio/revisor.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* avisa a mesma sequência de elementos em 4+ cenas seguidas (cara de template)
|
|
17
17
|
* avisa imagem sem a pessoa pedir, ou imagem em mais da metade das cenas
|
|
18
18
|
* conserta efeito sonoro demais (mais de 1 a cada 1,5 s) → ficam os mais altos
|
|
19
|
+
* avisa texto pequeno demais para o celular (< 2% da altura no 9:16)
|
|
20
|
+
* avisa texto com contraste abaixo de 3:1 contra o fundo da cena
|
|
19
21
|
* Puro: o plano entra, o plano consertado e a lista saem.
|
|
20
22
|
*/
|
|
21
23
|
|
|
@@ -34,6 +36,62 @@ const palavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
|
34
36
|
// Imagem ou vídeo DE FORA: o vídeo da própria gravação (deOrigem) e o recorte da pessoa não contam.
|
|
35
37
|
const imagemDeFora = (el) => el && (el.tipo === 'imagem' || (el.tipo === 'video' && el.deOrigem == null && !/pessoa-/.test(el.src || '')));
|
|
36
38
|
|
|
39
|
+
/* O TEXTO QUE NÃO SE LÊ (15/10, as referências): rótulo de 1,6% da altura
|
|
40
|
+
("NO COADOR") e laranja sobre bege. Nas referências a palavra principal
|
|
41
|
+
ocupa 55–85% da largura e a segunda voz ~5% da altura, sempre em contraste
|
|
42
|
+
duro. Contraste é o da WCAG para texto grande (3:1). */
|
|
43
|
+
const hex6 = (c) => {
|
|
44
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());
|
|
45
|
+
if (!m) return null;
|
|
46
|
+
const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];
|
|
47
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
48
|
+
};
|
|
49
|
+
const luminancia = (c) => {
|
|
50
|
+
const v = hex6(c);
|
|
51
|
+
if (!v) return null;
|
|
52
|
+
const [r, g, b] = v.map((x) => (x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)));
|
|
53
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
54
|
+
};
|
|
55
|
+
function contraste(a, b) {
|
|
56
|
+
const la = luminancia(a), lb = luminancia(b);
|
|
57
|
+
if (la == null || lb == null) return null;
|
|
58
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
59
|
+
}
|
|
60
|
+
const corDoFundo = (f) => (typeof f === 'string' ? f : f && (f.valor || f.cor)) || null;
|
|
61
|
+
|
|
62
|
+
function revisarLeitura(plano, avisos) {
|
|
63
|
+
const W = Number(plano.formato && plano.formato.largura) || 1920, H = Number(plano.formato && plano.formato.altura) || 1080;
|
|
64
|
+
const piso = H * (W > H ? 0.024 : 0.019);
|
|
65
|
+
const curto = (t) => String(t || '').slice(0, 24);
|
|
66
|
+
const pequenos = [], apagados = [];
|
|
67
|
+
(plano.cenas || []).forEach((c, i) => {
|
|
68
|
+
const fundo = corDoFundo(c.fundo) || plano.fundoGeral;
|
|
69
|
+
for (const e of c.elementos || []) {
|
|
70
|
+
if (!e) continue;
|
|
71
|
+
if (e.tipo === 'texto') {
|
|
72
|
+
const tam = Number(e.tamanho || (e.estilo && e.estilo.tamanho));
|
|
73
|
+
if (tam && tam < piso) pequenos.push(`cena ${i + 1} "${curto(e.texto)}" ${r2((tam / H) * 100)}%`);
|
|
74
|
+
const k = contraste(e.cor || (e.estilo && e.estilo.cor), fundo);
|
|
75
|
+
if (k != null && k < 3) apagados.push(`cena ${i + 1} "${curto(e.texto)}" ${r2(k)}:1`);
|
|
76
|
+
}
|
|
77
|
+
if (e.tipo === 'kit' && e.peca === 'gerativo' && e.partitura) {
|
|
78
|
+
const Hp = Number(e.partitura.altura) || H;
|
|
79
|
+
const pisoP = Hp * ((Number(e.partitura.largura) || W) > Hp ? 0.024 : 0.019);
|
|
80
|
+
for (const b of e.partitura.batidas || []) {
|
|
81
|
+
const cores = b.cores || {};
|
|
82
|
+
for (const l of b.linhas || []) {
|
|
83
|
+
if (l.tamanho && l.tamanho < pisoP) pequenos.push(`batida ${r2(b.de)}s "${curto(l.texto)}" ${r2((l.tamanho / Hp) * 100)}%`);
|
|
84
|
+
}
|
|
85
|
+
const k = contraste(cores.texto, cores.fundo);
|
|
86
|
+
if (k != null && k < 3) apagados.push(`batida ${r2(b.de)}s ${cores.texto} sobre ${cores.fundo} ${r2(k)}:1`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (pequenos.length) avisos.push(`texto pequeno demais para o celular (piso ~2% da altura): ${pequenos.slice(0, 3).join('; ')}${pequenos.length > 3 ? ` e mais ${pequenos.length - 3}` : ''}`);
|
|
92
|
+
if (apagados.length) avisos.push(`texto com contraste abaixo de 3:1 contra o fundo: ${apagados.slice(0, 3).join('; ')}${apagados.length > 3 ? ` e mais ${apagados.length - 3}` : ''}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
37
95
|
function revisar(plano, { semImagem = false } = {}) {
|
|
38
96
|
const consertos = [], avisos = [];
|
|
39
97
|
const cenas = (plano && plano.cenas) || [];
|
|
@@ -114,6 +172,9 @@ function revisar(plano, { semImagem = false } = {}) {
|
|
|
114
172
|
if (semImagem && comImagem) avisos.push(`${comImagem} cena(s) com imagem de fora, mas a pessoa não pediu imagem`);
|
|
115
173
|
else if (cenas.length >= 4 && comImagem / cenas.length > 0.5) avisos.push(`imagem em ${comImagem} de ${cenas.length} cenas — a imagem é um momento, não o vídeo inteiro`);
|
|
116
174
|
|
|
175
|
+
// 7. O texto que não se lê: pequeno demais ou sem contraste.
|
|
176
|
+
revisarLeitura(plano, avisos);
|
|
177
|
+
|
|
117
178
|
return { plano, consertos, avisos };
|
|
118
179
|
}
|
|
119
180
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voicestudio.js — a voz local do VoiceStudio, a primeira da cadeia.
|
|
3
|
+
*
|
|
4
|
+
* "achei isso: https://voicestudio.sh/ podemos usar pra narração do motion,
|
|
5
|
+
* ele entra primeiro na cadeia e depois, se falhar, o Gemini" (Joel, 15/10)
|
|
6
|
+
*
|
|
7
|
+
* O VoiceStudio é um app de desktop (AGPL-3.0) que a pessoa instala e deixa
|
|
8
|
+
* aberto; ele serve uma API no formato da OpenAI em localhost:3900, sem chave
|
|
9
|
+
* no loopback (docs/api-auth.md do repo dele). O PrimoCode só CHAMA a API —
|
|
10
|
+
* não copia nem distribui o código dele, então a licença não pega.
|
|
11
|
+
*
|
|
12
|
+
* Fechado, a porta recusa na hora e a cadeia segue para o Gemini sem esperar.
|
|
13
|
+
* Puro de rede: http do Node, sem dependência.
|
|
14
|
+
*
|
|
15
|
+
* VOICESTUDIO_URL outro endereço (padrão http://127.0.0.1:3900)
|
|
16
|
+
* VOICESTUDIO_VOZ o perfil de voz do app (padrão "default")
|
|
17
|
+
* VOICESTUDIO_MODELO o motor (padrão "omnivoice")
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const http = require('http');
|
|
23
|
+
const https = require('https');
|
|
24
|
+
|
|
25
|
+
const URL_PADRAO = 'http://127.0.0.1:3900';
|
|
26
|
+
const LIMITE = 4096; // o input máximo do /v1/audio/speech
|
|
27
|
+
|
|
28
|
+
function base(url) { return String(url || process.env.VOICESTUDIO_URL || URL_PADRAO).replace(/\/+$/, ''); }
|
|
29
|
+
|
|
30
|
+
function pedir(metodo, url, { corpo = null, timeout = 800 } = {}) {
|
|
31
|
+
return new Promise((ok) => {
|
|
32
|
+
let u;
|
|
33
|
+
try { u = new URL(url); } catch { ok({ ok: false, error: 'endereço inválido' }); return; }
|
|
34
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
35
|
+
const dados = corpo ? Buffer.from(JSON.stringify(corpo)) : null;
|
|
36
|
+
const req = lib.request(u, {
|
|
37
|
+
method: metodo,
|
|
38
|
+
headers: dados ? { 'content-type': 'application/json', 'content-length': dados.length } : {},
|
|
39
|
+
}, (res) => {
|
|
40
|
+
const partes = [];
|
|
41
|
+
res.on('data', (c) => partes.push(c));
|
|
42
|
+
res.on('end', () => ok({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, corpo: Buffer.concat(partes) }));
|
|
43
|
+
});
|
|
44
|
+
req.setTimeout(timeout, () => req.destroy(new Error('tempo esgotado')));
|
|
45
|
+
req.on('error', (e) => ok({ ok: false, error: e.message, recusou: /ECONNREFUSED|EADDRNOTAVAIL|ENOTFOUND/.test(e.code || e.message) }));
|
|
46
|
+
if (dados) req.write(dados);
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** O app está aberto? Pergunta rápida: fechado, a porta recusa na hora. */
|
|
52
|
+
async function disponivel({ url } = {}) {
|
|
53
|
+
const r = await pedir('GET', base(url) + '/v1/audio/voices', { timeout: 800 });
|
|
54
|
+
return r.ok === true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Um texto em áudio (mp3). Nunca lança: devolve {ok, audio} ou {ok:false, error}.
|
|
59
|
+
* O primeiro pedido carrega o modelo no app, por isso o tempo é longo.
|
|
60
|
+
*/
|
|
61
|
+
async function falar(texto, { url, voz, modelo, idioma = 'pt', timeout = 10 * 60000 } = {}) {
|
|
62
|
+
const input = String(texto || '').trim();
|
|
63
|
+
if (!input) return { ok: false, error: 'voicestudio: texto vazio' };
|
|
64
|
+
if (input.length > LIMITE) return { ok: false, error: `voicestudio: o texto passa de ${LIMITE} caracteres` };
|
|
65
|
+
if (!(await disponivel({ url }))) return { ok: false, indisponivel: true, error: 'voicestudio: o app não está aberto' };
|
|
66
|
+
const r = await pedir('POST', base(url) + '/v1/audio/speech', {
|
|
67
|
+
corpo: {
|
|
68
|
+
model: modelo || process.env.VOICESTUDIO_MODELO || 'omnivoice',
|
|
69
|
+
input,
|
|
70
|
+
voice: voz || process.env.VOICESTUDIO_VOZ || 'default',
|
|
71
|
+
response_format: 'mp3',
|
|
72
|
+
language: idioma,
|
|
73
|
+
},
|
|
74
|
+
timeout,
|
|
75
|
+
});
|
|
76
|
+
if (!r.ok) return { ok: false, error: `voicestudio: ${r.error || 'status ' + r.status}${r.corpo && r.corpo.length < 400 ? ' ' + r.corpo.toString() : ''}` };
|
|
77
|
+
if (!r.corpo || r.corpo.length < 8) return { ok: false, error: 'voicestudio: áudio vazio' };
|
|
78
|
+
return { ok: true, audio: r.corpo };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { disponivel, falar, URL_PADRAO, LIMITE };
|
package/lib/video/auto.js
CHANGED
|
@@ -254,9 +254,22 @@ function juntarSilencios(sil) {
|
|
|
254
254
|
return out;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/* O que vem depois do auto. Sem pedido falado (15/10: um vídeo do Instagram,
|
|
258
|
+
"edita pra mim?"), o plano é vazio e só a legenda entra — e o agente
|
|
259
|
+
exportava assim. Aí a edição volta para ele decidir, com o cardápio. */
|
|
260
|
+
function depois(passos) {
|
|
261
|
+
if ((passos || []).length) return { semPedido: false, texto: 'inspect para OLHAR os momentos; export renderiza (o 3D sai no export)' };
|
|
262
|
+
return {
|
|
263
|
+
semPedido: true,
|
|
264
|
+
texto: 'A gravação não tem pedido falado: só a legenda entrou, e isso ainda não é a edição. '
|
|
265
|
+
+ 'Se a pessoa pediu para editar, a edição é sua: leia a fala (get_transcript) e decida — por exemplo cortar os silêncios (remove_silence), '
|
|
266
|
+
+ 'zoom nos momentos fortes (auto_zoom), apoio quando ela cita uma coisa (add_clips), cor (grade). Olhe com inspect antes do export.',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
257
270
|
const OPS = {
|
|
258
271
|
auto: {
|
|
259
|
-
uso: 'auto {asset?,dryRun?} — O VÍDEO É O PEDIDO: lê o que a pessoa FALA na gravação ("separe em camadas", "coloca o logo do Claude aqui", "com o estalar de dedos me coloca numa praia", "junta e vira um cérebro", "cortou o silêncio", "o vídeo acaba") e aplica cada efeito no momento em que ela diz, ancorado na palavra, na mão, no estalo e no juntar das mãos; legenda sempre.
|
|
272
|
+
uso: 'auto {asset?,dryRun?} — O VÍDEO É O PEDIDO: lê o que a pessoa FALA na gravação ("separe em camadas", "coloca o logo do Claude aqui", "com o estalar de dedos me coloca numa praia", "junta e vira um cérebro", "cortou o silêncio", "o vídeo acaba") e aplica cada efeito no momento em que ela diz, ancorado na palavra, na mão, no estalo e no juntar das mãos; legenda sempre. Serve quando a gravação TRAZ pedidos falados. Sem pedido falado (vídeo de outra pessoa, ou só fala), ele põe só a legenda, e o resto da edição é decisão sua',
|
|
260
273
|
async run(ctx, a) {
|
|
261
274
|
const T = require('./timeline.js');
|
|
262
275
|
const C = require('./camadas.js');
|
|
@@ -317,13 +330,15 @@ const OPS = {
|
|
|
317
330
|
if (L.position && ids.length) await roda('style_captions', { ids, position: L.position }, desde !== null ? 'em cima da cabeça, desde o momento dito' : 'em cima da cabeça');
|
|
318
331
|
}
|
|
319
332
|
const falhou = feitos.filter((f) => !f.ok);
|
|
333
|
+
const prox = depois(plano.passos);
|
|
320
334
|
return { ok: !falhou.length, feitos, tookS: Math.round((Date.now() - inicio) / 100) / 10,
|
|
321
335
|
...(falhou.length ? { error: falhou.map((f) => `${f.op}: ${f.error}`).join('; ') } : {}),
|
|
322
|
-
|
|
336
|
+
...(prox.semPedido ? { semPedido: true } : {}),
|
|
337
|
+
depois: prox.texto };
|
|
323
338
|
},
|
|
324
339
|
},
|
|
325
340
|
};
|
|
326
341
|
|
|
327
342
|
const EX = { auto: '{}' };
|
|
328
343
|
|
|
329
|
-
module.exports = { planejar, frases, normal, fundoPedido, juntarSilencios, OPS, EX };
|
|
344
|
+
module.exports = { planejar, frases, normal, fundoPedido, juntarSilencios, depois, OPS, EX };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.13.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -7,43 +7,35 @@ autor: PrimoCode
|
|
|
7
7
|
Não existe template. Cada propriedade deste vídeo é uma decisão sua, e a
|
|
8
8
|
primeira decisão é **não repetir o vídeo anterior**.
|
|
9
9
|
|
|
10
|
-
## O que denuncia
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`rotateX`/`translateZ`, ou three, para 3D de verdade.
|
|
35
|
-
|
|
36
|
-
**Ritmo.** Informação simples 1,5–2,5 s; frase para ler 3–4 s; fecho 2 s.
|
|
37
|
-
|
|
38
|
-
**Silêncio visual.** Um quadro com uma frase e mais nada vale mais que um com
|
|
39
|
-
seis elementos. Espaço vazio é composição, não desperdício.
|
|
10
|
+
## O que denuncia IA (nunca)
|
|
11
|
+
|
|
12
|
+
Gradiente roxo-azul ou sem relação com o assunto; tudo centralizado; uma fonte
|
|
13
|
+
só; texto que só faz `opacidade 0→1`; ícone genérico ao lado de cada frase;
|
|
14
|
+
tempo e transição iguais em todas as cenas; letra cortada ou embaralhada na troca.
|
|
15
|
+
|
|
16
|
+
## A régua (medida nas referências que o Joel aprovou)
|
|
17
|
+
|
|
18
|
+
Números, não estilo pronto: o estilo continua seu.
|
|
19
|
+
|
|
20
|
+
- **Escala:** título de 1 a 3 palavras ocupando 55–85% da largura. Nada abaixo de 2% da altura.
|
|
21
|
+
- **Ritmo:** uma ideia por cena, 1 a 2 s (frase para ler 2–3 s). Com trilha, corte na batida.
|
|
22
|
+
- **Entrada:** máscara (sobe de baixo, recorte reto), palavra por palavra de desfocado
|
|
23
|
+
para nítido (0,15–0,3 s cada) ou esticando com borrão; `spring` ou ataque rápido e
|
|
24
|
+
freio forte. **Saída:** a frase sai inteira (desliza, recua, corte seco).
|
|
25
|
+
- **Cor:** fundo chapado, 1 tinta + 1 acento, contraste de 3:1 ou mais; até 5 cores.
|
|
26
|
+
- **Segunda voz:** o apoio é menor (~4% da altura), na segunda fonte, e chega 0,25–0,5 s depois.
|
|
27
|
+
- **À mão:** na palavra que é a tese, um círculo de caneta que se desenha (com ✓ ou seta).
|
|
28
|
+
- **Acento vivo e câmera:** um ponto ou forma acompanha o título e vira o elemento da
|
|
29
|
+
próxima cena (a transição é o elemento). Zoom lento de 1.0 a 1.08 e parallax dão vida.
|
|
30
|
+
- **Produto:** a interface é código (cartão, toggle, cursor que clica com resultado, número que conta).
|
|
31
|
+
- **Explicar:** uma cena ensina desenhando (curva, gráfico, contorno com âncoras).
|
|
32
|
+
- **Fecho:** a marca sozinha, centralizada, 1,5–2 s. Moldura de ferramenta (cantos,
|
|
33
|
+
timecode) só em reel de marca ou tecnologia: `moldura:"cantos"`.
|
|
40
34
|
|
|
41
35
|
## O caminho: você escreve cada cena
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
o gráfico que mostra a ideia, a palavra gigante que se transforma. Quem
|
|
46
|
-
desenha é você, em React.
|
|
37
|
+
Cada cena é UMA ideia desenhada para a fala (a interface montando, o número
|
|
38
|
+
que cresce, o objeto 3D, a palavra gigante). Quem desenha é você, em React.
|
|
47
39
|
|
|
48
40
|
1. `estudio_projeto` (criar). A marca tem site? `estudio_site` mede cores e fontes.
|
|
49
41
|
2. `estudio_midia acao:"estilos"`: monte a identidade pelo assunto em
|
|
@@ -62,23 +54,13 @@ desenha é você, em React.
|
|
|
62
54
|
arquivo e mande o roteiro de novo.
|
|
63
55
|
6. `estudio_render`. A voz, a trilha, os sons e o fundo são do Estúdio.
|
|
64
56
|
|
|
65
|
-
**Variedade:** `estudio_midia acao:"efeitos"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Parte sem `componente` cai no gerativo (a tipografia anima sozinha): é a
|
|
69
|
-
reserva, não o objetivo. Sem imagem é o padrão (foto só se pedirem).
|
|
70
|
-
|
|
71
|
-
## Cor e tipo
|
|
72
|
-
|
|
73
|
-
A paleta sai do ASSUNTO, no `estiloVisual` (um nome do cardápio ou 5 cores
|
|
74
|
-
suas). Se a marca existe, as cores medidas pelo `estudio_site` mandam — não
|
|
75
|
-
as que você imagina que ela usa.
|
|
57
|
+
**Variedade:** `estudio_midia acao:"efeitos"`, adapte e use como `componente`; uma
|
|
58
|
+
técnica nova por vídeo. Parte sem `componente` cai no gerativo (a reserva). Foto só se pedirem.
|
|
76
59
|
|
|
77
|
-
|
|
60
|
+
## Cor, tipo e conferência
|
|
78
61
|
|
|
79
|
-
|
|
62
|
+
A paleta sai do ASSUNTO (`estiloVisual`); a marca existe? As cores medidas pelo
|
|
63
|
+
`estudio_site` mandam. Duas fontes: display para o que grita, neutra para o que se lê.
|
|
80
64
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- Leia o seu próprio plano e pergunte: em que este vídeo é diferente do
|
|
84
|
-
último que eu fiz? Se não houver resposta, refaça antes de renderizar.
|
|
65
|
+
Antes do render: os tempos batem com a narração MEDIDA, nenhuma cena repete a
|
|
66
|
+
composição da anterior, e este vídeo é diferente do último. Senão, refaça.
|