primocode 10.0.9 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +19 -2
- package/lib/act.js +5 -2
- package/lib/catalogo.js +5 -2
- package/lib/config.js +10 -2
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +37 -17
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/estilo.js +24 -8
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +21 -3
- package/lib/estudio/hibrido.js +16 -12
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +134 -23
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +15 -5
- package/lib/estudio/kit/index.jsx +2 -1
- package/lib/estudio/kit/tres.jsx +88 -21
- package/lib/estudio/kit.js +3 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +3 -4
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/remotion.js +51 -6
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +80 -2
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +15 -1
- package/lib/tools.js +31 -5
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/voz_kokoro.py +98 -0
package/lib/estudio/index.js
CHANGED
|
@@ -35,8 +35,10 @@ const { spawn } = require('child_process');
|
|
|
35
35
|
const projeto = require('./projeto.js');
|
|
36
36
|
const acervo = require('./acervo.js');
|
|
37
37
|
const hibrido = require('./hibrido.js');
|
|
38
|
+
const biblioteca = require('./biblioteca.js');
|
|
38
39
|
const referencias = require('./referencias.js');
|
|
39
40
|
const variedade = require('./variedade.js');
|
|
41
|
+
const variacao3d = require('./variacao3d.js');
|
|
40
42
|
const plano = require('./plano.js');
|
|
41
43
|
const remotion = require('./remotion.js');
|
|
42
44
|
const assets = require('./assets.js');
|
|
@@ -50,7 +52,9 @@ const impacto = require('./impacto.js');
|
|
|
50
52
|
const cinema = require('./cinema.js');
|
|
51
53
|
const motion = require('./motion.js');
|
|
52
54
|
const gerativo = require('./gerativo.js');
|
|
55
|
+
const fluido = require('./fluido.js');
|
|
53
56
|
const vox = require('./vox.js');
|
|
57
|
+
const kokoro = require('./kokoro.js');
|
|
54
58
|
const lancamento = require('./lancamento.js');
|
|
55
59
|
const glifos = require('./glifos.js');
|
|
56
60
|
const { escolherEstilo, LEVES } = require('./estilo.js');
|
|
@@ -153,9 +157,11 @@ function semProjeto(id) {
|
|
|
153
157
|
};
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
async function ferramentaProjeto(args) {
|
|
160
|
+
async function ferramentaProjeto(args, ctx = {}) {
|
|
157
161
|
const acao = String(args.acao || 'listar').toLowerCase();
|
|
158
162
|
|
|
163
|
+
if (acao === 'editar' || acao === 'editor' || acao === 'studio') return editarNoStudio(args, ctx);
|
|
164
|
+
|
|
159
165
|
if (acao === 'criar' || acao === 'novo') {
|
|
160
166
|
const r = projeto.criar({
|
|
161
167
|
titulo: args.titulo, pedido: args.pedido || args.titulo, formato: args.formato || 'video',
|
|
@@ -199,6 +205,28 @@ async function ferramentaProjeto(args) {
|
|
|
199
205
|
};
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
/* EDITAR um vídeo que o Estúdio fez: abre o projeto Remotion dele no
|
|
209
|
+
Remotion Studio. Sem id, é o projeto mais novo. */
|
|
210
|
+
async function editarNoStudio(args, ctx = {}) {
|
|
211
|
+
const p = args.id ? projeto.ler(args.id) : projeto.listar({ limite: 1 })[0];
|
|
212
|
+
if (!p) return semProjeto(args.id);
|
|
213
|
+
const raiz = path.join(p.pasta, 'remotion');
|
|
214
|
+
if (!fs.existsSync(path.join(raiz, 'package.json'))) {
|
|
215
|
+
// Projeto que ainda não virou Remotion: escreve agora, sem renderizar.
|
|
216
|
+
const m = await ferramentaRender({ id: p.id, somenteProjeto: true }, ctx);
|
|
217
|
+
if (!m.ok) return m;
|
|
218
|
+
}
|
|
219
|
+
const r = remotion.abrirStudio({ raiz, spawnar: ctx.spawnar });
|
|
220
|
+
if (!r.ok) return { ...r, pasta: raiz };
|
|
221
|
+
projeto.anotar(p.id, 'editor', 'abriu no Remotion Studio');
|
|
222
|
+
return {
|
|
223
|
+
ok: true, id: p.id, pasta: raiz, comando: r.comando,
|
|
224
|
+
aviso: 'O Remotion Studio está ABRINDO no navegador (geralmente http://localhost:3000). '
|
|
225
|
+
+ `Diga ao usuário onde está o projeto: ${raiz}. Lá ele mexe e renderiza pelo botão Render do Studio. `
|
|
226
|
+
+ 'Cuidado: estudio_render reescreve a pasta src/ a partir do plano e apaga o que ele mudou à mão.',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
202
230
|
function listarArquivos(pasta) {
|
|
203
231
|
const saida = {};
|
|
204
232
|
for (const sub of ['entrada', 'midia', 'saida']) {
|
|
@@ -317,6 +345,8 @@ async function ferramentaPlano(args) {
|
|
|
317
345
|
try { original = JSON.parse(typeof args.roteiro === 'string' ? args.roteiro : JSON.stringify(args.roteiro)); } catch { original = args.roteiro; }
|
|
318
346
|
const r = await prepararRoteiro(p, args);
|
|
319
347
|
if (!r.ok) return r;
|
|
348
|
+
const ic = await variarIcones(r.plano, original);
|
|
349
|
+
if (ic.trocados) projeto.anotar(p.id, 'icones', `coleção ${ic.colecao}: ${ic.trocados} ícones`);
|
|
320
350
|
let conferida = plano.conferir(r.plano, { pasta: p.pasta });
|
|
321
351
|
/* A rede de segurança: o elemento que não passa sai, e o vídeo segue.
|
|
322
352
|
Um defeito num ícone não pode custar o vídeo inteiro. */
|
|
@@ -384,7 +414,7 @@ async function ferramentaMidia(args) {
|
|
|
384
414
|
|
|
385
415
|
if (acao === 'fontes') {
|
|
386
416
|
if (args.conferir) return assets.conferirFonte(args.conferir);
|
|
387
|
-
return { ok: true, fontes: assets.FONTES,
|
|
417
|
+
return { ok: true, fontes: assets.FONTES, biblioteca: biblioteca.cardapio(),
|
|
388
418
|
nota: 'qualquer família do Google Fonts funciona — esta lista é para não repetir sempre as mesmas.' };
|
|
389
419
|
}
|
|
390
420
|
|
|
@@ -682,27 +712,54 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
682
712
|
}
|
|
683
713
|
|
|
684
714
|
/* A mídia de apoio de um termo: vídeo do Pexels quando pedido e houver,
|
|
685
|
-
senão
|
|
715
|
+
senão uma foto. Devolve {src, tipo} ou null. O clipe sai sorteado entre
|
|
716
|
+
os que os últimos vídeos não usaram (variedade.js), não o primeiro da
|
|
717
|
+
busca — "o b-roll veio igual nos dois vídeos" (Joel, 05/10). */
|
|
686
718
|
async function baixarApoio(p, termo, { orientacao = null, video: querVideo = false } = {}) {
|
|
687
719
|
if (querVideo) {
|
|
688
720
|
try {
|
|
689
|
-
const
|
|
690
|
-
|
|
721
|
+
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
722
|
+
const b = await assets.procurarVideos(t, { quantas: 8, orientacao });
|
|
723
|
+
return (b.ok ? b.videos : []).filter((v) => !v.duracao || v.duracao >= 3);
|
|
724
|
+
}, termo);
|
|
725
|
+
for (const item of itens.slice(0, 4)) {
|
|
691
726
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
|
|
692
|
-
if (d.ok) return { src: d.relativo, tipo: 'video' };
|
|
727
|
+
if (d.ok) { variedade.marcar('broll', item.url); return { src: d.relativo, tipo: 'video' }; }
|
|
693
728
|
}
|
|
694
729
|
} catch { /* cai para a foto */ }
|
|
695
730
|
}
|
|
696
731
|
try {
|
|
697
|
-
const
|
|
698
|
-
|
|
732
|
+
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
733
|
+
const b = await assets.procurarImagens(t, { quantas: 8, orientacao });
|
|
734
|
+
return b.ok ? b.imagens : [];
|
|
735
|
+
}, termo);
|
|
736
|
+
for (const item of itens.slice(0, 4)) {
|
|
699
737
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'apoio-' + termo });
|
|
700
|
-
if (d.ok) return { src: d.relativo, tipo: 'imagem' };
|
|
738
|
+
if (d.ok) { variedade.marcar('broll', item.url); return { src: d.relativo, tipo: 'imagem' }; }
|
|
701
739
|
}
|
|
702
740
|
} catch { /* sem apoio */ }
|
|
703
741
|
return null;
|
|
704
742
|
}
|
|
705
743
|
|
|
744
|
+
/* Os ícones automáticos trocam de coleção a cada vídeo (Joel, 05/10: "várias
|
|
745
|
+
opções de ícones... que não sejam padrão da IA"). O ícone que o roteiro
|
|
746
|
+
pediu pelo nome fica; o que não existe na coleção sorteada fica no 3D. */
|
|
747
|
+
async function variarIcones(plano, roteiro) {
|
|
748
|
+
const colecao = variedade.colecaoIcones();
|
|
749
|
+
const explicitos = [];
|
|
750
|
+
try { JSON.stringify(roteiro || {}, (k, v) => { if (k === 'icone' && typeof v === 'string') explicitos.push(v); return v; }); } catch { /* sem */ }
|
|
751
|
+
const troca = {};
|
|
752
|
+
if (colecao !== 'fluent-3d') {
|
|
753
|
+
for (const nome of variedade.iconesTrocaveis(plano, explicitos)) {
|
|
754
|
+
const novo = colecao + ':' + nome.split(':')[1];
|
|
755
|
+
try { const r = await assets.icone(novo); if (r && r.ok) troca[nome] = novo; } catch { /* fica o 3D */ }
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const n = variedade.trocarIcones(plano, troca);
|
|
759
|
+
if (n || colecao === 'fluent-3d') variedade.marcar('icones', colecao);
|
|
760
|
+
return { colecao, trocados: n };
|
|
761
|
+
}
|
|
762
|
+
|
|
706
763
|
/* ── A VOZ DO GEMINI ─────────────────────────────────────────────────────
|
|
707
764
|
* O roteiro inteiro numa chamada só (o plano grátis tem poucos pedidos por
|
|
708
765
|
* minuto), e as partes achadas depois na transcrição do próprio áudio: a
|
|
@@ -867,11 +924,23 @@ async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
|
867
924
|
const dir = path.join(os.homedir(), '.primocode', 'studio', 'cache-voz');
|
|
868
925
|
const cm = path.join(dir, chave + '.mp3'), cj = path.join(dir, chave + '.json');
|
|
869
926
|
if (fs.existsSync(cm) && fs.existsSync(cj)) { fs.copyFileSync(cm, mp3); return { ok: true, ...JSON.parse(fs.readFileSync(cj, 'utf8')) }; }
|
|
927
|
+
// A voz local pedida (voz: 'kokoro'): direto nela, sem gastar a cota do Gemini.
|
|
928
|
+
if (kokoro.querKokoro(voz)) return kokoro.narrar(lista, mp3, { voz, uv: acharUv() });
|
|
870
929
|
const v = await narrarComGemini(lista, mp3, { voz, direcao });
|
|
871
930
|
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 */ } }
|
|
872
931
|
return v;
|
|
873
932
|
}
|
|
874
933
|
|
|
934
|
+
/* A VOZ DE RESERVA, quando o Gemini falha: o Kokoro (local, sem cota) e
|
|
935
|
+
depois o Edge. A falha de uma passa para a próxima, nunca derruba. */
|
|
936
|
+
function narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge = false, semKokoro = false } = {}) {
|
|
937
|
+
const passos = kokoro.ordem(voz, process.env, { soEdge }).filter((n) => n !== 'gemini' && !(semKokoro && n === 'kokoro'));
|
|
938
|
+
return kokoro.corrente(passos, {
|
|
939
|
+
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
940
|
+
edge: () => rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz', String(kokoro.querKokoro(voz, {}) ? vozEdge : voz || vozEdge)], { timeout: 10 * 60000 }),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
875
944
|
/* A MÚSICA TEM LETRA? O acervo não diz se a faixa é instrumental, então ela
|
|
876
945
|
é ouvida: 25 s do meio vão para a transcrição, e mais de 6 palavras é voz
|
|
877
946
|
cantada. Sem servidor para ouvir, a faixa passa (melhor música que nenhuma). */
|
|
@@ -903,7 +972,7 @@ async function sonsReais(p, video) {
|
|
|
903
972
|
for (const termo of variedade.termosSfx(nome)) {
|
|
904
973
|
if (sons[nome] !== sint[nome]) break;
|
|
905
974
|
try {
|
|
906
|
-
const b = await assets.procurarSom(termo, { quantas: 10 });
|
|
975
|
+
const b = await assets.procurarSom(termo, { quantas: 10, livre: true });
|
|
907
976
|
for (const item of variedade.ordenar(b.ok ? b.sons : [], 'sfx').slice(0, 3)) {
|
|
908
977
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'sfx-real-' + nome });
|
|
909
978
|
if (d.ok) { sons[nome] = d.relativo; variedade.marcar('sfx', item.url); break; }
|
|
@@ -939,7 +1008,9 @@ async function prepararRoteiro(p, args) {
|
|
|
939
1008
|
IMPACTO. O que o agente pediu vale mais que tudo. */
|
|
940
1009
|
const pedidoTxt = `${p.pedido || ''} ${args.pedido || ''}`;
|
|
941
1010
|
const ref = referencias.atual();
|
|
942
|
-
|
|
1011
|
+
/* Ninguém escolheu? O padrão gira (estilo.js, RODIZIO) — menos no "sem
|
|
1012
|
+
referências", que só tem o motion e segue a regra de baixo. */
|
|
1013
|
+
let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: ref.modo !== 'sem' });
|
|
943
1014
|
/* "Sem referências, só motion": o híbrido vive de b-roll de fora, então
|
|
944
1015
|
cai para o motion — sem nada baixado. */
|
|
945
1016
|
const produtoNoPedido = /produto|an[uú]nci|lan[çc]a|headset|fone|celular|t[eê]nis|rel[oó]gio|garrafa|lata|app\b/i.test(pedidoTxt);
|
|
@@ -953,10 +1024,31 @@ async function prepararRoteiro(p, args) {
|
|
|
953
1024
|
if (/\b3\s?d\b|tridimensional/i.test(pedidoTxt) && (!args.estilo || estilo === 'gerativo' || estilo === 'lancamento')) {
|
|
954
1025
|
// O 3D mora numa batida do gerativo; o lançamento antigo, só pelo nome.
|
|
955
1026
|
if (!args.estilo && estilo !== 'lancamento') estilo = 'gerativo';
|
|
956
|
-
if (!roteiro.modelo3d) roteiro.modelo3d =
|
|
957
|
-
|
|
1027
|
+
if (!roteiro.modelo3d) roteiro.modelo3d = variacao3d.modeloDoPedido(pedidoTxt);
|
|
1028
|
+
}
|
|
1029
|
+
// O 3D de cada vídeo: acabamento, câmera, luz e ritmo sorteados sem repetir os últimos.
|
|
1030
|
+
/* Por PROJETO, não por chamada: mandar o roteiro de novo para ajustar uma
|
|
1031
|
+
frase não pode sortear outro 3D nem outro vídeo. Projeto novo, sorteio novo. */
|
|
1032
|
+
if (roteiro.modelo3d && !roteiro.variacao3d) {
|
|
1033
|
+
roteiro.variacao3d = p.variacao3d || variacao3d.escolherVariacao3d();
|
|
1034
|
+
if (!p.variacao3d) projeto.salvar(p.id, { variacao3d: roteiro.variacao3d });
|
|
1035
|
+
}
|
|
1036
|
+
/* A SEMENTE do gerativo saía do texto do roteiro: o mesmo roteiro dava o
|
|
1037
|
+
mesmo vídeo, batida por batida (05/10). Sem semente do roteiro, cada
|
|
1038
|
+
projeto sorteia a sua. */
|
|
1039
|
+
if (estilo === 'gerativo' && roteiro.semente == null) {
|
|
1040
|
+
roteiro.semente = p.semente != null ? p.semente : Math.floor(Math.random() * 1e9);
|
|
1041
|
+
if (p.semente == null) projeto.salvar(p.id, { semente: roteiro.semente });
|
|
958
1042
|
}
|
|
959
1043
|
if (ref.modo === 'fechado' && !roteiro.busca && referencias.termos().length) roteiro.busca = referencias.termos();
|
|
1044
|
+
/* A BIBLIOTECA (biblioteca.js): sem fonte e cor da marca, o híbrido, o
|
|
1045
|
+
cinema e o impacto caíam sempre no Inter e no azul e roxo — "os vídeos
|
|
1046
|
+
todos iguais" (02/10). A marca manda; depois o roteiro.visual da IA;
|
|
1047
|
+
sem nada, o sorteio que pula o que os últimos vídeos usaram. */
|
|
1048
|
+
if (['hibrido', 'cinema', 'impacto'].includes(estilo)) {
|
|
1049
|
+
const v = biblioteca.vestir(roteiro);
|
|
1050
|
+
if (v.mudou) projeto.anotar(p.id, 'visual', [v.titulo && `fonte ${v.titulo} + ${v.texto}`, v.paleta && `paleta ${v.paleta}`].filter(Boolean).join(', '));
|
|
1051
|
+
}
|
|
960
1052
|
// Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
|
|
961
1053
|
const ehMotion = LEVES.has(estilo);
|
|
962
1054
|
const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
|
|
@@ -1044,6 +1136,8 @@ async function prepararRoteiro(p, args) {
|
|
|
1044
1136
|
}
|
|
1045
1137
|
} catch { /* fica o alinhamento guardado */ }
|
|
1046
1138
|
}
|
|
1139
|
+
} else if (kokoro.querKokoro(args.voz || roteiro.voz)) {
|
|
1140
|
+
v = { ok: false, error: 'kokoro pedido' };
|
|
1047
1141
|
} else {
|
|
1048
1142
|
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz,
|
|
1049
1143
|
direcao: 'Leia em português do Brasil como narrador de trailer de marca premium: voz grave, confiante e humana, '
|
|
@@ -1056,7 +1150,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1056
1150
|
const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
|
|
1057
1151
|
// Ritmo NORMAL: o "citação" (-10%, respiro longo) esticou o vídeo para 125 s.
|
|
1058
1152
|
fs.writeFileSync(arq, JSON.stringify(lista.map((x) => ({ ...x, registro: 'normal' }))));
|
|
1059
|
-
v = await
|
|
1153
|
+
v = await narrarReserva(lista, mp3, arq, args.voz || roteiro.voz, 'antonio', { soEdge });
|
|
1060
1154
|
}
|
|
1061
1155
|
if (!v.ok) return v;
|
|
1062
1156
|
/* A DURAÇÃO que o roteiro pede ("60–75 s"): se a voz passa do que cabe,
|
|
@@ -1171,20 +1265,27 @@ async function prepararRoteiro(p, args) {
|
|
|
1171
1265
|
const proxy = `${slug}.leve.mp4`;
|
|
1172
1266
|
if (jaTem.includes(proxy)) out.push({ src: 'midia/' + proxy, tipo: 'video', cat: cat(termo), alt: termo });
|
|
1173
1267
|
else try {
|
|
1174
|
-
|
|
1175
|
-
|
|
1268
|
+
// Sorteado entre os não usados, com o termo variado (variedade.js, 05/10).
|
|
1269
|
+
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
1270
|
+
const b = await assets.procurarVideos(t, { quantas: 8, orientacao });
|
|
1271
|
+
return (b.ok ? b.videos : []).filter((v) => !v.duracao || v.duracao >= 4);
|
|
1272
|
+
}, termo);
|
|
1273
|
+
for (const item of itens.slice(0, 1)) {
|
|
1176
1274
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: slug });
|
|
1177
1275
|
const l = d.ok ? await leve(d.relativo, proxy) : null;
|
|
1178
|
-
if (l) out.push({ src: l, tipo: 'video', cat: cat(termo), alt: termo });
|
|
1276
|
+
if (l) { out.push({ src: l, tipo: 'video', cat: cat(termo), alt: termo }); variedade.marcar('broll', item.url); }
|
|
1179
1277
|
}
|
|
1180
1278
|
} catch { /* segue com a foto */ }
|
|
1181
1279
|
const foto = jaTem.find((f) => f.startsWith(slug + '-') && /\.(jpe?g|png|webp)$/i.test(f));
|
|
1182
1280
|
if (foto) out.push({ src: 'midia/' + foto, tipo: 'imagem', cat: cat(termo), alt: termo });
|
|
1183
1281
|
else try {
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1282
|
+
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
1283
|
+
const b = await assets.procurarImagens(t, { quantas: 8, orientacao });
|
|
1284
|
+
return b.ok ? b.imagens : [];
|
|
1285
|
+
}, termo);
|
|
1286
|
+
for (const item of itens.slice(0, 1)) {
|
|
1186
1287
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: slug });
|
|
1187
|
-
if (d.ok) out.push({ src: d.relativo, tipo: 'imagem', cat: cat(termo), alt: termo });
|
|
1288
|
+
if (d.ok) { out.push({ src: d.relativo, tipo: 'imagem', cat: cat(termo), alt: termo }); variedade.marcar('broll', item.url); }
|
|
1188
1289
|
}
|
|
1189
1290
|
} catch { /* sem */ }
|
|
1190
1291
|
return out;
|
|
@@ -1241,7 +1342,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1241
1342
|
const r = cinema.montarCinema({ roteiro, largura, altura, tempos: v.partes, narracao: path.relative(p.pasta, mp3).replace(/\\/g, '/'),
|
|
1242
1343
|
sons: sonsC, trilha: trilhaC, fotos: fotosC, fortes, ritmoLeitura: hib ? 0.55 : 1 });
|
|
1243
1344
|
if (r.ok && hib) {
|
|
1244
|
-
hibrido.enfeitar(r.plano, { extras: hib.extras, partes, marca: roteiro.marca || {}, largura, altura });
|
|
1345
|
+
hibrido.enfeitar(r.plano, { extras: hib.extras, partes, marca: roteiro.marca || {}, largura, altura, hud: roteiro.hud === true, particulas: roteiro.particulas === true });
|
|
1245
1346
|
r.resumo.estilo = 'hibrido';
|
|
1246
1347
|
}
|
|
1247
1348
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${hib ? 'hibrido' : 'cinema'}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
|
|
@@ -1263,18 +1364,23 @@ async function prepararRoteiro(p, args) {
|
|
|
1263
1364
|
? 'Leia em português do Brasil como locutor de reels de tecnologia: RÁPIDO e contínuo, com voz cheia, quente e encorpada, '
|
|
1264
1365
|
+ 'bem perto do microfone, muita energia e sorriso na voz. Ataque firme e ênfase curta e forte nas palavras de impacto; '
|
|
1265
1366
|
+ 'quase sem pausa entre as frases. Nunca arraste, nunca leia devagar nem solene.'
|
|
1367
|
+
// O fluido é o vídeo de produto: voz limpa e segura, e rápida ("mais rápido", 06/10).
|
|
1368
|
+
: estilo === 'fluido' ? 'Leia em português do Brasil como narrador de vídeo de produto premium: seguro, próximo e RÁPIDO, '
|
|
1369
|
+
+ 'frases limpas e contínuas, quase sem pausa entre elas. Nunca arraste, nunca leia devagar; sem tom de anúncio.'
|
|
1266
1370
|
: estilo === 'impacto' ? 'Leia em português do Brasil como locutor de comercial agressivo e vendedor: energia alta, ritmo rápido, '
|
|
1267
1371
|
+ 'voz encorpada, ênfase forte nas palavras de impacto, pausas curtas e secas entre as frases, sorrindo na voz.'
|
|
1268
1372
|
// "A narração tá lenta, deixa mais viva" (29/09): todo estilo fala com energia de reels.
|
|
1269
1373
|
: 'Leia em português do Brasil como narrador de reels de tecnologia: vivo, confiante e rápido, sorrindo na voz, '
|
|
1270
1374
|
+ 'ênfase nas palavras que importam, pausas curtas entre as frases. Nada de leitura lenta ou solene.' });
|
|
1271
|
-
|
|
1375
|
+
// O Kokoro pedido já foi tentado no narrarComCache: a reserva não repete.
|
|
1376
|
+
if (!v.ok) v = await narrarReserva(lista, mp3, arq, args.voz || roteiro.voz, 'thalita', { semKokoro: kokoro.querKokoro(args.voz || roteiro.voz) });
|
|
1272
1377
|
if (!v.ok) return v;
|
|
1273
1378
|
/* O RITMO DA VOZ. Mesmo com a direção, a voz sai no passo de leitura, e
|
|
1274
1379
|
o vídeo — que segue a voz palavra por palavra — sai lento junto. O
|
|
1275
1380
|
áudio acelera 1,12× (1,3× ficou corrido demais, 29/09; atempo não muda o tom) e cada tempo de palavra
|
|
1276
1381
|
acompanha; `roteiro.ritmoVoz` ajusta (1 desliga). */
|
|
1277
|
-
|
|
1382
|
+
// O fluido corre mais ("mais rápido", 06/10): 1,22× por padrão.
|
|
1383
|
+
const ritmo = Math.max(1, Math.min(1.5, Number(roteiro.ritmoVoz || args.ritmoVoz) || (estilo === 'fluido' ? 1.22 : 1.12)));
|
|
1278
1384
|
if (ritmo > 1.01) {
|
|
1279
1385
|
const rap = mp3.replace(/\.mp3$/, '-r.mp3');
|
|
1280
1386
|
const c = await video.rodar(video.ffmpeg(), ['-y', '-i', mp3, '-filter:a', `atempo=${ritmo.toFixed(3)}`, rap], { timeout: 120000 });
|
|
@@ -1405,6 +1511,11 @@ async function prepararRoteiro(p, args) {
|
|
|
1405
1511
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo gerativo: ${r.resumo.batidas} batidas (${r.resumo.composicoes.join(' → ')}), ${r.resumo.segundos}s, semente ${r.resumo.semente}`);
|
|
1406
1512
|
return r;
|
|
1407
1513
|
}
|
|
1514
|
+
if (estilo === 'fluido' && modo === 'video') {
|
|
1515
|
+
const r = fluido.montarFluido({ roteiro, largura, altura, tempos, narracao, sons, trilha });
|
|
1516
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo fluido: ${r.resumo.batidas} batidas, paleta ${r.resumo.paleta}, ${r.resumo.segundos}s, batida atrasada no máximo ${r.resumo.atraso}s, palavra atrás da voz no máximo ${r.resumo.sincronia}s`);
|
|
1517
|
+
return r;
|
|
1518
|
+
}
|
|
1408
1519
|
if (estilo === 'motion' && modo === 'video') {
|
|
1409
1520
|
const r = motion.montarMotion({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
|
|
1410
1521
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo motion: ${r.resumo.segundos}s contínuos, ${r.resumo.partes} partes, ${r.resumo.fps} fps`);
|