primocode 10.9.1 → 10.10.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 +2 -2
- package/lib/act.js +2 -0
- package/lib/catalogo.js +2 -1
- package/lib/estudio/gerativo.js +85 -3
- package/lib/estudio/index.js +84 -2
- package/lib/estudio/kit/gerativo.jsx +22 -2
- package/lib/estudio/referencias.js +9 -2
- package/lib/tools.js +4 -2
- package/lib/ui.js +14 -1
- package/lib/video/cor.js +104 -0
- package/lib/video/filters.js +3 -0
- package/lib/video/ops.js +31 -0
- package/package.json +2 -2
- package/studio/edge.py +5 -0
- package/studio/narrar.py +16 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.
|
|
1
|
+
# PrimoCode v10.10.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.10.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/act.js
CHANGED
|
@@ -939,6 +939,8 @@ async function runAct(userPrompt, opts) {
|
|
|
939
939
|
+ 'Senão: '
|
|
940
940
|
+ 'rode `primocode video help` UMA vez (lista as operações), depois import → remove_silence/remove_words → add_captions → inspect (olhe o png) → export. '
|
|
941
941
|
+ 'Cada edição devolve um delta curto e tem undo: edite sem pedir licença a cada passo.\n'
|
|
942
|
+
/* "deixar os vídeos cinematográficos… fazer um color grading sozinha" (13/10). */
|
|
943
|
+
+ 'CINEMATOGRÁFICO: `primocode video grade --json \'{}\'` mede a imagem e faz o color grading de cinema sozinho (look:cinema|quente|noite|natural|pb, intensidade); use antes do export quando a pessoa quer o vídeo bonito, e OLHE o inspect.\n'
|
|
942
944
|
+ 'EFEITOS que seguem a pessoa: separate_layers (recorte + momento 3D), replace_background (fundo novo no estalo), '
|
|
943
945
|
+ 'attach_3d (logo 3D na mão, segue a mão), merge_on_join (mãos juntas → fusão → cérebro), cut_silence_at (silêncio depois do estalo), '
|
|
944
946
|
+ 'style_captions (cor, legenda em cima da cabeça) — detalhe em `primocode video help efeitos`. Atalho: `primocode video do --json \'{"pedido":"<o pedido em português>"}\'`.' });
|
package/lib/catalogo.js
CHANGED
|
@@ -93,7 +93,8 @@ const GRUPOS = [
|
|
|
93
93
|
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
94
94
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). O texto da TELA sai da FALA: o título usa palavras que a fala diz, cedo na frase (a tela entra quando a voz diz; palavra que a voz não diz deixa a tela esperando). '
|
|
95
95
|
+ 'visual:{titulo,texto,paleta} (hibrido, cinema, impacto): fonte e cor do cardápio (estudio_midia acao:fontes); sem visual, o PrimoCode sorteia sem repetir os últimos vídeos. '
|
|
96
|
-
+ '
|
|
96
|
+
+ 'componente:"componentes/x.jsx" numa parte = a cena que VOCÊ escreveu em React (a voz, a trilha e os sons seguem do Estúdio). '
|
|
97
|
+
+ 'estilo (deixe VAZIO: o estilo da casa, o gerativo com as cores da marca, é o ponto de partida; outro só se a pessoa pedir pelo nome): hibrido (SÓ quando a pessoa pediu imagem: foto e vídeo reais + o motion por cima; dê busca:"termo em inglês" só nas partes em que a imagem agrega — a COISA que a frase nomeia, 2 a 4 palavras, não a frase inteira; rota:"tipografia" em número/citação, "produto" quando a frase fala do produto; ancora:"palavra" — o clipe troca 0,2 s depois dela) | gerativo (= motion: sem template — a tipografia anima nos eixos da fonte numa linha do tempo contínua, composição, fonte, cor e som sorteados do roteiro; modelo3d fone|celular|notebook|relogio|garrafa|lata|caixa: o objeto gira em 3D numa batida — ponha quando o vídeo é de um produto ou objeto físico; ideia abstrata, história ou explicação: deixe vazio; o vídeo tem UMA identidade (2 chãos, 1 acento, 2 fontes) e o fundo só troca na virada de seção — se o roteiro tem atos, dê secao:"nome curto" nas partes, senão o PrimoCode divide; moldura cantos|completa: o quadro fixo do reel (cantos, o assunto no alto, "01 · seção", o tempo) — SÓ quando a pessoa pedir; vazio = sem moldura (ela entrega que o vídeo foi gerado); heroi em cada parte: o desenho da batida além do texto, tirado do CONTEÚDO — um ícone 3D RENDERIZADO da biblioteca ("fluent3d:hot beverage", "fluent3d:money bag"; procure com estudio_midia acao:icones3d — é o que mais parece 3D de verdade), uma animação pronta ("noto:1f525"; acao:animacoes), um ícone do Iconify do objeto que a fala cita ("mdi:bread-slice", "mdi:qrcode"; acao:icones), um 3D do kit (raio|relogio|check|balao|escudo|coracao|estrela|sol|play|engrenagem|cadeado|barras|fone|onda|celular|seta|bateria) ou "nenhum" quando a tipografia basta; forma abstrata (explosao|poligono|esfera|toro|truchet|aneis|grade|rastro) só em assunto abstrato ou de tecnologia — numa conversa de padaria, nunca; ambiente limpo|luz|bokeh e acento traco|viagem (o ponto que viaja entre as batidas): vazio, o PrimoCode escolhe com memória e o bokeh quase não sai fora de tecnologia) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
97
98
|
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | fluido (minimalista: uma ideia por batida, a forma de uma vira a próxima, sem corte; paleta noite|breu|preto|cobalto (escuras) ou lavanda|creme|laranja|branco (claras) — escolha pelo assunto e NÃO repita a do vídeo anterior; vazio, o PrimoCode escolhe; fontesFluido aprovado|editorial|revista|carater|tecnico|classico, vazio gira) | impacto | cinema | personagem (a ANIMAÇÃO 3D de verdade, o vídeo GRAVADO em 3D no visual de blocos/voxel de render de cinema — é a escolha para animação 3D, personagem, bicho, boneco, avatar, mascote, podcast, entrevista ou alguém FALANDO em 3D; cada parte é UMA fala {falante, texto, emocao?, destaque?, acao?, cenario?}, sem tipo). NADA É FIXO, monte o que a pessoa pediu: cena:"texto livre do lugar" (praia, cidade, floresta, parque, fazenda, neve, deserto, espaço, fundo do mar, escritório, sala, cozinha, escola, palco… + hora: "à noite", "ao pôr do sol") ou cena:{lugar, hora, elementos:["palmeira","barraca","violão",…]} — elemento fora do catálogo vira ícone 3D em blocos; parte.cenario muda o lugar a partir daquela fala. Até 4 falantes; personagens:{"nome":{tipo:pessoa|robo|animal|coisa, especie (animal: cachorro|gato|cavalo|vaca|porco|ovelha|raposa|lobo|urso|panda|coelho|leao|tigre|elefante|girafa|tartaruga|galinha|pato|passaro|papagaio|coruja|pinguim|peixe|tubarao|golfinho|dinossauro|sapo|macaco), icone (coisa: "pizza", "café", "polvo" — qualquer objeto/bicho que fala), pessoa: pele, cabelo:curto|cacheado|longo|topete|coque|careca, corCabelo, roupa, oculos, barba; robo: cor}} — sem tipo, o nome decide (nome de bicho vira bicho, de IA vira o mascote laranja, de objeto vira coisa); figurantes:["cachorro","pessoa"] (quem está na cena sem falar); BICHO NÃO FALA: a fala de um bicho sai da narração e ele fica na cena — só fala se a pessoa pedir ("cachorro falante") ou personagens.x.fala:true; a voz de cada falante sai do GÊNERO do nome (personagens.x.genero:f|m manda), uma voz por personagem do começo ao fim; bigode:true e cabeca:"cubo" (o rosto de pixel antigo) em personagens; sem lugar no pedido, o set é a cobertura à noite (janela da cidade, cartões de interface na frente). acao em cada parte: acenar|apontar|pular|dancar|comemorar|pensar|andar|correr. emocao feliz|serio|surpreso|pensativo|animado|curioso; destaque:"texto curto"; paineis:["título",…] só no set de telas (o padrão quando o pedido não diz lugar); visual:"liso" só se pedirem o boneco liso antigo). '
|
|
98
99
|
+ 'marca e lancamento aceitam visual:orbe|vetor|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
99
100
|
+ 'voz (thalita|antonio|francisca). DUAS VOZES (o pedido fala em dois narradores, diálogo, conversa, entrevista): roteiro.vozes {"nome":"antonio","outro":"thalita"} e falante:"nome" em cada parte — cada um sai na sua voz. Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
package/lib/estudio/gerativo.js
CHANGED
|
@@ -323,7 +323,7 @@ const MOLDURAS = ['sem', 'cantos', 'completa'];
|
|
|
323
323
|
'bokeh' (as manchas de luz desfocadas na frente). O acento: 'traco' (cada
|
|
324
324
|
traço se desenha e some) ou 'viagem' (o ponto que nasce no meio, viaja
|
|
325
325
|
entre as batidas e se recolhe — o de antes). */
|
|
326
|
-
const AMBIENTES = ['limpo', 'luz', 'bokeh'];
|
|
326
|
+
const AMBIENTES = ['limpo', 'luz', 'bokeh', 'brilho'];
|
|
327
327
|
const ACENTOS_JEITO = ['traco', 'viagem'];
|
|
328
328
|
const RITMOS_ICONE = ['seguidos', 'alternados', 'esparsos'];
|
|
329
329
|
const MATERIAIS_ICONE = ['argila', 'verniz', 'metal', 'perola', 'acetinado'];
|
|
@@ -815,7 +815,25 @@ function desenharSom(P, { trilha = null, sons = {} } = {}) {
|
|
|
815
815
|
const eco = () => `aecho=0.8:${f(sh.entre(0.4, 0.75))}:${Math.round(sh.entre(30, 90))}|${Math.round(sh.entre(100, 220))}:${f(sh.entre(0.15, 0.35))}|${f(sh.entre(0.08, 0.2))}`;
|
|
816
816
|
const ss = () => f(sh.entre(4, Math.max(6, DUR + 20))); // de onde da trilha sai o pedaço
|
|
817
817
|
const sint = sonsDoVideo({ partes: P.batidas.map((b) => ({ titulo: b.comp, fala: String(b.de) })) }).receitas;
|
|
818
|
+
/* OS SONS PEQUENOS (13/10: "90% do motion é efeito sonoro: barulhinho
|
|
819
|
+
bonitinho, gostoso, satisfatório"). Sintetizados, limpos e cada um com
|
|
820
|
+
a sua altura: o tique da palavra que entra, o pop do ícone e do item, o
|
|
821
|
+
brilho da marca. Não vêm da trilha: são o "clique" do motion. */
|
|
822
|
+
const MICRO = new Set(['tique', 'pop', 'brilho']);
|
|
823
|
+
const receitaMicro = (gesto) => {
|
|
824
|
+
if (gesto === 'tique') {
|
|
825
|
+
const fq = Math.round(sh.entre(1800, 3200)), d = f(sh.entre(0.05, 0.08));
|
|
826
|
+
return ['-f', 'lavfi', '-i', `aevalsrc='0.55*sin(2*PI*${fq}*t)*exp(-${Math.round(sh.entre(70, 110))}*t)+0.12*(random(0)*2-1)*exp(-200*t)':s=44100:d=${d}`, '-af', `highpass=f=900,${panF()}`];
|
|
827
|
+
}
|
|
828
|
+
if (gesto === 'pop') {
|
|
829
|
+
const a0 = Math.round(sh.entre(700, 1200)), a1 = Math.round(sh.entre(380, 640));
|
|
830
|
+
return ['-f', 'lavfi', '-i', `aevalsrc='0.8*sin(2*PI*(${a0}-${a0 - a1}*t*5)*t)*exp(-${Math.round(sh.entre(22, 34))}*t)*(1-exp(-800*t))':s=44100:d=0.2`, '-af', panF()];
|
|
831
|
+
}
|
|
832
|
+
const b0 = Math.round(sh.entre(780, 1050));
|
|
833
|
+
return ['-f', 'lavfi', '-i', `aevalsrc='0.3*sin(2*PI*${b0}*t)*exp(-3*t)+0.2*sin(2*PI*${Math.round(b0 * 1.5)}*t)*exp(-3.8*t)+0.12*sin(2*PI*${b0 * 2}*t)*exp(-4.6*t)':s=44100:d=1.3`, '-af', `${eco()},${panF()}`];
|
|
834
|
+
};
|
|
818
835
|
const receita = (gesto) => {
|
|
836
|
+
if (MICRO.has(gesto)) return receitaMicro(gesto);
|
|
819
837
|
if (!musica) { // sem trilha: a síntese, um arquivo por evento (a semente muda a cada um)
|
|
820
838
|
const base = { revela: 'revela0', golpe: sh.um(['golpe0', 'golpe1']), fatia: sh.um(['nasce0', 'nasce1', 'nasce2']), gaguejo: sh.um(['passagem0', 'passagem1']), ar: sh.um(['passagem1', 'passagem2']) }[gesto];
|
|
821
839
|
const args = [...sint[base]];
|
|
@@ -854,7 +872,9 @@ function desenharSom(P, { trilha = null, sons = {} } = {}) {
|
|
|
854
872
|
};
|
|
855
873
|
const evento = (t, gesto, volume) => {
|
|
856
874
|
if (t < 0 || t > DUR - 0.1) return;
|
|
857
|
-
|
|
875
|
+
// Som grande não encosta em som grande (0,25 s); o pequeno só não encosta em nada a menos de 0,09 s.
|
|
876
|
+
const micro = MICRO.has(gesto);
|
|
877
|
+
if (efeitos.some((e) => Math.abs(e.t - t) < (micro || MICRO.has(e.gesto) ? 0.09 : 0.25))) return;
|
|
858
878
|
const nome = 'e' + String(efeitos.length).padStart(2, '0') + '-' + gesto;
|
|
859
879
|
receitas[nome] = receita(gesto);
|
|
860
880
|
efeitos.push({ t: f(Math.max(0, t + (sh.prox() - 0.5) * 0.02)), nome, gesto, volume: f(volume * (0.85 + sh.prox() * 0.3)) });
|
|
@@ -882,6 +902,18 @@ function desenharSom(P, { trilha = null, sons = {} } = {}) {
|
|
|
882
902
|
if (tr === 'atravessa' || tr === 'inunda') return evento(b.de - 0.4, 'ar', 0.75);
|
|
883
903
|
return evento(b.de - 0.08, 'fatia', 0.75);
|
|
884
904
|
});
|
|
905
|
+
// O estilo da casa ganha os sons pequenos: cada palavra da tese, cada ícone e item, e o brilho da marca.
|
|
906
|
+
if (P.somDenso) {
|
|
907
|
+
bs.forEach((b) => {
|
|
908
|
+
for (const l of b.linhas || []) {
|
|
909
|
+
if (l.papel === 'tese' || l.papel === 'numero') for (const p of l.palavras || []) if (p.t != null) evento(p.t, 'tique', 0.32);
|
|
910
|
+
if (l.papel === 'item' && l.t != null) evento(l.t, 'pop', 0.42);
|
|
911
|
+
}
|
|
912
|
+
if (b.icone3d && b.icone3d.t != null) evento(b.icone3d.t, 'pop', 0.5);
|
|
913
|
+
if (b.comp === 'marca') evento(b.de + 0.35, 'brilho', 0.55);
|
|
914
|
+
if (b.livre) evento(b.de + 0.08, 'pop', 0.45); // a cena da IA entra com um pop
|
|
915
|
+
});
|
|
916
|
+
}
|
|
885
917
|
efeitos.sort((a, b) => a.t - b.t);
|
|
886
918
|
// O src é o nome que sons-leves.prepararReceitas dá (semente + nome): um arquivo por evento.
|
|
887
919
|
const sufixo = (P.semente >>> 0).toString(36);
|
|
@@ -905,6 +937,27 @@ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], n
|
|
|
905
937
|
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos, icones });
|
|
906
938
|
const DUR = partitura.duracao;
|
|
907
939
|
|
|
940
|
+
/* A PARTE QUE A IA DESENHA (13/10: "a IA vai criar os estilos, os ícones,
|
|
941
|
+
as coisas"). A parte com `componente` aponta um React escrito pela IA na
|
|
942
|
+
pasta do projeto; ele ocupa o quadro no tempo da batida e recebe em
|
|
943
|
+
el.dados o texto, cada palavra com o seu tempo (t relativo ao início da
|
|
944
|
+
batida), as cores e as fontes. O Estúdio segue cuidando da voz, da
|
|
945
|
+
trilha, dos sons e do fundo; a batida deixa de desenhar o texto dela. */
|
|
946
|
+
const livres = [];
|
|
947
|
+
partitura.batidas.forEach((b) => {
|
|
948
|
+
const pt = partes[b.parte];
|
|
949
|
+
const src = pt && typeof pt.componente === 'string' ? pt.componente.trim() : '';
|
|
950
|
+
if (!src) return;
|
|
951
|
+
const palavras = (b.linhas || []).flatMap((l) => (l.palavras || []).map((p) => ({ texto: p.texto, t: p.t != null ? r3(p.t - b.de) : null })));
|
|
952
|
+
livres.push({ tipo: 'componente', src, de: b.de, ate: b.ate, dados: {
|
|
953
|
+
fala: pt.fala || '', titulo: pt.titulo || '', palavras, duracao: r3(b.ate - b.de),
|
|
954
|
+
cores: b.cores, fontes: partitura.fontes, largura, altura, ...(pt.dados && typeof pt.dados === 'object' ? pt.dados : {}) } });
|
|
955
|
+
// As linhas ficam (o leiaute do kit conta com elas), mas ocultas: quem desenha é a cena da IA.
|
|
956
|
+
for (const l of b.linhas || []) l.oculta = true;
|
|
957
|
+
b.acento = []; b.livre = true;
|
|
958
|
+
delete b.heroi; delete b.icone3d; delete b.textura; delete b.contador;
|
|
959
|
+
});
|
|
960
|
+
|
|
908
961
|
// ── o som: efeitos nas transformações, a música abaixa sob a voz ─────
|
|
909
962
|
/* O SOM (desenharSom): um evento por batida, no máximo — e cada um é um
|
|
910
963
|
arquivo PRÓPRIO, feito da trilha do vídeo (ou sintetizado, sem trilha). */
|
|
@@ -928,7 +981,7 @@ function montarGerativo({ roteiro, largura = 1920, altura = 1080, tempos = [], n
|
|
|
928
981
|
// As chaves "fonte*" são o que o fontes.css procura (remotion.js).
|
|
929
982
|
fonte: partitura.fontes.display, fonteTexto: partitura.fontes.texto, fonteMono: partitura.fontes.mono,
|
|
930
983
|
fonteCinetica: partitura.fontes.cinetica, fonteSerifa: partitura.fontes.serifa,
|
|
931
|
-
}],
|
|
984
|
+
}, ...livres],
|
|
932
985
|
}],
|
|
933
986
|
audio: {
|
|
934
987
|
...(narracao ? { narracao } : {}),
|
|
@@ -1498,6 +1551,34 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1498
1551
|
}
|
|
1499
1552
|
});
|
|
1500
1553
|
|
|
1554
|
+
/* O ESTILO DA CASA (13/10, as referências do Joel — o "Vantage"): o vídeo
|
|
1555
|
+
inteiro no chão escuro da marca, com o brilho do acento derivando no
|
|
1556
|
+
fundo; a frase média (título 7–11% da altura, motionmaxxing SKILL:134),
|
|
1557
|
+
não a palavra gigante; cada palavra entra inteira, desfocando (type.md
|
|
1558
|
+
R2.6), na hora em que a voz a diz; sem a parede de texto repetido. */
|
|
1559
|
+
if (roteiro.casa) {
|
|
1560
|
+
fundo.ambiente = 'brilho';
|
|
1561
|
+
const escura = batidas.reduce((m, b) => (b.cores && (!m || luz(b.cores.fundo) < luz(m.fundo)) ? b.cores : m), null);
|
|
1562
|
+
const teto = (deitado ? 0.11 : 0.075) * H;
|
|
1563
|
+
for (const b of batidas) {
|
|
1564
|
+
if (escura) b.cores = { ...escura };
|
|
1565
|
+
b.troca = 'corte';
|
|
1566
|
+
// A saída é da LINHA inteira (desliza) ou corte seco: o "evapora" e o "comprime" espalhavam as letras ("a ad e").
|
|
1567
|
+
if (b.saida && b.saida !== 'fim') b.saida = batidas.indexOf(b) % 2 ? 'corte' : 'desliza';
|
|
1568
|
+
delete b.textura;
|
|
1569
|
+
// O enfeite abstrato (colunas 3D, truchet, a textura de curvas) sai; o ícone do site (b.icone3d) fica.
|
|
1570
|
+
delete b.heroi;
|
|
1571
|
+
for (const l of b.linhas || []) {
|
|
1572
|
+
// O teto vale também para o "encher" do kit (cresce até l.max): sem isso a tese gigante passava de 30% da altura.
|
|
1573
|
+
if ((l.papel === 'tese' || l.papel === 'numero' || l.papel === 'fraco') && !l.marca) {
|
|
1574
|
+
if (l.tamanho > teto) l.tamanho = Math.round(teto);
|
|
1575
|
+
if (l.encher) l.max = Math.round(Math.min(l.max || teto, teto));
|
|
1576
|
+
}
|
|
1577
|
+
if (l.papel === 'tese' && !(b.contador && b.contador.linha === l.id)) l.comportamento = 'desfoque';
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1501
1582
|
return {
|
|
1502
1583
|
versao: 1, semente, fps: FPS, humor,
|
|
1503
1584
|
musica: { genero: ou(E.genero, s.um(H0.generos)), generos: H0.generos }, largura: W, altura: H, duracao, regras: REGRAS,
|
|
@@ -1505,6 +1586,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], icon
|
|
|
1505
1586
|
margem: M, fundo, batidas, camera, produtos,
|
|
1506
1587
|
identidade: { chaos: ident.chaos, acento: ident.acento, comeca: campos[comeca], fontes: [display, texto] }, secoes: secoes.nomes,
|
|
1507
1588
|
hud: molduraDe(roteiro, E, { W, H, deitado, marca, secoes: secoes.nomes }),
|
|
1589
|
+
somDenso: !!roteiro.casa,
|
|
1508
1590
|
};
|
|
1509
1591
|
}
|
|
1510
1592
|
|
package/lib/estudio/index.js
CHANGED
|
@@ -898,6 +898,12 @@ async function variarIcones(plano, roteiro) {
|
|
|
898
898
|
const explicitos = [];
|
|
899
899
|
try { JSON.stringify(roteiro || {}, (k, v) => { if (k === 'icone' && typeof v === 'string') explicitos.push(v); return v; }); } catch { /* sem */ }
|
|
900
900
|
const troca = {};
|
|
901
|
+
/* VÁRIAS BIBLIOTECAS NO MESMO VÍDEO (13/10: "não uma biblioteca de ícone,
|
|
902
|
+
mas várias, que vai misturar tudo"). Antes o vídeo sorteava UMA coleção
|
|
903
|
+
e trocava todos os ícones para ela. Agora cada ícone tenta uma coleção,
|
|
904
|
+
em rodízio a partir da sorteada; sem o desenho nela, a próxima. */
|
|
905
|
+
const colecoes = [colecao, ...assets.COLECOES.map((c) => c[0]).filter((c) => c !== colecao)];
|
|
906
|
+
let vez = 0;
|
|
901
907
|
for (const nome of variedade.iconesTrocaveis(plano, explicitos)) {
|
|
902
908
|
/* Primeiro O ÍCONE: o conceito escolhe entre as formas dele
|
|
903
909
|
(edicao.ALTERNATIVAS) pelos pontos da variedade — o que os últimos
|
|
@@ -914,9 +920,12 @@ async function variarIcones(plano, roteiro) {
|
|
|
914
920
|
variedade.marcar('icone-nome', escolhido);
|
|
915
921
|
}
|
|
916
922
|
let novo = 'fluent-3d:' + escolhido;
|
|
917
|
-
|
|
918
|
-
|
|
923
|
+
for (let k = 0; k < colecoes.length; k++) {
|
|
924
|
+
const c = colecoes[(vez + k) % colecoes.length];
|
|
925
|
+
if (c === 'fluent-3d') break;
|
|
926
|
+
try { const r = await assets.icone(c + ':' + escolhido); if (r && r.ok) { novo = c + ':' + escolhido; break; } } catch { /* a próxima */ }
|
|
919
927
|
}
|
|
928
|
+
vez++;
|
|
920
929
|
if (novo !== nome) troca[nome] = novo;
|
|
921
930
|
}
|
|
922
931
|
const n = variedade.trocarIcones(plano, troca);
|
|
@@ -1300,6 +1309,24 @@ async function prepararRoteiro(p, args) {
|
|
|
1300
1309
|
for (const pt of roteiro.partes || []) {
|
|
1301
1310
|
if (pt.src && !existe(pt.src)) { if (!pt.busca && /^https?:/i.test(pt.src)) pt.busca = pt.titulo || ''; delete pt.src; }
|
|
1302
1311
|
}
|
|
1312
|
+
/* A CENA QUE A IA ESCREVEU (parte.componente): ela escreve na pasta de
|
|
1313
|
+
trabalho, e o Remotion só lê o que está no projeto. O arquivo é copiado
|
|
1314
|
+
para componentes/ do projeto; se não existe, a parte fica no estilo da
|
|
1315
|
+
casa (e o caderno diz por quê), em vez de o render quebrar. */
|
|
1316
|
+
for (const pt of roteiro.partes || []) {
|
|
1317
|
+
if (!pt || typeof pt.componente !== 'string' || !pt.componente.trim()) continue;
|
|
1318
|
+
const pedido = pt.componente.trim();
|
|
1319
|
+
const achado = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
|
|
1320
|
+
.find((f) => f && fs.existsSync(f) && /\.(jsx|tsx|js)$/i.test(f));
|
|
1321
|
+
if (!achado) { projeto.anotar(p.id, 'plano', `componente ${pedido} não existe: a parte ficou no estilo da casa`); delete pt.componente; continue; }
|
|
1322
|
+
const dentro = path.resolve(p.pasta) + path.sep;
|
|
1323
|
+
if (!achado.startsWith(dentro)) {
|
|
1324
|
+
const destino = path.join(p.pasta, 'componentes', path.basename(achado));
|
|
1325
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
1326
|
+
fs.copyFileSync(achado, destino);
|
|
1327
|
+
pt.componente = path.relative(p.pasta, destino).replace(/\\/g, '/');
|
|
1328
|
+
} else pt.componente = path.relative(p.pasta, achado).replace(/\\/g, '/');
|
|
1329
|
+
}
|
|
1303
1330
|
// As imagens do pedido (o terminal guardou em referencias.imagens) entram sozinhas, uma vez por projeto.
|
|
1304
1331
|
const jaTem = new Set((p.referencias || []).map((x) => x.de));
|
|
1305
1332
|
const novas = referencias.imagens().filter((f) => !jaTem.has(f));
|
|
@@ -1333,6 +1360,8 @@ async function prepararRoteiro(p, args) {
|
|
|
1333
1360
|
if (estilo === 'gerativo' && !/\bfontes?\b|tipografia|\bserifa/i.test(pedidoTxt)) {
|
|
1334
1361
|
roteiro.marca = { ...(roteiro.marca || {}), fontes: { titulo: 'Inter', texto: 'JetBrains Mono' } };
|
|
1335
1362
|
}
|
|
1363
|
+
// O estilo da casa no gerativo: chão escuro com brilho, frase média, palavra que entra desfocando (gerativo.js).
|
|
1364
|
+
if (estilo === 'gerativo') roteiro.casa = true;
|
|
1336
1365
|
/* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
|
|
1337
1366
|
com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
|
|
1338
1367
|
o agente tenha escolhido outro estilo pelo nome. */
|
|
@@ -2061,10 +2090,63 @@ async function transcrever(p, alvo, ctx) {
|
|
|
2061
2090
|
|
|
2062
2091
|
// ── estudio_render ───────────────────────────────────────────────────────
|
|
2063
2092
|
|
|
2093
|
+
/* O SEGUNDO MOTOR: HYPERFRAMES (heygen-com/hyperframes, Apache-2.0, 13/10:
|
|
2094
|
+
"trabalhar com o Remotion, mas também com o Hyperframes"). A IA escreve a
|
|
2095
|
+
composição em HTML (index.html com data-start/data-duration e GSAP, CSS ou
|
|
2096
|
+
three.js) numa pasta dela; aqui a pasta vai para o projeto, ganha a midia/
|
|
2097
|
+
do projeto (voz, trilha, sons do Estúdio), e o CLI renderiza num Chrome
|
|
2098
|
+
escondido — sem telemetria (HYPERFRAMES_NO_TELEMETRY). Depois, o mesmo do
|
|
2099
|
+
Remotion: volume de Reels, a conferência e o vídeo aberto. */
|
|
2100
|
+
const HYPERFRAMES = 'hyperframes@0.8.154';
|
|
2101
|
+
async function renderHyperframes(p, args, ctx = {}) {
|
|
2102
|
+
const pedido = String(args.html || args.composicao || 'hyperframes').trim();
|
|
2103
|
+
const origem = [path.resolve(p.pasta, pedido), args.__pasta && path.resolve(args.__pasta, pedido), path.isAbsolute(pedido) && pedido]
|
|
2104
|
+
.find((d) => d && fs.existsSync(path.join(d, 'index.html')));
|
|
2105
|
+
if (!origem) return { ok: false, error: `não achei ${pedido}/index.html — escreva a composição do Hyperframes numa pasta com index.html e passe html:"<pasta>".` };
|
|
2106
|
+
const destino = path.join(p.pasta, 'hyperframes');
|
|
2107
|
+
if (path.resolve(origem) !== path.resolve(destino)) {
|
|
2108
|
+
fs.rmSync(destino, { recursive: true, force: true });
|
|
2109
|
+
fs.cpSync(origem, destino, { recursive: true });
|
|
2110
|
+
}
|
|
2111
|
+
const midia = path.join(destino, 'midia');
|
|
2112
|
+
if (!fs.existsSync(midia) && fs.existsSync(path.join(p.pasta, 'midia'))) fs.symlinkSync(path.join(p.pasta, 'midia'), midia);
|
|
2113
|
+
const saida = path.join(p.pasta, 'saida', `${projeto.apelidar(p.titulo)}.mp4`);
|
|
2114
|
+
fs.mkdirSync(path.dirname(saida), { recursive: true });
|
|
2115
|
+
(ctx.aoFalar || (() => {}))('renderizando no Hyperframes');
|
|
2116
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
2117
|
+
const r = await new Promise((resolve) => {
|
|
2118
|
+
const { spawn } = require('child_process');
|
|
2119
|
+
const proc = spawn(npx, ['--yes', HYPERFRAMES, 'render', destino, '-o', saida, '--quiet'], { env: { ...process.env, HYPERFRAMES_NO_TELEMETRY: '1', DO_NOT_TRACK: '1' } });
|
|
2120
|
+
let erro = '';
|
|
2121
|
+
proc.stderr.on('data', (d) => { erro += d; });
|
|
2122
|
+
proc.stdout.on('data', (d) => { erro += d; });
|
|
2123
|
+
const relogio = setTimeout(() => { try { proc.kill(); } catch { /* já saiu */ } }, (Number(args.minutos) || 20) * 60000);
|
|
2124
|
+
proc.on('close', (code) => { clearTimeout(relogio); resolve({ ok: code === 0 && fs.existsSync(saida), erro }); });
|
|
2125
|
+
proc.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, erro: e.message }); });
|
|
2126
|
+
});
|
|
2127
|
+
if (!r.ok) return { ok: false, error: `o Hyperframes não renderizou: ${r.erro.slice(-400)}`, comoResolver: 'precisa de Node 22+ e do ffmpeg; rode `npx hyperframes doctor` na pasta.' };
|
|
2128
|
+
const med = await video.medir(saida);
|
|
2129
|
+
if (med.ok && med.temAudio) {
|
|
2130
|
+
const tmp = saida.replace(/\.mp4$/, '.norm.mp4');
|
|
2131
|
+
const ln = await video.rodar(video.ffmpeg(), ['-y', '-i', saida, '-c:v', 'copy', '-af', 'loudnorm=I=-14:TP=-1.2:LRA=10', '-c:a', 'aac', '-b:a', '256k', tmp], { timeout: 15 * 60000 });
|
|
2132
|
+
if (ln.ok && fs.existsSync(tmp)) fs.renameSync(tmp, saida); else { try { fs.unlinkSync(tmp); } catch { /* não havia */ } }
|
|
2133
|
+
}
|
|
2134
|
+
const bytes = fs.statSync(saida).size;
|
|
2135
|
+
projeto.salvar(p.id, { renders: [...(p.renders || []), { quando: new Date().toISOString(), arquivo: saida, bytes, motor: 'hyperframes' }] });
|
|
2136
|
+
projeto.anotar(p.id, 'render', `hyperframes: ${(bytes / 1048576).toFixed(1)} MB, ${(med.segundos || 0).toFixed(1)}s`);
|
|
2137
|
+
const conf = await conferencia.conferir(saida, { duracao: med.segundos || 0 });
|
|
2138
|
+
if (conf.ok && conf.avisos.length) projeto.anotar(p.id, 'conferencia', conf.avisos.join('; '));
|
|
2139
|
+
if (ctx.abrir) { try { ctx.abrir(saida); } catch { /* o caminho está na resposta */ } }
|
|
2140
|
+
return { ok: true, motor: 'hyperframes', arquivo: saida, bytes, segundos: med.segundos,
|
|
2141
|
+
conferencia: conf.ok ? { som: conf.som, parados: conf.parados, avisos: conf.avisos } : undefined,
|
|
2142
|
+
aviso: 'ABERTO para o usuário assistir. Chame finish dizendo o que foi feito' + (conf.ok && conf.avisos.length ? ' — e cite os avisos da conferência.' : '.') };
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2064
2145
|
async function ferramentaRender(args, ctx = {}) {
|
|
2065
2146
|
const p = projetoOuRecente(args.id);
|
|
2066
2147
|
if (!p) return semProjeto(args.id);
|
|
2067
2148
|
if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
|
|
2149
|
+
if (String(args.motor || '').toLowerCase() === 'hyperframes' || args.html) return renderHyperframes(p, args, ctx);
|
|
2068
2150
|
const pl = projeto.lerPlano(args.id);
|
|
2069
2151
|
if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
|
|
2070
2152
|
|
|
@@ -592,7 +592,8 @@ function pontoDaLetra({ t, fps, l, p, idx, n, b, cx, cores, alvoSaida }) {
|
|
|
592
592
|
|
|
593
593
|
// ── uma linha ──────────────────────────────────────────────────────────
|
|
594
594
|
function Linha({ l, b, cx, t, fps, P, frame, alvoSaida, centroBloco }) {
|
|
595
|
-
|
|
595
|
+
// A linha da parte que a IA desenha (componente): o texto está na cena dela.
|
|
596
|
+
if (!cx || l.oculta) return null;
|
|
596
597
|
const folga = Math.max(0, (P.largura * 0.94) / Math.max(1, cx.w) - 1);
|
|
597
598
|
// O GOLPE: a tese soca (escala que bate e assenta).
|
|
598
599
|
let soco = 1;
|
|
@@ -725,7 +726,26 @@ function Fundo({ P, b, prevB, t, cam, frame, origem }) {
|
|
|
725
726
|
return (
|
|
726
727
|
<div style={{ position: 'absolute', inset: 0, background: corBase, overflow: 'hidden' }}>
|
|
727
728
|
{circulo}
|
|
728
|
-
{(f.ambiente || 'bokeh') !== 'limpo' && <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />}
|
|
729
|
+
{(f.ambiente || 'bokeh') !== 'limpo' && f.ambiente !== 'brilho' && <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at ' + bx + '% ' + by + '%, ' + alfa(claro ? '#000000' : '#ffffff', 0.05) + ' 0%, transparent 60%)' }} />}
|
|
730
|
+
{/* O BRILHO (13/10, a referência "Vantage" do Joel): dois halos grandes
|
|
731
|
+
da cor de acento derivando devagar no fundo escuro, e um arco de luz
|
|
732
|
+
no horizonte na batida da marca e no fim. É o ambiente, não um efeito. */}
|
|
733
|
+
{f.ambiente === 'brilho' && (() => {
|
|
734
|
+
const ac = b.cores.acento;
|
|
735
|
+
const h1 = { x: 30 + 14 * Math.sin(t * 0.21), y: 38 + 10 * Math.cos(t * 0.17) }, h2 = { x: 72 + 12 * Math.cos(t * 0.15), y: 66 + 9 * Math.sin(t * 0.23) };
|
|
736
|
+
const pulso = 0.9 + 0.1 * Math.sin(t * 0.8);
|
|
737
|
+
const arco = b.comp === 'marca' || b.arco;
|
|
738
|
+
const ka = arco ? ed(c01((t - b.de) / 0.9)) : 0;
|
|
739
|
+
return (
|
|
740
|
+
<>
|
|
741
|
+
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 55% 60% at ' + h1.x + '% ' + h1.y + '%, ' + alfa(ac, (claro ? 0.22 : 0.42) * pulso) + ' 0%, transparent 70%)' }} />
|
|
742
|
+
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 45% 50% at ' + h2.x + '% ' + h2.y + '%, ' + alfa(ac, (claro ? 0.14 : 0.28) * pulso) + ' 0%, transparent 70%)' }} />
|
|
743
|
+
{ka > 0 && <div style={{ position: 'absolute', left: -W * 0.25, width: W * 1.5, height: W * 1.5, top: H * (1.02 - 0.3 * ka), borderRadius: '50%',
|
|
744
|
+
boxShadow: '0 0 ' + (W * 0.05) + 'px ' + (W * 0.012) + 'px ' + alfa(ac, 0.75 * ka) + ', inset 0 ' + (W * 0.02) + 'px ' + (W * 0.05) + 'px ' + alfa(ac, 0.45 * ka),
|
|
745
|
+
borderTop: Math.max(2, W * 0.002) + 'px solid ' + alfa('#ffffff', 0.55 * ka) }} />}
|
|
746
|
+
</>
|
|
747
|
+
);
|
|
748
|
+
})()}
|
|
729
749
|
{(f.particulas || []).map((q, i) => {
|
|
730
750
|
const x = ((q.x - cam.x * q.z * 1.6 + t * q.v * (i % 2 ? 1 : -0.6)) % W + W) % W;
|
|
731
751
|
const y = ((q.y - cam.y * q.z * 1.6 - t * q.v * 0.7) % H + H) % H;
|
|
@@ -80,8 +80,15 @@ function notasDaAssinatura() {
|
|
|
80
80
|
function instrucao(atual = ATUAL) { const i = instrucaoDoModo(atual); return i ? i + notasDaAssinatura() : ''; }
|
|
81
81
|
function instrucaoDoModo({ modo, escopo, explicito } = ATUAL) {
|
|
82
82
|
// O vídeo sem imagem é motion: o agente escolhe o estilo de motion que serve ao roteiro, não um fixo.
|
|
83
|
-
const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som
|
|
84
|
-
+ '
|
|
83
|
+
const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som. O estilo da casa (o gerativo, com as cores da marca) '
|
|
84
|
+
+ 'é o PONTO DE PARTIDA, não o limite: não ponha roteiro.estilo, a menos que a pessoa peça outro pelo nome. '
|
|
85
|
+
/* A IA CRIA (13/10: "a IA vai criar os estilos, os ícones, as coisas"). */
|
|
86
|
+
+ 'Quando a referência ou o pedido pede um visual que o estilo não faz (uma janela de app, um gráfico, uma colagem, um objeto), '
|
|
87
|
+
+ 'CRIE a cena: escreva um React (write_file, ex.: componentes/cena-x.jsx, export default ({el, t}) => …, t em segundos desde o '
|
|
88
|
+
+ 'começo da parte; el.dados traz palavras [{texto, t}], cores {fundo, texto, acento}, fontes, largura, altura) e ponha '
|
|
89
|
+
+ 'componente:"componentes/cena-x.jsx" naquela parte. A voz, a trilha, os sons e o fundo continuam do Estúdio. '
|
|
90
|
+
+ 'Vídeo inteiro desenhado por você: escreva uma composição Hyperframes (pasta com index.html, data-start/data-duration, GSAP), '
|
|
91
|
+
+ 'use a voz do estudio_narrar e os sons de midia/, e estudio_render motor:"hyperframes" html:"<pasta>". '
|
|
85
92
|
/* O ROTEIRO (13/10: "o roteiro que monta é horrível" — eram os títulos da landing em fila). */
|
|
86
93
|
+ 'O ROTEIRO é uma história de 15 a 25 s: gancho (a situação de quem assiste) → o que o produto FAZ → UMA prova concreta '
|
|
87
94
|
+ 'dos fatos do site → a ação (nome, comando, link). Nunca os títulos das seções do site em sequência. Cada fala tem de 5 a '
|
package/lib/tools.js
CHANGED
|
@@ -815,7 +815,8 @@ async function despachar(name, args, ctx) {
|
|
|
815
815
|
case 'estudio_projeto':
|
|
816
816
|
return estudio.ferramentaProjeto(args, ctx);
|
|
817
817
|
case 'estudio_plano':
|
|
818
|
-
|
|
818
|
+
// A pasta de trabalho vai junto: o componente que a IA escreveu mora nela (index.js copia para o projeto).
|
|
819
|
+
return estudio.ferramentaPlano({ ...args, __pasta: projectDir });
|
|
819
820
|
case 'estudio_midia':
|
|
820
821
|
return estudio.ferramentaMidia(args);
|
|
821
822
|
case 'estudio_narrar':
|
|
@@ -825,7 +826,8 @@ async function despachar(name, args, ctx) {
|
|
|
825
826
|
// contexto do pedido atravessa até aqui.
|
|
826
827
|
return estudio.ferramentaVideo(args, ctx);
|
|
827
828
|
case 'estudio_render':
|
|
828
|
-
|
|
829
|
+
// A pasta de trabalho vai junto: a composição do Hyperframes que a IA escreveu mora nela.
|
|
830
|
+
return estudio.ferramentaRender({ ...args, __pasta: projectDir }, {
|
|
829
831
|
...ctx,
|
|
830
832
|
// O vídeo pronto abre para a pessoa assistir. É a única
|
|
831
833
|
// coisa que abre: foi o vídeo que ela pediu.
|
package/lib/ui.js
CHANGED
|
@@ -567,13 +567,26 @@ const ROTULOS = {
|
|
|
567
567
|
seguidas eram uma coluna de ganchos sem dizer o que era ação e o que era
|
|
568
568
|
saída. O argumento vem numa linha só e cortado: um comando de três linhas
|
|
569
569
|
no cabeçalho empurra o resultado para fora da vista. */
|
|
570
|
+
/* O CAMINHO LEGÍVEL (13/10: "deixar o layout mais organizado"). O cabeçalho
|
|
571
|
+
mostrava o caminho cru — "/Users/joel/.primocode/studio/projetos/2026-10/
|
|
572
|
+
primocode-bastidor-vox/midia/logo-prim…" — e o que importa (o arquivo)
|
|
573
|
+
era o que o corte comia. Agora: a pasta pessoal vira ~, o projeto do
|
|
574
|
+
Estúdio vira "projeto <id> ›", e caminho longo fica com o fim. */
|
|
575
|
+
function encurtarCaminhos(texto) {
|
|
576
|
+
const home = require('os').homedir();
|
|
577
|
+
return String(texto || '')
|
|
578
|
+
.replace(/(?:~|\/[^\s'"]*?)\/\.primocode\/studio\/projetos\/(?:\d{4}-\d{2}\/)?([^\/\s'"]+)\/?/g, (m, id) => `projeto ${id} › `)
|
|
579
|
+
.split(home).join('~')
|
|
580
|
+
.replace(/(?:~|\/)[^\s'"]{50,}/g, (cam) => { const ps = cam.split('/').filter(Boolean); return ps.length > 3 ? '…/' + ps.slice(-2).join('/') : cam; });
|
|
581
|
+
}
|
|
582
|
+
|
|
570
583
|
function actionHeader(name, args) {
|
|
571
584
|
const label = ROTULOS[name] || name;
|
|
572
585
|
let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
|
|
573
586
|
// Campo que veio como objeto (o modelo às vezes manda {"window": {}})
|
|
574
587
|
// virava "[object Object]" na tela.
|
|
575
588
|
if (argStr && typeof argStr === 'object') argStr = JSON.stringify(argStr).slice(0, 60);
|
|
576
|
-
argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
|
|
589
|
+
argStr = encurtarCaminhos(String(argStr || '').replace(/\s+/g, ' ').trim());
|
|
577
590
|
// O que ele está fazendo AGORA — a barra da direita da tela cheia mostra.
|
|
578
591
|
agora.texto = label + (argStr ? ' · ' + argStr.slice(0, 60) : '');
|
|
579
592
|
agora.desde = Date.now();
|
package/lib/video/cor.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cor.js — o color grading do vídeo da pessoa, decidido pela própria imagem.
|
|
3
|
+
*
|
|
4
|
+
* "deixar os vídeos cinematográficos, não só recortar o fundo… ela conseguir
|
|
5
|
+
* sozinha fazer um color grading. No meu vídeo." (Joel, 13/10/2026)
|
|
6
|
+
*
|
|
7
|
+
* O `apply_color` já existia, mas só com números passados à mão — e quem
|
|
8
|
+
* passava era o agente, chutando. Aqui a imagem decide:
|
|
9
|
+
*
|
|
10
|
+
* 1. MEDE o ffmpeg (signalstats) lê, a 1 quadro/s nos primeiros 60 s: o
|
|
11
|
+
* brilho médio, a sombra e a luz mais forte, a saturação e a
|
|
12
|
+
* dominante de cor (U e V longe de 128 = vídeo azulado ou amarelado).
|
|
13
|
+
* 2. CORRIGE a exposição vai para o meio (gama, não brilho chapado) e a
|
|
14
|
+
* dominante sai — o look é posto em cima de uma imagem neutra.
|
|
15
|
+
* 3. LOOK o de cinema separa sombra e luz: sombra para o azul-petróleo, luz
|
|
16
|
+
* (e a pele) para o laranja, curva de filme com o preto levemente
|
|
17
|
+
* levantado, saturação contida, vinheta e grão leves.
|
|
18
|
+
*
|
|
19
|
+
* O look sai pela imagem quando ninguém pediu: escuro vira "noite", quase sem
|
|
20
|
+
* cor vira "natural", o resto "cinema". Medir é I/O; corrigir e montar os
|
|
21
|
+
* filtros são puros — o teste confere sem vídeo nenhum.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const LOOKS = ['cinema', 'quente', 'noite', 'natural', 'pb'];
|
|
27
|
+
|
|
28
|
+
/** Média das métricas do signalstats impressas pelo `metadata=print`. Pura. */
|
|
29
|
+
function lerMedidas(texto) {
|
|
30
|
+
const soma = {}, n = {};
|
|
31
|
+
for (const m of String(texto || '').matchAll(/lavfi\.signalstats\.(YAVG|YLOW|YHIGH|SATAVG|UAVG|VAVG)=([\d.]+)/g)) {
|
|
32
|
+
soma[m[1]] = (soma[m[1]] || 0) + Number(m[2]);
|
|
33
|
+
n[m[1]] = (n[m[1]] || 0) + 1;
|
|
34
|
+
}
|
|
35
|
+
if (!n.YAVG) return null;
|
|
36
|
+
const media = (k, padrao) => (n[k] ? soma[k] / n[k] : padrao);
|
|
37
|
+
return { y: media('YAVG'), ylow: media('YLOW', 16), yhigh: media('YHIGH', 235), sat: media('SATAVG', 30), u: media('UAVG', 128), v: media('VAVG', 128) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function medir(arquivo, { video = require('../estudio/video.js') } = {}) {
|
|
41
|
+
const r = await video.rodar(video.ffmpeg(), ['-hide_banner', '-t', '60', '-i', arquivo, '-an',
|
|
42
|
+
'-vf', 'fps=1,scale=320:-2,signalstats,metadata=print:file=-', '-f', 'null', '-'], { timeout: 5 * 60000 });
|
|
43
|
+
const m = lerMedidas(r.saida + '\n' + r.erro);
|
|
44
|
+
return m ? { ok: true, ...m } : { ok: false, error: 'não consegui medir a cor do vídeo' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lim = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
48
|
+
const n3 = (v) => Number(v.toFixed(3));
|
|
49
|
+
|
|
50
|
+
/** O look pela imagem, quando ninguém pediu. Pura. */
|
|
51
|
+
function escolherLook(m) {
|
|
52
|
+
if (m.y < 72) return 'noite';
|
|
53
|
+
if (m.sat < 10) return 'natural';
|
|
54
|
+
return 'cinema';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A correção que deixa a imagem neutra antes do look. Pura. */
|
|
58
|
+
function correcao(m) {
|
|
59
|
+
// Gama que leva o brilho médio para ~44% (o meio de um vídeo bem exposto).
|
|
60
|
+
// No eq do ffmpeg a saída é entrada^(1/gama): gama = ln(brilho)/ln(0,44) (>1 clareia).
|
|
61
|
+
const gama = lim(Math.log(lim(m.y / 255, 0.05, 0.95)) / Math.log(0.44), 0.72, 1.4);
|
|
62
|
+
// Dominante: U acima de 128 é azul demais, V acima é vermelho demais. Tira ~70% dela.
|
|
63
|
+
const azul = lim(-((m.u - 128) / 128) * 0.7, -0.15, 0.15);
|
|
64
|
+
const vermelho = lim(-((m.v - 128) / 128) * 0.7, -0.15, 0.15);
|
|
65
|
+
// Faixa curta entre a sombra e a luz = imagem lavada: um pouco de contraste.
|
|
66
|
+
const contraste = m.yhigh - m.ylow < 170 ? 1.08 : 1;
|
|
67
|
+
return { gama: n3(gama), azul: n3(azul), vermelho: n3(vermelho), contraste };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const CURVA_FILME = "0/0.035 0.25/0.21 0.5/0.5 0.78/0.82 1/0.97";
|
|
71
|
+
|
|
72
|
+
/** Os filtros do ffmpeg para o grade. Puro: { look, intensidade 0..1, ...correção }. */
|
|
73
|
+
function filtrosDoGrade(p = {}) {
|
|
74
|
+
const k = lim(p.intensidade != null ? Number(p.intensidade) : 0.8, 0, 1);
|
|
75
|
+
const look = LOOKS.includes(p.look) ? p.look : 'cinema';
|
|
76
|
+
const f = [];
|
|
77
|
+
// 1. a imagem neutra
|
|
78
|
+
const eq = [`gamma=${n3(p.gama || 1)}`];
|
|
79
|
+
if (p.contraste && p.contraste !== 1) eq.push(`contrast=${n3(p.contraste)}`);
|
|
80
|
+
f.push('eq=' + eq.join(':'));
|
|
81
|
+
if (p.azul || p.vermelho) f.push(`colorbalance=rm=${n3(p.vermelho || 0)}:bm=${n3(p.azul || 0)}`);
|
|
82
|
+
// 2. o look, na intensidade pedida
|
|
83
|
+
const s = (v) => n3(v * k);
|
|
84
|
+
if (look === 'cinema') {
|
|
85
|
+
f.push(`colorbalance=rs=${s(-0.06)}:bs=${s(0.09)}:rh=${s(0.07)}:gh=${s(0.02)}:bh=${s(-0.07)}`);
|
|
86
|
+
f.push(`curves=all='${CURVA_FILME}'`, `eq=saturation=${n3(1 - 0.1 * k)}`);
|
|
87
|
+
} else if (look === 'quente') {
|
|
88
|
+
f.push(`colorbalance=rm=${s(0.05)}:bm=${s(-0.05)}:rh=${s(0.04)}:bh=${s(-0.05)}`);
|
|
89
|
+
f.push(`curves=all='0/0.03 0.5/0.52 1/0.97'`, `eq=saturation=${n3(1 + 0.05 * k)}`);
|
|
90
|
+
} else if (look === 'noite') {
|
|
91
|
+
f.push(`colorbalance=rs=${s(-0.04)}:bs=${s(0.1)}:rm=${s(-0.02)}:bm=${s(0.06)}`);
|
|
92
|
+
f.push(`curves=all='0/0.02 0.3/0.24 0.6/0.6 1/0.94'`, `eq=saturation=${n3(1 - 0.22 * k)}`);
|
|
93
|
+
} else if (look === 'pb') {
|
|
94
|
+
f.push('hue=s=0', `curves=all='0/0.04 0.3/0.24 0.7/0.78 1/0.97'`);
|
|
95
|
+
} else {
|
|
96
|
+
f.push(`curves=all='0/0.02 0.25/0.23 0.75/0.78 1/0.98'`);
|
|
97
|
+
}
|
|
98
|
+
// 3. o acabamento de filme: vinheta e grão leves
|
|
99
|
+
f.push(`vignette=angle=${n3(Math.PI / (5.8 - 1.2 * k))}`);
|
|
100
|
+
if (k > 0.2) f.push(`noise=alls=${Math.round(3 + 4 * k)}:allf=t`);
|
|
101
|
+
return f;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { LOOKS, lerMedidas, medir, escolherLook, correcao, filtrosDoGrade };
|
package/lib/video/filters.js
CHANGED
|
@@ -92,6 +92,9 @@ const EFEITOS = {
|
|
|
92
92
|
return f;
|
|
93
93
|
},
|
|
94
94
|
},
|
|
95
|
+
// O grade de cinema (cor.js): correção medida + look. Quem preenche os números é a op `grade`.
|
|
96
|
+
'color.cine': { params: 'look (cinema|quente|noite|natural|pb), intensidade (0..1, 0.8), gama, contraste, azul, vermelho',
|
|
97
|
+
filtro: (p) => require('./cor.js').filtrosDoGrade(p) },
|
|
95
98
|
'blur.gaussian': { params: 'sigma (px, 8)', filtro: (p) => [`gblur=sigma=${num(lim(p.sigma != null ? p.sigma : 8, 0.1, 200))}`] },
|
|
96
99
|
sharpen: { params: 'amount (0..3, 1)', filtro: (p) => [`unsharp=5:5:${num(lim(p.amount != null ? p.amount : 1, 0, 3))}`] },
|
|
97
100
|
'stylize.vignette': { params: 'strength (0..1, 0.5)', filtro: (p) => [`vignette=angle=${num(0.25 + 0.9 * lim(p.strength !== undefined ? p.strength : 0.5, 0, 1))}`] },
|
package/lib/video/ops.js
CHANGED
|
@@ -209,6 +209,8 @@ const OPS = {
|
|
|
209
209
|
},
|
|
210
210
|
},
|
|
211
211
|
apply_color: {
|
|
212
|
+
// Fora da ajuda (13/10): o `grade` mede e escolhe sozinho; este, de número à mão, segue funcionando.
|
|
213
|
+
oculto: true,
|
|
212
214
|
uso: `apply_color {ids|"all",preset?(${Object.keys(F.PRESETS_COR).join('|')}),exposure,contrast,saturation,temperature,tint(-1..1),gamma}`,
|
|
213
215
|
async run(ctx, a) {
|
|
214
216
|
precisaProjeto(ctx);
|
|
@@ -218,6 +220,35 @@ const OPS = {
|
|
|
218
220
|
});
|
|
219
221
|
},
|
|
220
222
|
},
|
|
223
|
+
/* O COLOR GRADING SOZINHO (cor.js, 13/10): mede cada mídia, corrige a
|
|
224
|
+
exposição e a dominante e põe o look de cinema — sem número chutado.
|
|
225
|
+
Troca o apply_color/grade que o clipe tinha. */
|
|
226
|
+
grade: {
|
|
227
|
+
uso: `grade {ids?|"all",look?(${require('./cor.js').LOOKS.join('|')}; sem: a imagem escolhe),intensidade?:0.8} — color grading de cinema medido do vídeo; depois inspect e OLHE`,
|
|
228
|
+
async run(ctx, a) {
|
|
229
|
+
precisaProjeto(ctx);
|
|
230
|
+
const C = require('./cor.js');
|
|
231
|
+
const ids = alvos(ctx.tl, { ...a, ids: a.ids || 'all' }).filter((id) => { const { clip } = T.achar(ctx.tl, id); return clip.asset && ctx.tl.assets[clip.asset] && clip.type !== 'audio'; });
|
|
232
|
+
const porAsset = {};
|
|
233
|
+
for (const id of ids) {
|
|
234
|
+
const { clip } = T.achar(ctx.tl, id);
|
|
235
|
+
if (porAsset[clip.asset]) continue;
|
|
236
|
+
const asset = ctx.tl.assets[clip.asset];
|
|
237
|
+
const m = await C.medir(asset.path);
|
|
238
|
+
if (!m.ok) return m;
|
|
239
|
+
const look = C.LOOKS.includes(a.look) ? a.look : C.escolherLook(m);
|
|
240
|
+
porAsset[clip.asset] = { look, ...C.correcao(m), intensidade: a.intensidade != null ? Number(a.intensidade) : 0.8 };
|
|
241
|
+
}
|
|
242
|
+
const r = await mutar(ctx, 'grade', (tl) => {
|
|
243
|
+
for (const id of ids) {
|
|
244
|
+
const { clip } = T.achar(tl, id);
|
|
245
|
+
const params = porAsset[clip.asset];
|
|
246
|
+
if (params) clip.effects = (clip.effects || []).filter((e) => e.type !== 'color.adjust' && e.type !== 'color.cine').concat({ type: 'color.cine', params });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return { ...r, grade: porAsset };
|
|
250
|
+
},
|
|
251
|
+
},
|
|
221
252
|
apply_effect: {
|
|
222
253
|
uso: `apply_effect {ids|"all",type(${Object.keys(F.EFEITOS).filter((k) => k !== 'color.adjust').join('|')}),params?,remove?:true}`,
|
|
223
254
|
async run(ctx, a) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.10.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/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/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"
|
|
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/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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
package/studio/edge.py
CHANGED
|
@@ -204,6 +204,11 @@ def sintetizar(texto, voz_cfg=None, timeout=90):
|
|
|
204
204
|
if r.returncode != 0 or not os.path.exists(mp3) or os.path.getsize(mp3) < 800:
|
|
205
205
|
msg = (r.stderr or b"").decode("utf-8", "replace")[-200:]
|
|
206
206
|
sys.stderr.write("[edge] síntese falhou: %s\n" % msg)
|
|
207
|
+
# 403 do Bing = edge-tts velho (13/10): atualiza em segundo plano, e a próxima fala já sai.
|
|
208
|
+
if "403" in msg:
|
|
209
|
+
threading.Thread(target=lambda: subprocess.run(
|
|
210
|
+
[sys.executable, "-m", "pip", "install", "--user", "--quiet", "--upgrade", "--break-system-packages", "edge-tts"],
|
|
211
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=240), daemon=True).start()
|
|
207
212
|
return None, None
|
|
208
213
|
with open(mp3, "rb") as fh:
|
|
209
214
|
dados = fh.read()
|
package/studio/narrar.py
CHANGED
|
@@ -317,6 +317,22 @@ def comando_roteiro(args):
|
|
|
317
317
|
try:
|
|
318
318
|
ps = _parte_edge(texto, v["voz"], reg["ritmo"], reg["tom"], mp3)
|
|
319
319
|
except Exception as e:
|
|
320
|
+
# 403 do Bing: a Microsoft trocou a autenticação e o edge-tts
|
|
321
|
+
# desta máquina ficou velho (13/10: o 7.2.8 levava 403, o
|
|
322
|
+
# 7.3.0 não). Sem isto, toda narração caía na voz robótica de
|
|
323
|
+
# reserva quando a cota do Gemini acabava. Atualiza uma vez e
|
|
324
|
+
# roda de novo num processo novo (o módulo velho já carregou).
|
|
325
|
+
if "403" in str(e) and not os.environ.get("PRIMO_EDGE_ATUALIZADO"):
|
|
326
|
+
for estrategia in ESTRATEGIAS:
|
|
327
|
+
try:
|
|
328
|
+
r = subprocess.run([sys.executable] + estrategia[:3] + ["--upgrade"] + estrategia[3:],
|
|
329
|
+
capture_output=True, timeout=240)
|
|
330
|
+
if r.returncode == 0:
|
|
331
|
+
break
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
os.environ["PRIMO_EDGE_ATUALIZADO"] = "1"
|
|
335
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
320
336
|
return {"ok": False, "error": f"a voz falhou na parte {i + 1}: {str(e)[:200]}"}
|
|
321
337
|
dur = _duracao(mp3)
|
|
322
338
|
wav = Path(tmp) / f"p{i:02d}.wav"
|