primocode 10.1.0 → 10.3.0-beta.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 +6 -26
- package/lib/act.js +5 -0
- package/lib/browser.js +1 -1
- package/lib/catalogo.js +17 -8
- package/lib/estudio/assets.js +75 -1
- package/lib/estudio/assinatura.js +108 -0
- package/lib/estudio/assinaturas/fluido.json +15 -0
- package/lib/estudio/assinaturas/gerativo.json +15 -0
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +52 -12
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/diretor.js +70 -4
- package/lib/estudio/edicao.js +34 -16
- package/lib/estudio/efeitos.js +95 -0
- package/lib/estudio/estilo.js +17 -6
- package/lib/estudio/falantes.js +97 -0
- package/lib/estudio/fluido.js +110 -16
- package/lib/estudio/gerativo.js +309 -85
- package/lib/estudio/hibrido.js +1 -1
- package/lib/estudio/index.js +252 -45
- package/lib/estudio/kit/fluido.jsx +130 -9
- package/lib/estudio/kit/gerativo.jsx +90 -21
- package/lib/estudio/kit/marca.jsx +58 -1
- package/lib/estudio/kit/tres.jsx +4 -1
- package/lib/estudio/kit.js +2 -0
- package/lib/estudio/plano.js +5 -1
- package/lib/estudio/print-site.js +152 -0
- package/lib/estudio/referencias.js +47 -25
- package/lib/estudio/remotion.js +74 -3
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variedade.js +83 -21
- package/lib/video/filters.js +6 -1
- package/lib/video/ops.js +4 -3
- package/lib/video/timeline.js +1 -1
- package/lib/video/zoom.js +275 -0
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +3 -5
package/lib/estudio/index.js
CHANGED
|
@@ -38,16 +38,23 @@ const hibrido = require('./hibrido.js');
|
|
|
38
38
|
const biblioteca = require('./biblioteca.js');
|
|
39
39
|
const referencias = require('./referencias.js');
|
|
40
40
|
const variedade = require('./variedade.js');
|
|
41
|
+
const diretor = require('./diretor.js');
|
|
42
|
+
const falantes = require('./falantes.js');
|
|
41
43
|
const variacao3d = require('./variacao3d.js');
|
|
42
44
|
const plano = require('./plano.js');
|
|
43
45
|
const remotion = require('./remotion.js');
|
|
44
46
|
const assets = require('./assets.js');
|
|
45
47
|
const video = require('./video.js');
|
|
48
|
+
const batida = require('./batida.js');
|
|
49
|
+
const revisor = require('./revisor.js');
|
|
50
|
+
const assinatura = require('./assinatura.js');
|
|
46
51
|
const identidade = require('./identidade.js');
|
|
47
52
|
const slides = require('./slides.js');
|
|
48
53
|
const edicao = require('./edicao.js');
|
|
49
54
|
const composicao = require('./composicao.js');
|
|
50
55
|
const acervoSite = require('./acervo-site.js');
|
|
56
|
+
const printSite = require('./print-site.js');
|
|
57
|
+
const efeitos = require('./efeitos.js');
|
|
51
58
|
const impacto = require('./impacto.js');
|
|
52
59
|
const cinema = require('./cinema.js');
|
|
53
60
|
const motion = require('./motion.js');
|
|
@@ -198,6 +205,17 @@ async function ferramentaProjeto(args, ctx = {}) {
|
|
|
198
205
|
return { ok: true };
|
|
199
206
|
}
|
|
200
207
|
|
|
208
|
+
/* O FEEDBACK que vira estilo: "adorei a virada", "texto demais" fica na
|
|
209
|
+
assinatura da pessoa (assinatura.js), e o próximo vídeo já parte dele.
|
|
210
|
+
Sem texto, devolve a assinatura (as faixas e as notas). */
|
|
211
|
+
if (acao === 'assinatura' || acao === 'feedback' || acao === 'gostei') {
|
|
212
|
+
const nome = args.nome || args.assinatura || 'fluido';
|
|
213
|
+
if (!args.texto) { const a = assinatura.ler(nome); return a ? { ok: true, assinatura: a } : { ok: false, error: `a assinatura "${nome}" não existe.` }; }
|
|
214
|
+
const r = assinatura.anotar(nome, args.texto);
|
|
215
|
+
if (r.ok && args.id) projeto.anotar(args.id, 'assinatura', `feedback gravado na assinatura ${nome}: ${args.texto}`);
|
|
216
|
+
return r;
|
|
217
|
+
}
|
|
218
|
+
|
|
201
219
|
const todos = projeto.listar({ limite: Number(args.limite) || 20 });
|
|
202
220
|
return {
|
|
203
221
|
ok: true,
|
|
@@ -260,7 +278,8 @@ function roteiroValido(r) {
|
|
|
260
278
|
const CINEMA = ['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'];
|
|
261
279
|
const NOVOS = ['logo', 'cores', 'produto']; // vox.js e lancamento.js
|
|
262
280
|
if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo) || NOVOS.includes(pt.tipo)))) return null;
|
|
263
|
-
|
|
281
|
+
// Duas vozes: roteiro.vozes e parte.falante, limpos (falantes.js). Sem eles, nada muda.
|
|
282
|
+
const obj = falantes.normalizar(Array.isArray(x) ? { partes } : x);
|
|
264
283
|
return partes.some((pt) => CINEMA.includes(pt.tipo)) && !obj.estilo ? { ...obj, estilo: 'cinema' } : obj;
|
|
265
284
|
}
|
|
266
285
|
|
|
@@ -363,6 +382,13 @@ async function ferramentaPlano(args) {
|
|
|
363
382
|
// O roteiro fica guardado: ajustar uma frase é mandar o roteiro de novo, não reescrever tudo.
|
|
364
383
|
try { fs.writeFileSync(path.join(p.pasta, 'roteiro.json'), JSON.stringify(original || args.roteiro, null, 2)); } catch { /* segue */ }
|
|
365
384
|
projeto.salvar(args.id, { duracao: conferida.duracao });
|
|
385
|
+
/* A memória da estrutura (diretor.cardapioEstruturas): o próximo roteiro
|
|
386
|
+
vê o que este usou. Uma vez por projeto — mandar o roteiro de novo
|
|
387
|
+
para ajustar uma frase não é um vídeo novo. */
|
|
388
|
+
if (!p.estrutura) {
|
|
389
|
+
const usada = diretor.registrarEstrutura(original || args.roteiro);
|
|
390
|
+
projeto.salvar(args.id, { estrutura: usada.estrutura || '-' });
|
|
391
|
+
}
|
|
366
392
|
return {
|
|
367
393
|
ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
|
|
368
394
|
proximo: 'estudio_render com este id. Para ajustar, mande o roteiro de novo (é rápido) — não edite as cenas à mão.',
|
|
@@ -435,6 +461,26 @@ async function ferramentaMidia(args) {
|
|
|
435
461
|
return { ok: true, src: relativo, metodo: r.metodo, aviso: r.aviso || undefined };
|
|
436
462
|
}
|
|
437
463
|
|
|
464
|
+
/* O print de verdade do site (print-site.js), para a peça 3D `printsite`.
|
|
465
|
+
Só roda quando o modelo pede: o cardápio diz quando vale. */
|
|
466
|
+
if (acao === 'print') {
|
|
467
|
+
if (!p) return { ok: false, error: 'para tirar o print é preciso dizer em qual projeto (id).' };
|
|
468
|
+
const r = await printSite.capturarSite(args.url || args.site || args.termo, { pasta: p.pasta, celular: !!args.celular, inteira: args.inteira !== false });
|
|
469
|
+
if (r.ok) projeto.anotar(p.id, 'midia', `print de ${r.url} (${r.tela}): ${r.src}`);
|
|
470
|
+
return r;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// O catálogo de efeitos que a IA escreveu (efeitos.js): guardar, listar, reusar.
|
|
474
|
+
if (acao === 'efeitos') return { ok: true, efeitos: efeitos.cardapio() };
|
|
475
|
+
if (acao === 'guardar-efeito' || acao === 'usar-efeito') {
|
|
476
|
+
if (!p) return { ok: false, error: 'diga em qual projeto (id).' };
|
|
477
|
+
const r = acao === 'guardar-efeito'
|
|
478
|
+
? efeitos.guardar({ pasta: p.pasta, arquivo: args.arquivo || args.src, nome: args.nome, descricao: args.descricao, tags: args.tags })
|
|
479
|
+
: efeitos.reusar({ pasta: p.pasta, nome: args.nome });
|
|
480
|
+
if (r.ok) projeto.anotar(p.id, 'midia', `${acao === 'guardar-efeito' ? 'guardou' : 'reusou'} o efeito ${r.nome || args.nome}`);
|
|
481
|
+
return r;
|
|
482
|
+
}
|
|
483
|
+
|
|
438
484
|
if (acao === 'logo') {
|
|
439
485
|
if (!p) return { ok: false, error: 'para baixar a logo é preciso dizer em qual projeto (id).' };
|
|
440
486
|
const nome = String(args.marca || args.nome || args.termo || '').trim();
|
|
@@ -666,7 +712,7 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
666
712
|
if ((args.estilo || 'viral') === 'viral' && args.recorte !== false) {
|
|
667
713
|
const recortes = {};
|
|
668
714
|
for (const [i, info] of montado.infos.entries()) {
|
|
669
|
-
if (!
|
|
715
|
+
if (!info.recorte || !info.forte || info.forte.peso < 9 || Object.keys(recortes).length >= 3) continue;
|
|
670
716
|
const de = Math.max(0, info.forte.de - 0.15);
|
|
671
717
|
const ate = Math.min(info.dur, de + 2.2);
|
|
672
718
|
if (ate - de < 1) continue;
|
|
@@ -715,6 +761,18 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
715
761
|
senão uma foto. Devolve {src, tipo} ou null. O clipe sai sorteado entre
|
|
716
762
|
os que os últimos vídeos não usaram (variedade.js), não o primeiro da
|
|
717
763
|
busca — "o b-roll veio igual nos dois vídeos" (Joel, 05/10). */
|
|
764
|
+
/** A foto editorial (Wikimedia, museu) que os últimos vídeos não usaram; o crédito fica no caderno. */
|
|
765
|
+
async function baixarEditorial(p, termo) {
|
|
766
|
+
try {
|
|
767
|
+
const b = await assets.procurarEditorial(termo, { quantas: 8 });
|
|
768
|
+
for (const item of variedade.ordenar(b.imagens, 'broll').slice(0, 4)) {
|
|
769
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'foto-' + termo });
|
|
770
|
+
if (d.ok) { variedade.marcar('broll', item.url); projeto.anotar(p.id, 'creditos', item.creditos); return { src: d.relativo, tipo: 'imagem' }; }
|
|
771
|
+
}
|
|
772
|
+
} catch { /* sem foto */ }
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
|
|
718
776
|
async function baixarApoio(p, termo, { orientacao = null, video: querVideo = false } = {}) {
|
|
719
777
|
if (querVideo) {
|
|
720
778
|
try {
|
|
@@ -749,11 +807,26 @@ async function variarIcones(plano, roteiro) {
|
|
|
749
807
|
const explicitos = [];
|
|
750
808
|
try { JSON.stringify(roteiro || {}, (k, v) => { if (k === 'icone' && typeof v === 'string') explicitos.push(v); return v; }); } catch { /* sem */ }
|
|
751
809
|
const troca = {};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
810
|
+
for (const nome of variedade.iconesTrocaveis(plano, explicitos)) {
|
|
811
|
+
/* Primeiro O ÍCONE: o conceito escolhe entre as formas dele
|
|
812
|
+
(edicao.ALTERNATIVAS) pelos pontos da variedade — o que os últimos
|
|
813
|
+
vídeos menos usaram. Depois A COLEÇÃO. */
|
|
814
|
+
const base = nome.split(':')[1];
|
|
815
|
+
let escolhido = base;
|
|
816
|
+
const alts = edicao.ALTERNATIVAS[base];
|
|
817
|
+
if (alts) {
|
|
818
|
+
const ordem = variedade.ordenar([base, ...alts].map((url) => ({ url })), 'icone-nome').map((x) => x.url);
|
|
819
|
+
for (const n of ordem) {
|
|
820
|
+
if (n === base) { escolhido = base; break; }
|
|
821
|
+
try { const r = await assets.icone('fluent-3d:' + n); if (r && r.ok) { escolhido = n; break; } } catch { /* tenta o próximo */ }
|
|
822
|
+
}
|
|
823
|
+
variedade.marcar('icone-nome', escolhido);
|
|
824
|
+
}
|
|
825
|
+
let novo = 'fluent-3d:' + escolhido;
|
|
826
|
+
if (colecao !== 'fluent-3d') {
|
|
827
|
+
try { const r = await assets.icone(colecao + ':' + escolhido); if (r && r.ok) novo = colecao + ':' + escolhido; } catch { /* fica o 3D */ }
|
|
756
828
|
}
|
|
829
|
+
if (novo !== nome) troca[nome] = novo;
|
|
757
830
|
}
|
|
758
831
|
const n = variedade.trocarIcones(plano, troca);
|
|
759
832
|
if (n || colecao === 'fluent-3d') variedade.marcar('icones', colecao);
|
|
@@ -941,6 +1014,20 @@ function narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge = false, semKokor
|
|
|
941
1014
|
});
|
|
942
1015
|
}
|
|
943
1016
|
|
|
1017
|
+
/* DUAS VOZES (falantes.js): os mp3 de cada falante, um depois do outro, num
|
|
1018
|
+
arquivo só — reamostrados, porque o Gemini sai a 24 kHz e o Edge não. A
|
|
1019
|
+
duração de cada um é MEDIDA: é ela que empurra os tempos do grupo seguinte. */
|
|
1020
|
+
async function juntarVozes(arquivos, mp3) {
|
|
1021
|
+
const fil = arquivos.map((_, i) => `[${i}:a]aresample=44100,aformat=channel_layouts=mono[a${i}]`).join(';')
|
|
1022
|
+
+ ';' + arquivos.map((_, i) => `[a${i}]`).join('') + `concat=n=${arquivos.length}:v=0:a=1[s]`;
|
|
1023
|
+
const c = await video.rodar(video.ffmpeg(), ['-y', ...arquivos.flatMap((a) => ['-i', a]), '-filter_complex', fil, '-map', '[s]',
|
|
1024
|
+
'-c:a', 'libmp3lame', '-q:a', '3', mp3], { timeout: 120000 });
|
|
1025
|
+
if (!c.ok) return { ok: false, error: 'não consegui juntar as vozes' };
|
|
1026
|
+
const duracoes = [];
|
|
1027
|
+
for (const a of arquivos) duracoes.push((await video.medir(a)).segundos);
|
|
1028
|
+
return { ok: true, duracoes };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
944
1031
|
/* A MÚSICA TEM LETRA? O acervo não diz se a faixa é instrumental, então ela
|
|
945
1032
|
é ouvida: 25 s do meio vão para a transcrição, e mais de 6 palavras é voz
|
|
946
1033
|
cantada. Sem servidor para ouvir, a faixa passa (melhor música que nenhuma). */
|
|
@@ -986,6 +1073,14 @@ async function sonsReais(p, video) {
|
|
|
986
1073
|
const PROPORCOES = { '16:9': [1920, 1080], '9:16': [1080, 1920], '1:1': [1080, 1080], '4:5': [1080, 1350] };
|
|
987
1074
|
|
|
988
1075
|
/** O roteiro vira plano: baixa o que falta, narra, e chama o compositor. */
|
|
1076
|
+
/* A ASSINATURA (assinatura.js): o vídeo é medido e conferido contra as
|
|
1077
|
+
faixas do estilo; o que saiu volta para o agente e fica no caderno. */
|
|
1078
|
+
function conferirAssinatura(p, r, nome) {
|
|
1079
|
+
const fora = assinatura.descrever(assinatura.conferir(nome, assinatura.medir(r.partitura, r.plano)).fora);
|
|
1080
|
+
r.resumo.assinatura = { nome, seguiu: !fora.length, fora: fora.length ? fora : undefined };
|
|
1081
|
+
projeto.anotar(p.id, 'assinatura', fora.length ? `fora da assinatura ${nome}: ${fora.join('; ')}` : `seguiu a assinatura ${nome}`);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
989
1084
|
async function prepararRoteiro(p, args) {
|
|
990
1085
|
const roteiro = typeof args.roteiro === 'string' ? JSON.parse(args.roteiro) : args.roteiro;
|
|
991
1086
|
if (Array.isArray(roteiro)) return prepararRoteiro(p, { ...args, roteiro: { partes: roteiro } });
|
|
@@ -1008,16 +1103,13 @@ async function prepararRoteiro(p, args) {
|
|
|
1008
1103
|
IMPACTO. O que o agente pediu vale mais que tudo. */
|
|
1009
1104
|
const pedidoTxt = `${p.pedido || ''} ${args.pedido || ''}`;
|
|
1010
1105
|
const ref = referencias.atual();
|
|
1011
|
-
/*
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
produto em 3D entra numa batida dele, logo abaixo). */
|
|
1019
|
-
if (ref.modo === 'sem' && estilo === 'hibrido') estilo = 'gerativo';
|
|
1020
|
-
void produtoNoPedido;
|
|
1106
|
+
/* Sem imagem é o padrão (07/10): sem o pedido dizer, nada de foto nem
|
|
1107
|
+
vídeo de fora. Ninguém escolheu o estilo? Ele gira (estilo.js) — sem
|
|
1108
|
+
imagem, só entre os que não vivem de foto. O híbrido e o cinema, pedidos
|
|
1109
|
+
pelo nome sem imagem, caem para o gerativo (sem nada baixado). */
|
|
1110
|
+
const semImagem = referencias.semImagem(ref);
|
|
1111
|
+
let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
|
|
1112
|
+
if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
|
|
1021
1113
|
/* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
|
|
1022
1114
|
com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
|
|
1023
1115
|
o agente tenha escolhido outro estilo pelo nome. */
|
|
@@ -1040,14 +1132,22 @@ async function prepararRoteiro(p, args) {
|
|
|
1040
1132
|
roteiro.semente = p.semente != null ? p.semente : Math.floor(Math.random() * 1e9);
|
|
1041
1133
|
if (p.semente == null) projeto.salvar(p.id, { semente: roteiro.semente });
|
|
1042
1134
|
}
|
|
1135
|
+
/* O humor, a paleta, as fontes e o gênero do gerativo, com memória entre
|
|
1136
|
+
vídeos (09/10, "vício em cores, músicas, fontes"): por PROJETO, como a semente. */
|
|
1137
|
+
// O assunto do vídeo é o rótulo da moldura (quando ela sai): o título do projeto.
|
|
1138
|
+
if (estilo === 'gerativo' && !roteiro.titulo && p.titulo) roteiro.titulo = p.titulo;
|
|
1139
|
+
if (estilo === 'gerativo' && !roteiro.escolhasGerativo) {
|
|
1140
|
+
roteiro.escolhasGerativo = p.escolhasGerativo || gerativo.escolherVisual(roteiro);
|
|
1141
|
+
if (!p.escolhasGerativo) projeto.salvar(p.id, { escolhasGerativo: roteiro.escolhasGerativo });
|
|
1142
|
+
}
|
|
1043
1143
|
if (ref.modo === 'fechado' && !roteiro.busca && referencias.termos().length) roteiro.busca = referencias.termos();
|
|
1044
1144
|
/* A BIBLIOTECA (biblioteca.js): sem fonte e cor da marca, o híbrido, o
|
|
1045
1145
|
cinema e o impacto caíam sempre no Inter e no azul e roxo — "os vídeos
|
|
1046
1146
|
todos iguais" (02/10). A marca manda; depois o roteiro.visual da IA;
|
|
1047
1147
|
sem nada, o sorteio que pula o que os últimos vídeos usaram. */
|
|
1048
1148
|
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(', '));
|
|
1149
|
+
const v = biblioteca.vestir(roteiro, { estilo });
|
|
1150
|
+
if (v.mudou) projeto.anotar(p.id, 'visual', [v.titulo && `fonte ${v.titulo} + ${v.texto} (tom ${v.tom})`, v.paleta && `paleta ${v.paleta}`].filter(Boolean).join(', '));
|
|
1051
1151
|
}
|
|
1052
1152
|
// Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
|
|
1053
1153
|
const ehMotion = LEVES.has(estilo);
|
|
@@ -1079,13 +1179,13 @@ async function prepararRoteiro(p, args) {
|
|
|
1079
1179
|
const videosMarca = [...(acervo.videos || [])];
|
|
1080
1180
|
const imagens = {}, fundos = {};
|
|
1081
1181
|
for (const [i, parte] of partes.entries()) {
|
|
1082
|
-
if (parte.tipo !== 'imagem' || parte.src) continue;
|
|
1182
|
+
if (semImagem || parte.tipo !== 'imagem' || parte.src) continue;
|
|
1083
1183
|
const texto = `${parte.titulo || ''} ${parte.sub || ''} ${parte.fala || ''} ${parte.busca || ''}`;
|
|
1084
1184
|
if (modo === 'video' && videosMarca.length && i % 2 === 1) { imagens[i] = { src: videosMarca.shift().src, tipo: 'video' }; continue; }
|
|
1085
1185
|
const m = melhorFoto(texto) || (parte.busca ? await baixarApoio(p, parte.busca, { orientacao: largura >= altura ? 'landscape' : 'portrait', video: modo === 'video' }) : null);
|
|
1086
1186
|
if (m) imagens[i] = m;
|
|
1087
1187
|
}
|
|
1088
|
-
if (acervo.fotos.length) {
|
|
1188
|
+
if (acervo.fotos.length && !semImagem) {
|
|
1089
1189
|
const comFundo = ['abertura', 'titulo', 'contraste', 'citacao', 'cta', 'passos'];
|
|
1090
1190
|
let alterna = 0;
|
|
1091
1191
|
for (const [i, parte] of partes.entries()) {
|
|
@@ -1109,7 +1209,13 @@ async function prepararRoteiro(p, args) {
|
|
|
1109
1209
|
// Só as partes com voz vão para a narração; o depoimento tem a voz do aluno.
|
|
1110
1210
|
// Só as partes com voz: o depoimento tem a voz do aluno, e a parte muda não tem voz.
|
|
1111
1211
|
// A parte com voz forte é narrada à parte: se entrasse aqui também, seria falada duas vezes (e muda uma delas).
|
|
1112
|
-
const
|
|
1212
|
+
const comVoz = partes.filter((pt) => pt.tipo !== 'depoimento' && cinema.falaDe(pt) !== '' && !pt.falaForte);
|
|
1213
|
+
const lista = comVoz.map((pt) => ({ texto: cinema.falaDe(pt), registro: 'normal' }));
|
|
1214
|
+
// Duas vozes: a voz de cada parte pelo falante dela (roteiro.vozes).
|
|
1215
|
+
const vozesL = falantes.vozesDasPartes(comVoz, roteiro, args.voz || roteiro.voz);
|
|
1216
|
+
const variasVozes = new Set(vozesL).size > 1;
|
|
1217
|
+
const DIRECAO_CINEMA = 'Leia em português do Brasil como narrador de trailer de marca premium: voz grave, confiante e humana, '
|
|
1218
|
+
+ 'ritmo firme, pausas curtas entre as frases, sem arrastar.';
|
|
1113
1219
|
const mp3 = path.join(p.pasta, 'midia', `narracao-${Date.now().toString(36)}.mp3`);
|
|
1114
1220
|
/* O CACHE DA VOZ: a mesma fala, na mesma voz, não é gerada duas vezes.
|
|
1115
1221
|
Refazer o roteiro em outro formato, ou mexer numa cena, não gasta a
|
|
@@ -1120,7 +1226,20 @@ async function prepararRoteiro(p, args) {
|
|
|
1120
1226
|
let v = null;
|
|
1121
1227
|
// motorVoz "edge": a voz do Edge direto, sem esperar a cota do Gemini ("a narração pode manter a do Edge", 25/09).
|
|
1122
1228
|
const soEdge = String(roteiro.motorVoz || args.motorVoz || '').toLowerCase() === 'edge';
|
|
1123
|
-
if (
|
|
1229
|
+
if (variasVozes) {
|
|
1230
|
+
/* Um grupo por falante, cada um com a sua voz e o seu cache (a chave
|
|
1231
|
+
do narrarComCache leva a voz). Falhou? Fica anotado, e a reserva
|
|
1232
|
+
de baixo narra numa voz só — melhor o vídeo do que nada. */
|
|
1233
|
+
v = await falantes.narrarPorFalante(lista.map((x, i) => ({ ...x, voz: vozesL[i] })), mp3, { juntar: juntarVozes,
|
|
1234
|
+
narrarUm: async (l, m, voz) => {
|
|
1235
|
+
const g = soEdge ? { ok: false } : await narrarComCache(l, m, { voz, direcao: DIRECAO_CINEMA });
|
|
1236
|
+
if (g.ok) return g;
|
|
1237
|
+
const a = m.replace(/\.mp3$/, '.json');
|
|
1238
|
+
fs.writeFileSync(a, JSON.stringify(l));
|
|
1239
|
+
return narrarReserva(l, m, a, voz, 'antonio', { soEdge });
|
|
1240
|
+
} });
|
|
1241
|
+
if (!v.ok) projeto.anotar(p.id, 'narracao', `as duas vozes falharam (${v.error}); saiu numa voz só`);
|
|
1242
|
+
} else if (soEdge) {
|
|
1124
1243
|
v = { ok: false, error: 'edge pedido' };
|
|
1125
1244
|
} else if (fs.existsSync(cacheMp3) && fs.existsSync(cacheJson)) {
|
|
1126
1245
|
fs.copyFileSync(cacheMp3, mp3);
|
|
@@ -1139,9 +1258,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1139
1258
|
} else if (kokoro.querKokoro(args.voz || roteiro.voz)) {
|
|
1140
1259
|
v = { ok: false, error: 'kokoro pedido' };
|
|
1141
1260
|
} else {
|
|
1142
|
-
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz,
|
|
1143
|
-
direcao: 'Leia em português do Brasil como narrador de trailer de marca premium: voz grave, confiante e humana, '
|
|
1144
|
-
+ 'ritmo firme, pausas curtas entre as frases, sem arrastar.' });
|
|
1261
|
+
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz, direcao: DIRECAO_CINEMA });
|
|
1145
1262
|
if (v.ok) {
|
|
1146
1263
|
try { fs.mkdirSync(cacheDir, { recursive: true }); fs.copyFileSync(mp3, cacheMp3); fs.writeFileSync(cacheJson, JSON.stringify({ voz: v.voz, segundos: v.segundos, partes: v.partes })); } catch { /* sem cache */ }
|
|
1147
1264
|
}
|
|
@@ -1316,12 +1433,14 @@ async function prepararRoteiro(p, args) {
|
|
|
1316
1433
|
// falaForte: true → a voz forte lê o texto da tela.
|
|
1317
1434
|
const textoForte = pt.falaForte === true ? cinema.falaDe({ ...pt, fala: undefined }) : pt.falaForte;
|
|
1318
1435
|
const soEdgeF = String(roteiro.motorVoz || args.motorVoz || '').toLowerCase() === 'edge';
|
|
1436
|
+
// A voz forte é a de quem fala a parte (duas vozes), não sempre a do narrador.
|
|
1437
|
+
const vozForte = falantes.vozesDasPartes([pt], roteiro, args.voz || roteiro.voz)[0];
|
|
1319
1438
|
const vf = soEdgeF ? await (async () => {
|
|
1320
1439
|
// No Edge, a ênfase é o registro "contraste": a mesma voz, um pouco mais firme.
|
|
1321
1440
|
const arqJ = arqF.replace(/\.mp3$/, '.json');
|
|
1322
1441
|
fs.writeFileSync(arqJ, JSON.stringify([{ texto: textoForte, registro: 'contraste' }]));
|
|
1323
|
-
return rodarPython('narrar.py', ['roteiro', '--arquivo', arqJ, '--saida', arqF, '--voz', String(
|
|
1324
|
-
})() : await narrarComCache([{ texto: textoForte }], arqF, { voz:
|
|
1442
|
+
return rodarPython('narrar.py', ['roteiro', '--arquivo', arqJ, '--saida', arqF, '--voz', String(vozForte || 'antonio')], { timeout: 5 * 60000 });
|
|
1443
|
+
})() : await narrarComCache([{ texto: textoForte }], arqF, { voz: vozForte,
|
|
1325
1444
|
// A MESMA voz do narrador, só com um pouco mais de ênfase: "não é mudar a voz, é só um pouco a entonação" (25/09).
|
|
1326
1445
|
direcao: pt.entonacao || 'Leia em português do Brasil com a mesma voz e o mesmo ritmo de um narrador de trailer de marca: '
|
|
1327
1446
|
+ 'confiante e natural, só um pouco mais de ênfase nas palavras principais. Sem dramatizar.' });
|
|
@@ -1356,8 +1475,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1356
1475
|
const mp3 = path.join(p.pasta, 'midia', `narracao-${Date.now().toString(36)}.mp3`);
|
|
1357
1476
|
// A voz de gente primeiro (Gemini, pelo servidor); o Edge é a reserva.
|
|
1358
1477
|
// Com cache: refazer o vídeo (outro estilo, outra proporção) não gasta a cota da voz.
|
|
1359
|
-
|
|
1360
|
-
direcao: estilo === 'gerativo'
|
|
1478
|
+
const direcao = estilo === 'gerativo'
|
|
1361
1479
|
/* O gerativo pede voz com CORPO ("narração encorpada, extravagante",
|
|
1362
1480
|
30/09): cheia, quente, perto do microfone — e o ritmo segue 1,12×. */
|
|
1363
1481
|
/* (A primeira versão pedia "trailer" e "ênfase sustentada": a voz saiu 25% mais lenta, medido em 30/09.) */
|
|
@@ -1371,9 +1489,19 @@ async function prepararRoteiro(p, args) {
|
|
|
1371
1489
|
+ 'voz encorpada, ênfase forte nas palavras de impacto, pausas curtas e secas entre as frases, sorrindo na voz.'
|
|
1372
1490
|
// "A narração tá lenta, deixa mais viva" (29/09): todo estilo fala com energia de reels.
|
|
1373
1491
|
: 'Leia em português do Brasil como narrador de reels de tecnologia: vivo, confiante e rápido, sorrindo na voz, '
|
|
1374
|
-
+ 'ênfase nas palavras que importam, pausas curtas entre as frases. Nada de leitura lenta ou solene.'
|
|
1375
|
-
|
|
1376
|
-
|
|
1492
|
+
+ 'ênfase nas palavras que importam, pausas curtas entre as frases. Nada de leitura lenta ou solene.';
|
|
1493
|
+
/* Uma voz por falante (falantes.js). Com uma voz só, um grupo só: é a
|
|
1494
|
+
narração de sempre, com o mesmo cache. */
|
|
1495
|
+
const vozesL = falantes.vozesDasPartes(partes, roteiro, args.voz || roteiro.voz);
|
|
1496
|
+
let v = await falantes.narrarPorFalante(lista.map((x, i) => ({ ...x, voz: vozesL[i] })), mp3, { juntar: juntarVozes,
|
|
1497
|
+
narrarUm: async (l, m, voz) => {
|
|
1498
|
+
const g = await narrarComCache(l, m, { voz, direcao });
|
|
1499
|
+
if (g.ok) return g;
|
|
1500
|
+
// O Kokoro pedido já foi tentado no narrarComCache: a reserva não repete.
|
|
1501
|
+
const a = m === mp3 ? arq : m.replace(/\.mp3$/, '.json');
|
|
1502
|
+
if (a !== arq) fs.writeFileSync(a, JSON.stringify(l));
|
|
1503
|
+
return narrarReserva(l, m, a, voz, 'thalita', { semKokoro: kokoro.querKokoro(voz) });
|
|
1504
|
+
} });
|
|
1377
1505
|
if (!v.ok) return v;
|
|
1378
1506
|
/* O RITMO DA VOZ. Mesmo com a direção, a voz sai no passo de leitura, e
|
|
1379
1507
|
o vídeo — que segue a voz palavra por palavra — sai lento junto. O
|
|
@@ -1408,9 +1536,8 @@ async function prepararRoteiro(p, args) {
|
|
|
1408
1536
|
funk para o fone, eletrônica para IA…), sorteia a faixa e não
|
|
1409
1537
|
repete as últimas usadas — nada de "inspiring corporate". */
|
|
1410
1538
|
let m = null;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
try { usadas = JSON.parse(fs.readFileSync(usadasArq, 'utf8')); } catch { usadas = []; }
|
|
1539
|
+
// A memória da trilha é a da variedade ('trilha'): antes era um arquivo à parte (trilhas-usadas.json).
|
|
1540
|
+
const usadas = variedade.usadas('trilha');
|
|
1414
1541
|
const mus = estilo === 'gerativo' ? gerativo.musicaDe(roteiro) : null;
|
|
1415
1542
|
/* A trilha do gerativo vem do Jamendo (Openverse), pelo GÊNERO do
|
|
1416
1543
|
assunto, instrumental, sorteada e sem repetir as últimas — a
|
|
@@ -1421,9 +1548,10 @@ async function prepararRoteiro(p, args) {
|
|
|
1421
1548
|
classical: 'piano classical instrumental', 'soul-rnb': 'soul instrumental' };
|
|
1422
1549
|
try {
|
|
1423
1550
|
const b = await assets.procurarMusica(BUSCA[roteiro.genero || mus.genero] || ((roteiro.genero || mus.genero) + ' instrumental'), { quantas: 8, duracaoMin: Math.max(45, segundos + 10) });
|
|
1424
|
-
|
|
1551
|
+
// Pelos pontos da variedade: a recente e a muito usada vão para o fim, não somem.
|
|
1552
|
+
const opcoes = variedade.ordenar((b.ok ? b.sons : []).filter((x) => !/vocal|voice|feat|ft\.|remix/i.test(x.titulo || '')), 'trilha');
|
|
1425
1553
|
for (let k = 0; k < opcoes.length && !m; k++) {
|
|
1426
|
-
const item = opcoes[
|
|
1554
|
+
const item = opcoes[k];
|
|
1427
1555
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha-' + (roteiro.genero || mus.genero) });
|
|
1428
1556
|
if (!d.ok) continue;
|
|
1429
1557
|
if (await temLetra(path.join(p.pasta, d.relativo))) { projeto.anotar(p.id, 'midia', `trilha com voz descartada: ${item.titulo}`); continue; }
|
|
@@ -1432,12 +1560,12 @@ async function prepararRoteiro(p, args) {
|
|
|
1432
1560
|
} catch { /* cai na ende.app */ }
|
|
1433
1561
|
}
|
|
1434
1562
|
if (mus && m) {
|
|
1435
|
-
|
|
1563
|
+
variedade.marcar('trilha', m.id);
|
|
1436
1564
|
projeto.anotar(p.id, 'trilha', `gênero ${roteiro.genero || mus.genero} (Jamendo)`);
|
|
1437
1565
|
} else if (mus) {
|
|
1438
1566
|
m = await ende.provedor({ pasta: path.join(p.pasta, 'midia') }).gerar({ clima: roteiro.clima || 'energetico', segundos, genero: roteiro.genero || mus.genero, semente: mus.semente, evitar: usadas });
|
|
1439
1567
|
const id = m && Number((/ende-(\d+)\.mp3$/.exec(m.src) || [])[1]);
|
|
1440
|
-
if (id)
|
|
1568
|
+
if (id) variedade.marcar('trilha', id);
|
|
1441
1569
|
if (m) projeto.anotar(p.id, 'trilha', `gênero ${roteiro.genero || mus.genero}`);
|
|
1442
1570
|
} else m = await ende.provedor({ pasta: path.join(p.pasta, 'midia') }).gerar({ clima: roteiro.clima || 'tecnologia', segundos });
|
|
1443
1571
|
if (m) { trilha = path.relative(p.pasta, m.src).replace(/\\/g, '/'); projeto.anotar(p.id, 'trilha', m.credito); }
|
|
@@ -1502,18 +1630,71 @@ async function prepararRoteiro(p, args) {
|
|
|
1502
1630
|
'-af', 'afade=t=in:st=0:d=0.4,loudnorm=I=-18:TP=-2:LRA=11', '-ar', '44100', norm], { timeout: 5 * 60000 });
|
|
1503
1631
|
if (c.ok && fs.existsSync(norm)) trilha = path.relative(p.pasta, norm).replace(/\\/g, '/');
|
|
1504
1632
|
}
|
|
1505
|
-
|
|
1633
|
+
/* O herói que o roteiro pediu do Iconify ("mdi:bread-slice"): o desenho é
|
|
1634
|
+
baixado aqui (fica em midia/icones); o que não baixar, a batida fica sem. */
|
|
1635
|
+
const icones = {};
|
|
1636
|
+
for (const pt of partes) {
|
|
1637
|
+
const h = typeof (pt.heroi != null ? pt.heroi : pt.icone) === 'string' ? String(pt.heroi != null ? pt.heroi : pt.icone).trim() : '';
|
|
1638
|
+
if (!/^[a-z0-9-]+:[a-z0-9-]+$/i.test(h) || icones[h]) continue;
|
|
1639
|
+
try { const ic = await assets.icone(h, { pasta: p.pasta }); if (ic && ic.ok) icones[h] = ic.svg; else projeto.anotar(p.id, 'icones', `herói ${h} não baixou: ${(ic && ic.error) || '?'}`); } catch { /* fica sem */ }
|
|
1640
|
+
}
|
|
1641
|
+
const r = gerativo.montarGerativo({ roteiro, largura, altura, tempos, narracao, sons, trilha, icones });
|
|
1506
1642
|
if (r.ok) {
|
|
1507
1643
|
const feitos = sonsLeves.prepararReceitas(p.pasta, r.somReceitas);
|
|
1508
1644
|
// Som que o ffmpeg não conseguiu fazer sai do plano (o vídeo não espera um arquivo que não existe).
|
|
1509
1645
|
r.plano.audio.efeitos = r.plano.audio.efeitos.filter((e) => Object.values(feitos).includes(e.src));
|
|
1510
1646
|
}
|
|
1511
1647
|
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}`);
|
|
1648
|
+
if (r.ok) conferirAssinatura(p, r, roteiro.assinatura || 'gerativo');
|
|
1512
1649
|
return r;
|
|
1513
1650
|
}
|
|
1514
1651
|
if (estilo === 'fluido' && modo === 'video') {
|
|
1515
|
-
|
|
1516
|
-
|
|
1652
|
+
/* A CARA de cada vídeo do fluido, por PROJETO (ajustar uma frase não
|
|
1653
|
+
troca): a semente (marcador, jeito das imagens) e a paleta, que a
|
|
1654
|
+
memória de variedade não repete do vídeo anterior (08/10: "sempre o
|
|
1655
|
+
mesmo azul"). A paleta ou o tema pedidos mandam. */
|
|
1656
|
+
if (roteiro.semente == null) {
|
|
1657
|
+
roteiro.semente = p.semente != null ? p.semente : Math.floor(Math.random() * 1e9);
|
|
1658
|
+
if (p.semente == null) projeto.salvar(p.id, { semente: roteiro.semente });
|
|
1659
|
+
}
|
|
1660
|
+
/* O modelo também vicia: pedia "noite" em todo vídeo (era a primeira da
|
|
1661
|
+
lista). A paleta dele vale, menos quando é uma das 2 últimas usadas —
|
|
1662
|
+
aí vem a menos usada do mesmo tema (escura ou clara). */
|
|
1663
|
+
if (!p.paletaFluido) {
|
|
1664
|
+
const P = fluido.PALETAS, nomes = Object.keys(P);
|
|
1665
|
+
const pedida = P[String(roteiro.paleta || '').toLowerCase()] ? String(roteiro.paleta).toLowerCase() : null;
|
|
1666
|
+
const recentes = variedade.usadas('paleta-fluido').slice(0, 2);
|
|
1667
|
+
const escuro = pedida ? P[pedida].escuro : /escuro|dark/i.test(roteiro.tema || '') ? true : /claro|light/i.test(roteiro.tema || '') ? false : null;
|
|
1668
|
+
let escolha = pedida && !recentes.includes(pedida) ? pedida : null;
|
|
1669
|
+
if (!escolha) escolha = variedade.sortear(nomes.filter((n) => escuro == null || P[n].escuro === escuro), 'paleta-fluido');
|
|
1670
|
+
else variedade.marcar('paleta-fluido', escolha);
|
|
1671
|
+
if (pedida && escolha !== pedida) projeto.anotar(p.id, 'visual', `paleta ${pedida} pedida, mas foi a de um dos últimos vídeos: ${escolha}`);
|
|
1672
|
+
projeto.salvar(p.id, { paletaFluido: escolha });
|
|
1673
|
+
p.paletaFluido = escolha;
|
|
1674
|
+
}
|
|
1675
|
+
roteiro.paleta = p.paletaFluido;
|
|
1676
|
+
// O trio de fontes, também por projeto e sem repetir os últimos vídeos (09/10, "vício em fontes").
|
|
1677
|
+
if (!p.fontesFluido) { p.fontesFluido = fluido.escolherFontes(roteiro.fontesFluido); projeto.salvar(p.id, { fontesFluido: p.fontesFluido }); }
|
|
1678
|
+
roteiro.fontesFluido = p.fontesFluido;
|
|
1679
|
+
/* As IMAGENS do fluido misto, só com imagem pedida: as partes com
|
|
1680
|
+
"busca", no máximo 1 em 3 e espalhadas (fluido.js decide o jeito). */
|
|
1681
|
+
const fotosFluido = {};
|
|
1682
|
+
if (!semImagem) {
|
|
1683
|
+
const comBusca = partes.map((pt, k) => (pt.busca || pt.src ? k : -1)).filter((k) => k >= 0 && (partes[k].tipo !== 'cta'));
|
|
1684
|
+
const teto = Math.max(1, Math.floor(partes.length / 3));
|
|
1685
|
+
const escolhidas = [];
|
|
1686
|
+
for (const k of comBusca) if (escolhidas.length < teto && !escolhidas.some((j) => Math.abs(j - k) < 2)) escolhidas.push(k);
|
|
1687
|
+
for (const k of escolhidas) {
|
|
1688
|
+
const pt = partes[k];
|
|
1689
|
+
// O acervo EDITORIAL (assets.procurarEditorial), não o banco de stock: sem foto que sirva, a parte fica sem imagem.
|
|
1690
|
+
const m = pt.src ? { src: pt.src } : await baixarEditorial(p, pt.busca);
|
|
1691
|
+
if (m && m.src) fotosFluido[k] = { src: m.src };
|
|
1692
|
+
}
|
|
1693
|
+
if (Object.keys(fotosFluido).length) projeto.anotar(p.id, 'midia', `fluido misto: imagem nas partes ${Object.keys(fotosFluido).map((k) => Number(k) + 1).join(', ')}`);
|
|
1694
|
+
}
|
|
1695
|
+
const r = fluido.montarFluido({ imagens: fotosFluido, roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
|
|
1696
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo fluido: ${r.resumo.batidas} batidas${r.resumo.imagens.length ? ` (imagens: ${r.resumo.imagens.join(', ')})` : ''}, 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`);
|
|
1697
|
+
if (r.ok) conferirAssinatura(p, r, roteiro.assinatura || 'fluido');
|
|
1517
1698
|
return r;
|
|
1518
1699
|
}
|
|
1519
1700
|
if (estilo === 'motion' && modo === 'video') {
|
|
@@ -1600,10 +1781,34 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
1600
1781
|
problemas: conferida.problemas.slice(0, 12) };
|
|
1601
1782
|
}
|
|
1602
1783
|
|
|
1784
|
+
/* A REVISÃO (revisor.js): o plano roda; agora, ele está bom? Conserta a
|
|
1785
|
+
receita (transição repetida, câmera sem respiro, som duplicado) e
|
|
1786
|
+
devolve o resto como aviso para quem fez o plano. */
|
|
1787
|
+
const revisao = revisor.revisar(pl, { semImagem: referencias.semImagem() });
|
|
1788
|
+
if (revisao.consertos.length) projeto.anotar(args.id, 'revisao', revisao.consertos.join('; '));
|
|
1789
|
+
if (revisao.avisos.length) projeto.anotar(args.id, 'revisao', 'avisos: ' + revisao.avisos.join('; '));
|
|
1790
|
+
|
|
1603
1791
|
// Os ícones vêm ANTES do render: um render que busca ícone na internet
|
|
1604
1792
|
// falha no meio, depois de vinte minutos.
|
|
1605
1793
|
const { mapa, faltaram } = await assets.iconesDoPlano(pl, p.pasta);
|
|
1606
1794
|
|
|
1795
|
+
/* A MÚSICA NO CORTE (batida.js): a faixa começa no ponto em que as
|
|
1796
|
+
batidas caem nos cortes do plano, e o corte de impacto num acento. Só
|
|
1797
|
+
quando ninguém disse o início (audio.trilha.inicio). */
|
|
1798
|
+
const tr = pl.audio && pl.audio.trilha;
|
|
1799
|
+
if (tr && !(typeof tr === 'object' && tr.inicio != null) && !(p.formato === 'slides' || p.formato === 'arte' || args.paginas)) {
|
|
1800
|
+
try {
|
|
1801
|
+
const src = path.join(p.pasta, tr.src || tr);
|
|
1802
|
+
const analise = await batida.analisar(src, { ffmpeg: video.ffmpeg() || 'ffmpeg' });
|
|
1803
|
+
if (analise && analise.batidas.length) {
|
|
1804
|
+
const duracao = (pl.cenas || []).reduce((n, c) => n + (Number(c.duracao) || 0), 0);
|
|
1805
|
+
const r = batida.melhorInicio(batida.cortesDoPlano(pl), analise, { maximo: Math.min(20, analise.segundos - duracao - 0.5) });
|
|
1806
|
+
pl.audio.trilha = { ...(typeof tr === 'object' ? tr : { src: tr }), inicio: r.inicio };
|
|
1807
|
+
projeto.anotar(args.id, 'trilha', `${analise.bpm} BPM; a faixa entra em ${r.inicio}s e ${r.alinhados} de ${r.total} cortes caem na batida`);
|
|
1808
|
+
}
|
|
1809
|
+
} catch { /* sem análise, a faixa entra do começo */ }
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1607
1812
|
const montado = remotion.montar({ pasta: p.pasta, titulo: p.titulo, plano: pl, icones: mapa });
|
|
1608
1813
|
|
|
1609
1814
|
if (args.somenteProjeto) {
|
|
@@ -1669,7 +1874,9 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
1669
1874
|
return {
|
|
1670
1875
|
ok: true, arquivo: saida, bytes, segundos: conferida.duracao,
|
|
1671
1876
|
faltaram: faltaram.length ? faltaram : undefined,
|
|
1672
|
-
|
|
1877
|
+
revisao: revisao.consertos.length || revisao.avisos.length ? { consertado: revisao.consertos, avisos: revisao.avisos } : undefined,
|
|
1878
|
+
aviso: 'ABERTO para o usuário assistir. Chame finish dizendo o que foi feito'
|
|
1879
|
+
+ (revisao.avisos.length ? ' — e cite os avisos da revisão que valem um novo plano.' : '.'),
|
|
1673
1880
|
};
|
|
1674
1881
|
}
|
|
1675
1882
|
|
|
@@ -1803,8 +2010,8 @@ async function ferramentaSite(args) {
|
|
|
1803
2010
|
return r;
|
|
1804
2011
|
}
|
|
1805
2012
|
|
|
1806
|
-
module.exports = {
|
|
2013
|
+
module.exports = { variarIcones,
|
|
1807
2014
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
1808
2015
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
1809
|
-
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro,
|
|
2016
|
+
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|
|
1810
2017
|
};
|