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/README.md
CHANGED
|
@@ -352,7 +352,7 @@ primocode -c # continua a última conversa salva (--co
|
|
|
352
352
|
|
|
353
353
|
```json
|
|
354
354
|
{
|
|
355
|
-
"server": "https://primocode-server.
|
|
355
|
+
"server": "https://primocode-server-production.up.railway.app",
|
|
356
356
|
"model": "main",
|
|
357
357
|
"token": null
|
|
358
358
|
}
|
|
@@ -361,7 +361,7 @@ primocode -c # continua a última conversa salva (--co
|
|
|
361
361
|
Ou por variável de ambiente — elas têm prioridade:
|
|
362
362
|
|
|
363
363
|
```bash
|
|
364
|
-
export PRIMOCODE_SERVER="https://primocode-server.
|
|
364
|
+
export PRIMOCODE_SERVER="https://primocode-server-production.up.railway.app"
|
|
365
365
|
export PRIMOCODE_MODEL="main"
|
|
366
366
|
export PRIMOCODE_TOKEN="..." # só se o servidor exigir
|
|
367
367
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -36,6 +36,7 @@ const skills = require('../lib/skills');
|
|
|
36
36
|
const checkpoint = require('../lib/checkpoint');
|
|
37
37
|
const comandosLib = require('../lib/comandos');
|
|
38
38
|
const marketplace = require('../lib/marketplace');
|
|
39
|
+
const portaoSkill = require('../lib/portao-skill');
|
|
39
40
|
const vozModo = require('../lib/voz');
|
|
40
41
|
const conta = require('../lib/conta');
|
|
41
42
|
const nuvem = require('../lib/nuvem');
|
|
@@ -1003,10 +1004,26 @@ async function instalarSkill() {
|
|
|
1003
1004
|
console.log('');
|
|
1004
1005
|
const caminho = String(await ask(' ' + c.brand('❯ ') + c.white('Caminho do SKILL.md: '))).trim();
|
|
1005
1006
|
if (!caminho) return;
|
|
1006
|
-
const r =
|
|
1007
|
+
const r = await instalarComPortao(caminho);
|
|
1007
1008
|
console.log(r.ok ? c.ok(` Skill instalada: ${r.nome}`) : c.err(` ${r.error}`));
|
|
1008
1009
|
}
|
|
1009
1010
|
|
|
1011
|
+
/* Instala passando pelo portão (lib/portao-skill.js). Achou algo: mostra cada
|
|
1012
|
+
achado com arquivo:linha e faz a mesma pergunta do que não tem volta
|
|
1013
|
+
(autorizarControle) — Enter sozinho é NÃO. */
|
|
1014
|
+
async function instalarComPortao(caminho) {
|
|
1015
|
+
const r = skills.instalarDe(caminho);
|
|
1016
|
+
if (!r.bloqueada) return r;
|
|
1017
|
+
console.log('');
|
|
1018
|
+
console.log(' ' + c.err(r.error));
|
|
1019
|
+
for (const linha of portaoSkill.descrever(r.achados).slice(0, 20)) console.log(' ' + c.warn(linha));
|
|
1020
|
+
if (r.achados.length > 20) console.log(c.dim(` … e mais ${r.achados.length - 20}`));
|
|
1021
|
+
console.log(' ' + c.dim('Leia cada linha: o portão acha padrão, não intenção.'));
|
|
1022
|
+
const resposta = await autorizarControle(`instalar a skill de ${caminho} mesmo assim`, { tipo: 'perigo' });
|
|
1023
|
+
if (resposta !== 'agora' && resposta !== 'sessao') return { ok: false, error: 'Skill não instalada.' };
|
|
1024
|
+
return skills.instalarDe(caminho, { forcar: true });
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1010
1027
|
async function sendPrompt(userText) {
|
|
1011
1028
|
/* O portão do plano. Sem ele o bloqueio da abertura seria só um texto na
|
|
1012
1029
|
tela: a pessoa leria "você não tem acesso" e mandaria a primeira coisa
|
|
@@ -2218,7 +2235,7 @@ async function handleSlash(text) {
|
|
|
2218
2235
|
console.log(c.dim(' Ou abra o ') + c.brand('/marketplace') + c.dim(' e escolha "New Skill" para escrever uma.'));
|
|
2219
2236
|
break;
|
|
2220
2237
|
}
|
|
2221
|
-
const r =
|
|
2238
|
+
const r = await instalarComPortao(rest);
|
|
2222
2239
|
if (!r.ok) { console.log(c.err(` ${r.error}`)); break; }
|
|
2223
2240
|
console.log(c.ok(` Skill instalada: ${r.nome}`) + c.muted(` (${shortPath(r.caminho)})`));
|
|
2224
2241
|
console.log(c.dim(' Já está ligada. /skills mostra todas.'));
|
package/lib/act.js
CHANGED
|
@@ -849,10 +849,13 @@ async function runAct(userPrompt, opts) {
|
|
|
849
849
|
* um diretor dentro do diretor é uma volta a mais para o mesmo lugar. */
|
|
850
850
|
if (!subagent && profundidade === 0) {
|
|
851
851
|
const diretor = require('./estudio/diretor.js');
|
|
852
|
+
// Vídeo que o Estúdio fez se edita no Remotion Studio, não no editor por linha do tempo.
|
|
853
|
+
const EDITAR_DO_ESTUDIO = 'SE ELA QUER EDITAR UM VÍDEO QUE O ESTÚDIO FEZ (não uma gravação dela): '
|
|
854
|
+
+ 'estudio_projeto acao:"editar" com o id do projeto abre o Remotion Studio no navegador; diga a pasta a ela e pare.\n';
|
|
852
855
|
const producao = diretor.precisaDeProducao(pedido);
|
|
853
856
|
if (diretor.precisaDeEdicao(pedido)) {
|
|
854
857
|
// A gravação da pessoa: o editor por linha do tempo (lib/video/), pelo run_shell.
|
|
855
|
-
messages.push({ role: 'system', content: 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
858
|
+
messages.push({ role: 'system', content: EDITAR_DO_ESTUDIO + 'PARA EDITAR A GRAVAÇÃO DA PESSOA use o editor por linha do tempo via run_shell: '
|
|
856
859
|
+ 'SE A PESSOA SÓ DISSE "edite esse vídeo" (sem dizer o quê), O VÍDEO É O PEDIDO: `primocode video import --json \'{"path":"<arquivo>"}\'`, '
|
|
857
860
|
+ 'depois `primocode video auto --json \'{}\'` (lê o que ela FALA na gravação e aplica cada efeito no momento em que diz), inspect, export e abra o mp4. '
|
|
858
861
|
+ 'Senão: '
|
|
@@ -887,7 +890,7 @@ async function runAct(userPrompt, opts) {
|
|
|
887
890
|
resposta é o modelo. Então isto é o caminho SE for produzir, e não
|
|
888
891
|
mais uma ordem de produzir. */
|
|
889
892
|
messages.push({ role: 'system', content:
|
|
890
|
-
'VOCÊ DECIDE o que entregar, pelo que a pessoa pediu: se ela quer só o roteiro, a ideia, um texto ou uma resposta, ESCREVA isso e pare — não produza vídeo. '
|
|
893
|
+
EDITAR_DO_ESTUDIO + 'VOCÊ DECIDE o que entregar, pelo que a pessoa pediu: se ela quer só o roteiro, a ideia, um texto ou uma resposta, ESCREVA isso e pare — não produza vídeo. '
|
|
891
894
|
+ `SE ela quer a peça pronta (formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}"), faça em três passos: `
|
|
892
895
|
+ `estudio_projeto acao:"criar" com formato "${producao.formato === 'apresentacao' ? 'slides' : 'video'}" → `
|
|
893
896
|
+ 'estudio_plano com "roteiro" (partes com tipo e fala; o PrimoCode faz layout, voz, ícones, música e sons) → '
|
package/lib/catalogo.js
CHANGED
|
@@ -77,7 +77,9 @@ const GRUPOS = [
|
|
|
77
77
|
resumo: 'vídeo, motion, slides e arte — a IA faz, e no fim abre o resultado pronto',
|
|
78
78
|
itens: [
|
|
79
79
|
{ tool: 'estudio_projeto', escreve: true, faz: 'abre, acha e lista os projetos do estúdio — cada peça é uma pasta que não some',
|
|
80
|
-
args: 'acao: criar | abrir | procurar | listar | anotar. '
|
|
80
|
+
args: 'acao: criar | abrir | procurar | listar | anotar | editar. '
|
|
81
|
+
+ 'editar: id (sem id, o mais novo) — quando o usuário quer EDITAR um vídeo que o Estúdio fez, '
|
|
82
|
+
+ 'abre o projeto no Remotion Studio (o editor, no navegador) e devolve a pasta. '
|
|
81
83
|
+ 'criar: titulo, pedido, formato (video|slides|arte). '
|
|
82
84
|
+ 'abrir: id — devolve o plano, o CADERNO (o que foi decidido e por quê) e os arquivos. '
|
|
83
85
|
+ 'procurar: texto — acha pelo que o usuário lembra ("aquele do PicPay"), inclusive por detalhe do meio do processo. '
|
|
@@ -89,8 +91,9 @@ const GRUPOS = [
|
|
|
89
91
|
+ 'Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens} passos{titulo,itens} '
|
|
90
92
|
+ 'contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras:[{rotulo,valor}]} cta{titulo,link} '
|
|
91
93
|
+ 'logo{titulo} cores{cores:[{nome,hex}]} produto{titulo,texto,destaque,selo,barras}. Em qualquer parte: destaque (a palavra-tese), fonte (a fonte citada). '
|
|
94
|
+
+ '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. '
|
|
92
95
|
+ 'estilo: hibrido (PADRÃO: foto e vídeo reais que não param + o motion por cima; dê busca:"termo em inglês" por parte para o b-roll) | 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; é o do modo sem referências e o de quem pede motion/animação; com modelo3d o produto 3D entra numa batida) | classico (o motion 3D antigo) | vox (explainer editorial: tinta sobre papel, câmera que conta a frase) | '
|
|
93
|
-
+ 'marca (brand reveal: brilho, letras em vetor, marca cromada, padrão) | lancamento (produto: cartões, painel, calendário) | impacto | cinema. '
|
|
96
|
+
+ '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|lavanda|creme ou tema claro|escuro no roteiro) | impacto | cinema. '
|
|
94
97
|
+ 'marca e lancamento aceitam visual:orbe|vetor|estrela|esfera|grade|site|padrao|cromo|cartoes|painel|cores numa parte. marca.nome dá o nome. '
|
|
95
98
|
+ 'voz (thalita|antonio|francisca). Ajuste fino: plano: {formato:{largura,altura,fps}, cenas:[…], audio:{…}} — ou cenas direto. '
|
|
96
99
|
+ 'cena (opcional): o número de UMA cena para trocar só ela, sem reescrever o resto. '
|
package/lib/config.js
CHANGED
|
@@ -273,6 +273,14 @@ function podarMemoriaGeral() {
|
|
|
273
273
|
} catch {}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/* O servidor saiu do Render pro Railway da Paris Group (05/10). Quem gravou o
|
|
277
|
+
endereço antigo no config.json vai pro novo: o Render vai ser desligado. */
|
|
278
|
+
const SERVIDOR = 'https://primocode-server-production.up.railway.app';
|
|
279
|
+
const SERVIDOR_ANTIGO = /^https?:\/\/primocode-server\.onrender\.com\/?$/;
|
|
280
|
+
function servidor(escolhido) {
|
|
281
|
+
return !escolhido || SERVIDOR_ANTIGO.test(String(escolhido).trim()) ? SERVIDOR : escolhido;
|
|
282
|
+
}
|
|
283
|
+
|
|
276
284
|
function loadConfig() {
|
|
277
285
|
ensureStructure();
|
|
278
286
|
let file = {};
|
|
@@ -280,7 +288,7 @@ function loadConfig() {
|
|
|
280
288
|
if (fs.existsSync(CONFIG_FILE)) file = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
281
289
|
} catch { file = {}; }
|
|
282
290
|
return {
|
|
283
|
-
server: process.env.PRIMOCODE_SERVER || file.server
|
|
291
|
+
server: servidor(process.env.PRIMOCODE_SERVER || file.server),
|
|
284
292
|
model: process.env.PRIMOCODE_MODEL || file.model || 'main',
|
|
285
293
|
// Esforço, estilo Claude Code (/effort). É o que o usuário escolhe; o
|
|
286
294
|
// modelo é derivado dele. Guardado para durar entre sessões.
|
|
@@ -363,7 +371,7 @@ function esquecerProjeto() {
|
|
|
363
371
|
module.exports = {
|
|
364
372
|
tokensDoAssunto, GENERICAS,
|
|
365
373
|
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
366
|
-
loadConfig, saveConfig, appendHistory,
|
|
374
|
+
loadConfig, saveConfig, appendHistory, servidor, SERVIDOR,
|
|
367
375
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
368
376
|
ensureStructure, slugify, stripAccents,
|
|
369
377
|
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
package/lib/estudio/assets.js
CHANGED
|
@@ -329,12 +329,18 @@ async function procurarMusica(termo, { quantas = 8, duracaoMin = 45 } = {}) {
|
|
|
329
329
|
return procurarSom(termo, { quantas, duracaoMin });
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
/* `livre`: só domínio público (CC0 e PDM). O efeito sonoro entra no vídeo sem
|
|
333
|
+
crédito, então não pode ser CC-BY, que o "commercial" deixa passar. */
|
|
334
|
+
function urlSom(termo, { quantas = 8, duracaoMin = 0, livre = false } = {}) {
|
|
335
|
+
let url = `https://api.openverse.org/v1/audio/?q=${encodeURIComponent(termo)}`
|
|
336
|
+
+ `&page_size=${quantas}` + (livre ? '&license=cc0,pdm' : '&license_type=commercial');
|
|
337
|
+
if (duracaoMin && duracaoMin < 30) url += '&length=short';
|
|
338
|
+
return url;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function procurarSom(termo, { quantas = 8, duracaoMin = 0, duracaoMax = 0, livre = false } = {}) {
|
|
333
342
|
try {
|
|
334
|
-
|
|
335
|
-
+ `&page_size=${quantas}&license_type=commercial`;
|
|
336
|
-
if (duracaoMin && duracaoMin < 30) url += '&length=short';
|
|
337
|
-
const r = await pegar(url, { json: true });
|
|
343
|
+
const r = await pegar(urlSom(termo, { quantas, duracaoMin, livre }), { json: true });
|
|
338
344
|
const sons = (r.results || [])
|
|
339
345
|
.filter((x) => !duracaoMax || !x.duration || x.duration / 1000 <= duracaoMax)
|
|
340
346
|
.filter((x) => !duracaoMin || !x.duration || x.duration / 1000 >= duracaoMin)
|
|
@@ -412,6 +418,6 @@ module.exports = {
|
|
|
412
418
|
extensaoPorConteudo,
|
|
413
419
|
pegar, guardar,
|
|
414
420
|
icone, procurarIcones, iconesDoPlano, COLECOES, procurarVideos, procurarMusica,
|
|
415
|
-
procurarImagens, procurarSom, baixar,
|
|
421
|
+
procurarImagens, procurarSom, urlSom, baixar,
|
|
416
422
|
FONTES, conferirFonte,
|
|
417
423
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* biblioteca.js — o cardápio de fonte e cor dos estilos de corte (híbrido,
|
|
3
|
+
* cinema, impacto), com memória do que os últimos vídeos usaram.
|
|
4
|
+
*
|
|
5
|
+
* "por falta de variedade a gente tá fazendo os vídeos todos iguais" (Joel, 02/10)
|
|
6
|
+
*
|
|
7
|
+
* Sem fonte e cor da marca, o cinema caía sempre no Inter e no gradiente
|
|
8
|
+
* azul e roxo (#2E81ED → #9567DB), e o híbrido é o padrão: todo vídeo saía
|
|
9
|
+
* com a mesma cara. Aqui a escolha segue esta ordem:
|
|
10
|
+
* 1. a marca (fontes e cores dela) — nunca é trocada;
|
|
11
|
+
* 2. o que a IA escolheu do cardápio (roteiro.visual);
|
|
12
|
+
* 3. um sorteio que pula o que os últimos vídeos já usaram.
|
|
13
|
+
*
|
|
14
|
+
* As 128 famílias foram conferidas no Google Fonts em 02/10, com o peso mais
|
|
15
|
+
* grosso de cada uma (o título do cinema é caixa alta em 800).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// ── as fontes: "Nome:peso máximo", por tipo ──────────────────────────────
|
|
25
|
+
const TIPOS = {
|
|
26
|
+
condensada: 'Anton:400, Antonio:700, Archivo Narrow:700, Barlow Condensed:900, Bebas Neue:400, Big Shoulders Display:900, '
|
|
27
|
+
+ 'Encode Sans Condensed:900, Fjalla One:400, Khand:700, League Gothic:400, Mohave:700, Oswald:700, Pathway Gothic One:400, '
|
|
28
|
+
+ 'Saira Condensed:900, Six Caps:400, Smooch Sans:900, Sofia Sans Condensed:900, Staatliches:400, Teko:700',
|
|
29
|
+
display: 'Alfa Slab One:400, Anybody:900, Archivo Black:400, Archivo:900, Black Ops One:400, Bricolage Grotesque:800, '
|
|
30
|
+
+ 'Chakra Petch:700, Chivo:900, Darker Grotesque:900, Epilogue:900, Exo 2:900, Familjen Grotesk:700, Funnel Display:800, '
|
|
31
|
+
+ 'Gabarito:900, Lexend:900, Mona Sans:900, Passion One:900, Red Hat Display:900, Righteous:400, Rubik:900, Russo One:400, '
|
|
32
|
+
+ 'Schibsted Grotesk:900, Shrikhand:400, Urbanist:900, Wix Madefor Display:800',
|
|
33
|
+
larga: 'Audiowide:400, Bagel Fat One:400, Bowlby One:400, Bungee Shade:400, Bungee:400, Dela Gothic One:400, Krona One:400, '
|
|
34
|
+
+ 'Lexend Zetta:900, Michroma:400, Monoton:400, Orbitron:900, Press Start 2P:400, Rubik Mono One:400, Silkscreen:700, Syne:800, Ultra:400',
|
|
35
|
+
texto: 'Afacad:700, Albert Sans:900, Barlow:900, Be Vietnam Pro:900, Figtree:900, Funnel Sans:800, Golos Text:900, '
|
|
36
|
+
+ 'Hanken Grotesk:900, Host Grotesk:800, Instrument Sans:700, Karla:800, Onest:900, Public Sans:900, Reddit Sans:900, '
|
|
37
|
+
+ 'Rethink Sans:800, Sofia Sans:900, Work Sans:900',
|
|
38
|
+
serifa: 'Abril Fatface:400, Arvo:700, Bitter:900, Bodoni Moda:900, Caprasimo:400, Cormorant Garamond:700, Crimson Pro:900, '
|
|
39
|
+
+ 'DM Serif Display:400, EB Garamond:800, Fraunces:900, Gloock:400, Instrument Serif:400, Libre Baskerville:700, '
|
|
40
|
+
+ 'Libre Bodoni:700, Libre Caslon Display:400, Literata:900, Lora:700, Merriweather:900, Newsreader:800, Playfair Display:900, '
|
|
41
|
+
+ 'Prata:400, Roboto Slab:900, Rozha One:400, Source Serif 4:900, Spectral:800, Yeseva One:400, Young Serif:400, Zilla Slab:700',
|
|
42
|
+
manuscrita: 'Caveat:700, Gloria Hallelujah:400, Kalam:700, Lobster:400, Marck Script:400, Nanum Pen Script:400, Pacifico:400, '
|
|
43
|
+
+ 'Permanent Marker:400, Rock Salt:400, Satisfy:400, Shadows Into Light:400, Yellowtail:400',
|
|
44
|
+
mono: 'Azeret Mono:900, DM Mono:400, Fragment Mono:400, Geist Mono:900, IBM Plex Mono:700, JetBrains Mono:800, '
|
|
45
|
+
+ 'Major Mono Display:400, Martian Mono:800, Red Hat Mono:700, Space Mono:700, VT323:400',
|
|
46
|
+
};
|
|
47
|
+
const FONTES = Object.entries(TIPOS).flatMap(([tipo, lista]) => lista.split(', ').map((x) => {
|
|
48
|
+
const [nome, peso] = x.split(':');
|
|
49
|
+
return { nome, tipo, peso: Number(peso) };
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
/* A largura média do caractere, em "em", por tipo — o caber() de
|
|
53
|
+
composicao.js usa para escolher o tamanho. A larga é a que estoura a borda. */
|
|
54
|
+
const LARGURA = { condensada: 0.46, display: 0.6, larga: 0.74, texto: 0.58, serifa: 0.55, manuscrita: 0.52, mono: 0.62 };
|
|
55
|
+
const larguraDe = (fonte) => { const f = acharFonte(fonte); return f ? LARGURA[f.tipo] : null; };
|
|
56
|
+
|
|
57
|
+
/* O título é caixa alta grossa: fonte de texto fina demais não serve. O
|
|
58
|
+
texto (etiquetas, nomes) é qualquer uma que se lê pequena. */
|
|
59
|
+
const TITULO = FONTES.filter((f) => ['condensada', 'display', 'larga'].includes(f.tipo) || (f.tipo === 'serifa' && f.peso >= 800)
|
|
60
|
+
|| (f.tipo === 'texto' && f.peso >= 900));
|
|
61
|
+
const TEXTO = FONTES.filter((f) => ['texto', 'mono'].includes(f.tipo) || (f.tipo === 'serifa' && f.peso >= 700) || (f.tipo === 'display' && f.peso >= 800));
|
|
62
|
+
|
|
63
|
+
// ── as paletas: fundo escuro e as duas cores do gradiente ────────────────
|
|
64
|
+
/* O cinema é escuro (o texto é branco), então todo fundo é escuro. Nada do
|
|
65
|
+
violeta de SaaS nem do neon: é o que denuncia vídeo feito por IA. */
|
|
66
|
+
const BASES = [
|
|
67
|
+
['brasa', '#120B08', '#FF5A1F', '#FFB23F'], ['ferrugem', '#15100D', '#C8553D', '#F2A65A'], ['mata', '#0B1410', '#2BB673', '#D8F05A'],
|
|
68
|
+
['oceano', '#071219', '#1E88E5', '#4FD1C5'], ['tijolo', '#170C0B', '#E2583E', '#F6D6AD'], ['limao', '#0E100A', '#C6F432', '#F4F1E8'],
|
|
69
|
+
['cobre', '#120E0C', '#D9822B', '#F3C98B'], ['sangue', '#100607', '#E5383B', '#FF9F1C'], ['musgo', '#0F120C', '#8DB255', '#E9C46A'],
|
|
70
|
+
['gelo', '#0A0F14', '#8EC5FF', '#F1F7FF'], ['sol-de-inverno', '#0D0D10', '#FFC93C', '#FF6B35'], ['cereja', '#14080B', '#FF4D6D', '#FFC2D1'],
|
|
71
|
+
['jade', '#06120F', '#00A878', '#9BE564'], ['areia', '#13110D', '#E9C46A', '#F4A261'], ['safira', '#070B16', '#2F6BFF', '#7FB2FF'],
|
|
72
|
+
['tangerina', '#110A06', '#FF7A00', '#FFD166'], ['pistache', '#0C110D', '#93C572', '#F0EAD6'], ['vinho', '#13070C', '#D1495B', '#FCB9B2'],
|
|
73
|
+
['petroleo', '#061214', '#0F9D9A', '#F2C14E'], ['ouro-velho', '#0F0D08', '#C9A227', '#F5E6B3'], ['coral', '#120A0A', '#FF6F59', '#FFD6A5'],
|
|
74
|
+
['asfalto', '#0E0F10', '#FF3B30', '#FFFFFF'], ['menta', '#08110E', '#3DDC97', '#E8FFF4'], ['lava', '#0F0705', '#FF3D00', '#FFB300'],
|
|
75
|
+
['ceu', '#08101A', '#56C3E8', '#F7F7F7'], ['terracota', '#140D0A', '#CB6843', '#EBD4B4'], ['abacate', '#0D100A', '#A3B18A', '#DAD7CD'],
|
|
76
|
+
['neve-e-fogo', '#0B0B0E', '#FFFFFF', '#FF4F00'], ['framboesa', '#12070D', '#E0245E', '#FFB3C6'], ['cacau', '#120D0A', '#A47148', '#F2D0A9'],
|
|
77
|
+
['esmeralda', '#04110B', '#10B981', '#FDE68A'], ['aco', '#0C0E11', '#9DB4C0', '#E0E1DD'], ['acafrao', '#110D05', '#F4B400', '#FF6D00'],
|
|
78
|
+
['lagoa', '#051214', '#48CAE4', '#CAF0F8'], ['papoula', '#110808', '#F94144', '#F9C74F'], ['oliva', '#0F0F09', '#B5A642', '#F1E9C9'],
|
|
79
|
+
['ardosia', '#0D1014', '#FF8C42', '#E8EEF2'], ['tinta-e-rosa', '#0E0B0C', '#FF8FAB', '#FFE5EC'], ['hortela-e-ouro', '#08120F', '#52B788', '#F6C453'],
|
|
80
|
+
['noite-azul', '#060A12', '#3A86FF', '#FFBE0B'], ['cinzas', '#101010', '#D9D9D9', '#FF5F1F'], ['pessego', '#120C0A', '#FFAD87', '#FFE3D3'],
|
|
81
|
+
['floresta', '#07100A', '#40916C', '#B7E4C7'], ['rubi', '#10060A', '#EF233C', '#EDF2F4'], ['mostarda', '#100E06', '#E1AD01', '#F6F1D1'],
|
|
82
|
+
['turquesa', '#061111', '#2EC4B6', '#FFBF69'], ['caramelo', '#120C07', '#DDA15E', '#FEFAE0'], ['grafite-limao', '#0F1011', '#D4FF3A', '#F2F2F2'],
|
|
83
|
+
['alvorada', '#0D0A0C', '#FF9E00', '#FF4D6D'], ['tempestade', '#0A0D10', '#6C9BD2', '#F2E8CF'],
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function rgb(hex) { const n = parseInt(String(hex).slice(1), 16); return [n >> 16 & 255, n >> 8 & 255, n & 255]; }
|
|
87
|
+
const hex = (c) => '#' + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
88
|
+
const misturar = (a, b, k) => hex(rgb(a).map((v, i) => v + (rgb(b)[i] - v) * k));
|
|
89
|
+
const luz = (c) => { const [r, g, b] = rgb(c); return (0.299 * r + 0.587 * g + 0.114 * b) / 255; };
|
|
90
|
+
|
|
91
|
+
/* Cada base vira quatro: como veio, o gradiente invertido, o fundo tingido
|
|
92
|
+
da cor principal e a monocromática (a cor e ela mais clara). */
|
|
93
|
+
const PALETAS = BASES.flatMap(([nome, fundo, a, b]) => [
|
|
94
|
+
{ nome, base: nome, fundo, grad: [a, b] },
|
|
95
|
+
{ nome: `${nome}-invertida`, base: nome, fundo, grad: [b, a] },
|
|
96
|
+
{ nome: `${nome}-tingida`, base: nome, fundo: misturar(fundo, a, 0.1), grad: [a, b] },
|
|
97
|
+
{ nome: `${nome}-mono`, base: nome, fundo, grad: [a, misturar(a, '#FFFFFF', 0.45)] },
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// ── a memória: o que os últimos vídeos já usaram ─────────────────────────
|
|
101
|
+
const ARQ = path.join(os.homedir(), '.primocode', 'studio', 'biblioteca-usada.json');
|
|
102
|
+
const LEMBRA = { titulo: 15, texto: 8, paleta: 20 }; // paleta conta pela base: "brasa-mono" depois de "brasa" é repetir
|
|
103
|
+
|
|
104
|
+
function lerMemoria(arq = ARQ) {
|
|
105
|
+
try { const m = JSON.parse(fs.readFileSync(arq, 'utf8')); return { titulo: [], texto: [], paleta: [], ...m }; }
|
|
106
|
+
catch { return { titulo: [], texto: [], paleta: [] }; }
|
|
107
|
+
}
|
|
108
|
+
function gravarMemoria(m, arq = ARQ) {
|
|
109
|
+
try { fs.mkdirSync(path.dirname(arq), { recursive: true }); fs.writeFileSync(arq, JSON.stringify(m)); } catch { /* sem memória, só sorteio */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const norm = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim();
|
|
113
|
+
function acharFonte(nome) { const n = norm(nome); return FONTES.find((f) => norm(f.nome) === n) || null; }
|
|
114
|
+
function acharPaleta(nome) { const n = norm(nome).replace(/\s+/g, '-'); return PALETAS.find((p) => p.nome === n) || null; }
|
|
115
|
+
|
|
116
|
+
/* O sorteio que pula os recentes. Se a lista inteira já foi usada, vale
|
|
117
|
+
qualquer uma — nunca fica sem. */
|
|
118
|
+
function sortear(lista, recentes, chave, sorte) {
|
|
119
|
+
const livres = lista.filter((x) => !recentes.includes(chave(x)));
|
|
120
|
+
const de = livres.length ? livres : lista;
|
|
121
|
+
return de[Math.floor(sorte() * de.length) % de.length];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Escolhe fonte e paleta para o roteiro e as põe em roteiro.marca, sem
|
|
126
|
+
* trocar nada que a marca já tem. Devolve o que escolheu e de onde veio.
|
|
127
|
+
* memoria: null não lê nem grava o disco (os testes); sorte: () => [0,1).
|
|
128
|
+
*/
|
|
129
|
+
function vestir(roteiro, { memoria = ARQ, sorte = Math.random } = {}) {
|
|
130
|
+
if (!roteiro || typeof roteiro !== 'object') return { mudou: false };
|
|
131
|
+
const marca = roteiro.marca || {};
|
|
132
|
+
const cores = marca.cores || {};
|
|
133
|
+
const visual = roteiro.visual || {};
|
|
134
|
+
const mem = memoria ? lerMemoria(memoria) : { titulo: [], texto: [], paleta: [] };
|
|
135
|
+
const escolha = { mudou: false };
|
|
136
|
+
|
|
137
|
+
if (!(marca.fontes && marca.fontes.titulo)) {
|
|
138
|
+
// A IA escolheu pelo nome: vale qualquer família do Google Fonts, mesmo fora do cardápio.
|
|
139
|
+
const titulo = visual.titulo ? String(visual.titulo) : sortear(TITULO, mem.titulo, (f) => f.nome, sorte).nome;
|
|
140
|
+
const textoLista = TEXTO.filter((f) => f.nome !== titulo);
|
|
141
|
+
const texto = visual.texto ? String(visual.texto) : sortear(textoLista, mem.texto, (f) => f.nome, sorte).nome;
|
|
142
|
+
marca.fontes = { ...(marca.fontes || {}), titulo, texto };
|
|
143
|
+
mem.titulo = [titulo, ...mem.titulo.filter((x) => x !== titulo)].slice(0, LEMBRA.titulo);
|
|
144
|
+
mem.texto = [texto, ...mem.texto.filter((x) => x !== texto)].slice(0, LEMBRA.texto);
|
|
145
|
+
Object.assign(escolha, { mudou: true, titulo, texto, fonteDaIA: !!visual.titulo });
|
|
146
|
+
}
|
|
147
|
+
// A cor da marca manda: com destaque ou gradiente declarado, a paleta não entra.
|
|
148
|
+
if (!(marca.gradiente && marca.gradiente.length >= 2) && !cores.destaque) {
|
|
149
|
+
const p = acharPaleta(visual.paleta) || sortear(PALETAS, mem.paleta, (x) => x.base, sorte);
|
|
150
|
+
marca.gradiente = [...p.grad];
|
|
151
|
+
marca.cores = { ...cores, fundo: cores.fundo || p.fundo, destaque: p.grad[1], apoio: p.grad[0] };
|
|
152
|
+
mem.paleta = [p.base, ...mem.paleta.filter((x) => x !== p.base)].slice(0, LEMBRA.paleta);
|
|
153
|
+
Object.assign(escolha, { mudou: true, paleta: p.nome, paletaDaIA: !!acharPaleta(visual.paleta) });
|
|
154
|
+
}
|
|
155
|
+
if (escolha.mudou) {
|
|
156
|
+
roteiro.marca = marca;
|
|
157
|
+
if (memoria) gravarMemoria(mem, memoria);
|
|
158
|
+
}
|
|
159
|
+
return escolha;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** O cardápio para a IA: nomes por tipo, e as paletas pela base. */
|
|
163
|
+
function cardapio() {
|
|
164
|
+
const porTipo = {};
|
|
165
|
+
for (const f of FONTES) (porTipo[f.tipo] = porTipo[f.tipo] || []).push(f.nome);
|
|
166
|
+
return {
|
|
167
|
+
fontes: porTipo,
|
|
168
|
+
paletas: BASES.map(([nome, fundo, a, b]) => `${nome} ${fundo} ${a} ${b}`),
|
|
169
|
+
variacoes: 'cada paleta também vem -invertida, -tingida e -mono (ex.: "brasa-mono")',
|
|
170
|
+
uso: 'roteiro.visual = {titulo, texto, paleta}. Sem visual, o PrimoCode sorteia e não repete os últimos vídeos.',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { vestir, cardapio, larguraDe, acharFonte, acharPaleta, FONTES, PALETAS, BASES, TITULO, TEXTO, luz, ARQ };
|
package/lib/estudio/cinema.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
'use strict';
|
|
28
28
|
|
|
29
29
|
const { caber } = require('./composicao.js');
|
|
30
|
+
const { minimoLeitura, sobra } = require('./legibilidade.js');
|
|
30
31
|
|
|
31
32
|
const r2 = (n) => Math.round(n * 100) / 100;
|
|
32
33
|
const tokens = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
@@ -34,6 +35,7 @@ const tokens = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').t
|
|
|
34
35
|
/** Quanto tempo um texto precisa ficar na tela para ser lido. Nunca menos que
|
|
35
36
|
0,3 s por palavra: a regra de legibilidade do /brag (Arthur, 28/09). */
|
|
36
37
|
const nPalavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
38
|
+
const SAIDA_MIN = 0.1, ENTRA_NUM = 0.4; // a saída mais curta; o número contando antes de ser lido
|
|
37
39
|
const leituraCheia = (t) => Math.max(0.55 + 0.22 * nPalavras(t), 0.3 * nPalavras(t));
|
|
38
40
|
|
|
39
41
|
/** A fala desta parte (o que a voz diz): a "fala", ou os textos da tela. */
|
|
@@ -113,7 +115,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
113
115
|
const PRETO = fundoMarca && luz(fundoMarca) < 0.25 ? fundoMarca : '#07070A';
|
|
114
116
|
const BRANCO = '#F5F5F7';
|
|
115
117
|
const DEST = grad[1];
|
|
116
|
-
const F = { titulo: (marca.fontes && marca.fontes.titulo) || 'Inter', texto: (marca.fontes && marca.fontes.texto) || 'Inter' };
|
|
118
|
+
const F = { titulo: (marca.fontes && marca.fontes.titulo) || 'Inter', texto: (marca.fontes && marca.fontes.texto) || 'Inter', destaque: 'Instrument Serif' };
|
|
117
119
|
const W = largura, H = altura, deitado = W >= H;
|
|
118
120
|
const M = Math.round(Math.min(W, H) * 0.08), W0 = W - 2 * M;
|
|
119
121
|
const base = Math.min(W, H);
|
|
@@ -180,26 +182,36 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
180
182
|
fundo: { tipo: 'gradiente', cores: [`rgba(7,7,10,${veu * 0.7})`, `rgba(7,7,10,${veu})`, `rgba(7,7,10,${Math.min(0.95, veu + 0.25)})`], angulo: 180 } });
|
|
181
183
|
};
|
|
182
184
|
// O texto: entra palavra por palavra no tempo da voz e fica; as palavras marcadas no gradiente.
|
|
183
|
-
|
|
185
|
+
/* SEM CARA DE IA (05/10): o texto sai como a pessoa escreveu (nada de caixa
|
|
186
|
+
alta forçada), um pouco menor e com mais respiro — e a palavra marcada é
|
|
187
|
+
UM acento, na serifa em itálico. Caixa alta só com "caixa: true". */
|
|
188
|
+
const pTexto = (els, texto, de, ate, { y = null, max = deitado ? H * 0.11 : W * 0.13, linhas = 3, peso = 700, caixa = false, marcar = [], cor = BRANCO, pal = [], alinhar = 'center', x = M, larg = W0, bater = false } = {}) => {
|
|
189
|
+
max *= 0.85; peso = Math.min(peso, 700);
|
|
184
190
|
// Frase longa (a narração na tela) vai em letra normal: em caixa alta ela encolhe e cansa.
|
|
185
|
-
if (caixa && String(texto).split(/\s+/).length > 7)
|
|
191
|
+
if (caixa && String(texto).split(/\s+/).length > 7) caixa = false;
|
|
186
192
|
// Frase longa ganha linhas (no vertical, até 5), para a letra não encolher.
|
|
187
193
|
if (String(texto).split(/\s+/).length > 7) linhas = Math.max(linhas, deitado ? 4 : 5);
|
|
188
194
|
const txt = caixa ? String(texto).toUpperCase() : String(texto);
|
|
189
|
-
const fit = caber(txt, { largura: larg * 0.
|
|
190
|
-
const h = fit.tamanho * 1.
|
|
195
|
+
const fit = caber(txt, { largura: larg * 0.78, linhas, max, fonte: F.titulo });
|
|
196
|
+
const h = fit.tamanho * 1.14 * fit.linhas;
|
|
191
197
|
const ws = txt.split(/\s+/);
|
|
192
198
|
/* Centralizado, a linha entra inteira: palavra por palavra, as que ainda
|
|
193
199
|
não foram ditas ocupam espaço invisível e o começo da frase aparece
|
|
194
200
|
torto, puxado para a esquerda. Alinhado à esquerda, palavra por palavra. */
|
|
195
201
|
const rev = ws.map((_, k) => (alinhar === 'center' ? 0 : r2(Math.max(0, (pal[k] != null ? pal[k] : de + 0.08 * k) - de))));
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
// Uma palavra só, e só em frase de mais de uma palavra (a palavra sozinha não tem com quem contrastar).
|
|
203
|
+
const idx = ws.length < 2 ? [] : ws.map((w, k) => (marcar.some((m) => tokens(w)[0] && tokens(m).includes(tokens(w)[0])) ? k : -1)).filter((k) => k >= 0).slice(0, 1);
|
|
204
|
+
/* Entra rápido, FICA parado o tempo de ler, e só então sai (a lei do
|
|
205
|
+
/brag). Cena curta encolhe a entrada e a saída, nunca a leitura. */
|
|
206
|
+
const vida = ate - de, ler = minimoLeitura(txt);
|
|
207
|
+
const ent = bater ? sobra(vida - SAIDA_MIN, ler, 0.08, 0.22) : 0, fe = ent / 0.22;
|
|
208
|
+
const saida = r2(sobra(vida - ent, ler, SAIDA_MIN, 0.25));
|
|
209
|
+
els.push({ tipo: 'texto', texto: txt, fonte: F.titulo, peso, tamanho: fit.tamanho, cor, entrelinha: 1.14, espacamento: caixa ? 1 : -0.5,
|
|
198
210
|
x: Math.round(x), y: Math.round(y == null ? (H - h) / 2 : y), largura: Math.round(larg), alinhamento: alinhar, de: r2(de), ate: r2(ate),
|
|
199
|
-
revelar: rev, ...(idx.length ? { destaques: idx, corDestaque: DEST } : {}),
|
|
200
|
-
anima: { opacidade: [{ t: 0, v: 1 }, { t: Math.max(0.01,
|
|
211
|
+
revelar: rev, ...(idx.length ? { destaques: idx, corDestaque: DEST, fonteDestaque: F.destaque } : {}),
|
|
212
|
+
anima: { opacidade: [{ t: 0, v: 1 }, { t: r2(Math.max(0.01, vida - saida)), v: 1 }, { t: r2(vida), v: 0 }], y: [{ t: 0, v: 10 }, { t: ate - de, v: -6, e: 'linear' }],
|
|
201
213
|
// O texto que BATE no fundo: entra grande e assenta de uma vez.
|
|
202
|
-
...(bater ? { escala: [{ t: 0, v: 1.35 }, { t: 0.12, v: 0.97, e: 'saida' }, { t:
|
|
214
|
+
...(bater ? { escala: [{ t: 0, v: 1.35 }, { t: r2(0.12 * fe), v: 0.97, e: 'saida' }, { t: r2(ent), v: 1 }] } : {}) } });
|
|
203
215
|
return h;
|
|
204
216
|
};
|
|
205
217
|
const linhaFina = (els, y, de, larg = W * 0.18) => els.push({ tipo: 'forma', forma: 'retangulo', x: Math.round((W - larg) / 2), y: Math.round(y), largura: Math.round(larg), altura: 2,
|
|
@@ -242,9 +254,14 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
242
254
|
com a voz é bom, ler é obrigatório. Se a leitura pede mais que a fala,
|
|
243
255
|
a cena cresce (medido: "EDIÇÕES" ficava meio segundo na tela). */
|
|
244
256
|
// Texto curto (um número, "3 DIAS.") se lê de relance: não espera a leitura cheia e não atrasa a voz.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
257
|
+
/* E nunca menos que a lei do /brag (0,8 s parado) mais a saída mínima; o
|
|
258
|
+
número ainda conta antes de ser lido (ENTRA_NUM). */
|
|
259
|
+
const minimo = (x) => Math.max(minimoLeitura(x) + SAIDA_MIN, Math.min(leitura(x), nPalavras(x) <= 3 ? 0.75 : 1.6)) + (p.tipo === 'numeros' ? ENTRA_NUM : 0);
|
|
260
|
+
const espacar = (passo = 0) => {
|
|
261
|
+
for (let k = 1; k < inicios.length; k++) inicios[k] = r2(Math.max(inicios[k], inicios[k - 1] + Math.max(passo, minimo(textosDaParte[k - 1]))));
|
|
262
|
+
if (inicios.length) dur = r2(Math.max(dur, inicios[inicios.length - 1] + Math.max(leitura(textosDaParte[inicios.length - 1]), minimo(textosDaParte[inicios.length - 1])) + 0.3));
|
|
263
|
+
};
|
|
264
|
+
espacar();
|
|
248
265
|
const fimDe = (k) => (k < inicios.length - 1 ? inicios[k + 1] : dur);
|
|
249
266
|
const palDo = (txt) => { const ws = String(txt).split(/\s+/); const a = pals.findIndex((w) => tokens(w.texto)[0] === tokens(ws[0])[0]); return a < 0 ? [] : ws.map((_, k) => (pals[a + k] ? rel(pals[a + k].de) : null)); };
|
|
250
267
|
|
|
@@ -256,11 +273,13 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
256
273
|
const rap = p.tipo === 'climax' ? 0.38 : (p.ritmo || 0.75);
|
|
257
274
|
let fimFotos = p.tipo === 'climax' && textosDaParte.length ? Math.max(1.8, inicios[0] - 0.4) : dur;
|
|
258
275
|
const fz = p.tipo === 'climax' ? fortes[i] : null;
|
|
276
|
+
// A virada começa quando as fotos acabam: o texto não perde a leitura para a montagem.
|
|
277
|
+
if (p.tipo === 'climax' && !fz && inicios.length) { inicios[0] = r2(Math.max(inicios[0], fimFotos)); espacar(1.2); }
|
|
259
278
|
if (fz) {
|
|
260
279
|
/* A voz forte entra DEPOIS da montagem, junto com o texto que bate. */
|
|
261
280
|
fimFotos = 1.4;
|
|
262
281
|
const ini2 = quando(textosDaParte, (fz.palavras || []).map((w) => ({ ...w, de: w.de + fimFotos })), fz.segundos, fimFotos);
|
|
263
|
-
ini2.forEach((v0, k) => { inicios[k] = k === 0 ? fimFotos : Math.max(v0, inicios[k - 1] + 1.2); });
|
|
282
|
+
ini2.forEach((v0, k) => { inicios[k] = k === 0 ? fimFotos : Math.max(v0, inicios[k - 1] + Math.max(1.2, minimo(textosDaParte[k - 1]))); });
|
|
264
283
|
dur = r2(Math.max(fimFotos + fz.segundos + 0.5, inicios[inicios.length - 1] + 1.6));
|
|
265
284
|
const tr = trechos[trechos.length - 1];
|
|
266
285
|
if (tr && tr.src === fz.src) tr.t = r2(t + fimFotos - 0.05);
|
|
@@ -301,9 +320,10 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
301
320
|
const larguraNum = String(mv[1] + m[1] + mv[3]).length * 0.8; // serifa larga: 0,62 ainda quebrava
|
|
302
321
|
const tam = Math.round(Math.min(deitado ? H * 0.24 : W * 0.3, (W0 * 0.96) / Math.max(1, larguraNum)));
|
|
303
322
|
const casas = /[.,]/.test(m[1]) ? 1 : 0;
|
|
323
|
+
const entN = r2(sobra(fimDe(k) - inicios[k], minimoLeitura(m[1]), 0.3, 0.6));
|
|
304
324
|
els.push({ tipo: 'texto', texto: mv[1] + m[1] + mv[3], fonte: F.titulo, peso: 800, tamanho: tam, cor: BRANCO, x: M, y: Math.round(H * 0.5 - tam * 0.75), largura: W0, alinhamento: 'center',
|
|
305
|
-
de: inicios[k], ate: fimDe(k), contar: { ate: Number(m[1].replace(',', '.')), dur:
|
|
306
|
-
anima: { opacidade: [{ t: 0, v: 0 }, { t: 0.25, v: 1 }], escala: [{ t: 0, v: 0.92 }, { t:
|
|
325
|
+
de: inicios[k], ate: fimDe(k), contar: { ate: Number(m[1].replace(',', '.')), dur: entN, casas, antes: mv[1].toUpperCase(), depois: mv[3] }, gradienteTexto: undefined,
|
|
326
|
+
anima: { opacidade: [{ t: 0, v: 0 }, { t: Math.min(0.25, entN), v: 1 }], escala: [{ t: 0, v: 0.92 }, { t: entN, v: 1, e: 'saida' }] } });
|
|
307
327
|
pTexto(els, m[2], inicios[k] + 0.1, fimDe(k), { y: H * 0.5 + tam * 0.3, max: base * 0.085, peso: 700, cor: DEST });
|
|
308
328
|
});
|
|
309
329
|
} else if (p.tipo === 'escada') {
|
|
@@ -330,7 +350,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
330
350
|
pFoto(els, foto(), 0, dur, { veu: 0.66, entrada: 'suave' });
|
|
331
351
|
som(t, 'whoosh', 0.3);
|
|
332
352
|
const tamN = Math.round(deitado ? H * 0.34 : W * 0.42);
|
|
333
|
-
els.push({ tipo: 'texto', texto: String(p.num), fonte: F.titulo, peso: 800, tamanho: tamN, cor:
|
|
353
|
+
els.push({ tipo: 'texto', texto: String(p.num), fonte: F.titulo, peso: 800, tamanho: tamN, cor: `${DEST}1F`, contorno: `${Math.max(3, Math.round(base * 0.004))}px ${DEST}`,
|
|
334
354
|
x: M, y: Math.round(deitado ? H * 0.12 : H * 0.14), largura: W0, alinhamento: deitado ? 'left' : 'center',
|
|
335
355
|
anima: { opacidade: [{ t: 0, v: 0 }, { t: 0.6, v: 1 }], x: [{ t: 0, v: -20 }, { t: dur, v: 20, e: 'linear' }] } });
|
|
336
356
|
const yT = deitado ? H * 0.5 : H * 0.42;
|
|
@@ -71,7 +71,8 @@ const FAMILIA_TIPO = (f) => (/condensed|narrow|anton|oswald|bebas|teko|league go
|
|
|
71
71
|
: /black|bricolage|archivo|sora|grotesk|syne|unbounded/i.test(f) ? 'display' : 'texto');
|
|
72
72
|
|
|
73
73
|
function caber(texto, { largura, linhas = 2, max = 200, min = 18, fonte = 'Inter' }) {
|
|
74
|
-
|
|
74
|
+
// A fonte da biblioteca sabe a própria largura; a de fora, pelo nome.
|
|
75
|
+
const cw = require('./biblioteca.js').larguraDe(fonte) || LARGURA[FAMILIA_TIPO(fonte)];
|
|
75
76
|
const palavras = String(texto || '').split(/\s+/).filter(Boolean);
|
|
76
77
|
for (let t = Math.round(max); t >= min; t -= 2) {
|
|
77
78
|
// Quebra de verdade, palavra por palavra: é o que o navegador fará.
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -389,7 +389,8 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
389
389
|
tipo: 'legenda', x: '50%', y: deitado ? '76%' : '68%', ancora: 'centro', fala,
|
|
390
390
|
estilo: {
|
|
391
391
|
fonte: 'Montserrat', peso: 900, tamanho: Math.round(base * (deitado ? 0.078 : 0.072)),
|
|
392
|
-
|
|
392
|
+
// Caixa normal (o que a pessoa disse, como disse) e a palavra forte na serifa em itálico.
|
|
393
|
+
destaque: e.destaque, corForte: e.forte, fonteForte: 'Instrument Serif', largura: '92%',
|
|
393
394
|
},
|
|
394
395
|
});
|
|
395
396
|
|
package/lib/estudio/estilo.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* hibrido O PADRÃO do vídeo: o cinema com o kit por cima — foto e vídeo reais
|
|
15
15
|
* que não param, e o motion por cima (hibrido.js)
|
|
16
16
|
* composicao a composição antiga, uma cena por parte (composicao.js)
|
|
17
|
+
* fluido minimalista: uma ideia por batida, a forma de uma vira a da próxima,
|
|
18
|
+
* sem corte (fluido.js). É o que "fluido", "minimalista", "clean" e "apple" pedem.
|
|
17
19
|
*
|
|
18
20
|
* Quem manda, em ordem: o estilo que o AGENTE pediu; o do roteiro (a menos
|
|
19
21
|
* que o pedido diga motion); o que o pedido descreve; e o motion.
|
|
@@ -21,15 +23,18 @@
|
|
|
21
23
|
|
|
22
24
|
'use strict';
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const variedade = require('./variedade.js');
|
|
27
|
+
|
|
28
|
+
const ESTILOS = ['hibrido', 'gerativo', 'motion', 'vox', 'marca', 'lancamento', 'impacto', 'cinema', 'composicao', 'fluido'];
|
|
25
29
|
// Nomes que o modelo usa para os mesmos estilos.
|
|
26
30
|
/* "motion" agora é o GERATIVO ("não pode ter template", 29–30/09); o motion
|
|
27
31
|
antigo segue pelo nome "classico" (e "limpo", o nome mais antigo dele). */
|
|
28
32
|
const APELIDOS = { motion: 'gerativo', generativo: 'gerativo', kinetic: 'gerativo', cinetico: 'gerativo', 'cinético': 'gerativo',
|
|
29
33
|
tipografico: 'gerativo', 'tipográfico': 'gerativo', animacao: 'gerativo', 'animação': 'gerativo',
|
|
30
34
|
classico: 'motion', 'clássico': 'motion', 'motion-classico': 'motion', motion3d: 'motion',
|
|
31
|
-
absurdo: 'hibrido', misto: 'hibrido', mix: 'hibrido', 'híbrido': 'hibrido', limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
|
|
35
|
+
absurdo: 'hibrido', misto: 'hibrido', mix: 'hibrido', 'híbrido': 'hibrido', limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', minimalista: 'fluido', minimal: 'fluido', clean: 'fluido', apple: 'fluido', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
|
|
32
36
|
|
|
37
|
+
const PEDE_FLUIDO = /\bfluid[oa]\b|minimalista|\bminimal\b|\bclean\b|\bapple\b/i;
|
|
33
38
|
const PEDE_MOTION = /\bmotion\b|anima[çc][ãa]o|animad[oa]/i;
|
|
34
39
|
const PEDE_VOX = /\bvox\b|explica|explainer|document[aá]rio|editorial|hist[oó]ria d[aoe]s?\b/i;
|
|
35
40
|
const PEDE_MARCA = /\bbrand|identidade visual|revela[çc][ãa]o d[ae] marca|\blogo(tipo|marca)?\b|vinheta de marca/i;
|
|
@@ -43,24 +48,35 @@ const normal = (e) => {
|
|
|
43
48
|
return ESTILOS.includes(n) ? n : null;
|
|
44
49
|
};
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
/* O RODÍZIO: quando ninguém escolheu ("vários estilos... que não sejam padrão
|
|
52
|
+
da IA", Joel 05/10), o padrão não é sempre o híbrido — gira entre os que
|
|
53
|
+
servem a qualquer roteiro, sem repetir os últimos (memória 'estilo'). */
|
|
54
|
+
const RODIZIO = ['hibrido', 'cinema', 'gerativo', 'impacto', 'fluido'];
|
|
55
|
+
|
|
56
|
+
function estiloDaVez(aleatorio = Math.random) {
|
|
57
|
+
return variedade.sortear(RODIZIO, 'estilo', { aleatorio }) || 'hibrido';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, aleatorio } = {}) {
|
|
47
61
|
const pedidoAgente = normal(agente);
|
|
48
62
|
if (pedidoAgente) return pedidoAgente;
|
|
49
63
|
const doRoteiro = normal(roteiro);
|
|
50
64
|
/* Pedir MOTION (ou animação) é pedir o gerativo — antes do vox, da marca e
|
|
51
65
|
do lançamento, que são leiaute fixo ("não pode ter template", 30/09). */
|
|
52
|
-
if (doRoteiro && !(doRoteiro !== 'gerativo' && PEDE_MOTION.test(pedido))) return doRoteiro;
|
|
66
|
+
if (doRoteiro && !(doRoteiro !== 'gerativo' && doRoteiro !== 'fluido' && PEDE_MOTION.test(pedido))) return doRoteiro;
|
|
67
|
+
// O fluido é um jeito de motion: "motion minimalista" é o fluido, não o gerativo.
|
|
68
|
+
if (PEDE_FLUIDO.test(pedido)) return 'fluido';
|
|
53
69
|
if (PEDE_MOTION.test(pedido)) return 'gerativo';
|
|
54
70
|
if (PEDE_VOX.test(pedido)) return 'vox';
|
|
55
71
|
if (PEDE_MARCA.test(pedido)) return 'marca';
|
|
56
72
|
if (PEDE_LANCAMENTO.test(pedido)) return 'lancamento';
|
|
57
73
|
/* Anúncio, reels e o resto: o HÍBRIDO. "Um mix da versão antiga (o reels
|
|
58
74
|
da imersão, no cinema) e da última (o kit) — absurdo, que nem parece
|
|
59
|
-
que foi IA" (29/09).
|
|
60
|
-
return 'hibrido';
|
|
75
|
+
que foi IA" (29/09). Com `girar`, o padrão roda (RODIZIO). */
|
|
76
|
+
return girar ? estiloDaVez(aleatorio) : 'hibrido';
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
/** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
|
|
64
|
-
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento']);
|
|
80
|
+
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido']);
|
|
65
81
|
|
|
66
|
-
module.exports = { escolherEstilo, ESTILOS, LEVES, PEDE_MOTION };
|
|
82
|
+
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO };
|