primocode 8.38.1 → 8.40.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/lib/act.js +87 -2
- package/lib/audio-livre.js +172 -0
- package/lib/catalogo.js +7 -0
- package/lib/marca.js +280 -0
- package/lib/referencia.js +210 -63
- package/lib/remotion.js +245 -45
- package/lib/studio.js +76 -3
- package/lib/tools.js +13 -0
- package/package.json +2 -2
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/arquivos/tela-motion-3ecaa.json +135 -0
- package/studio/arquivos/tela-motion-d6150.json +135 -0
- package/studio/edge.py +66 -16
- package/studio/ferramentas/01-modelos.md +20 -2
- package/studio/ferramentas/corte.md +140 -35
- package/studio/ferramentas/tela.md +21 -0
- package/studio/server.py +47 -11
- package/studio/studio.log +28 -0
- package/studio/web/js/player.js +30 -2
- package/studio/web/js/render.js +74 -4
- package/studio/web/js/sfx.js +128 -2
- package/studio/web/render.css +95 -0
package/lib/act.js
CHANGED
|
@@ -19,7 +19,13 @@ const cfg = require('./config');
|
|
|
19
19
|
const catalogo = require('./catalogo');
|
|
20
20
|
const repeticao = require('./repeticao');
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
|
|
23
|
+
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
24
|
+
// de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
|
|
25
|
+
const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
|
|
26
|
+
// Quantas vezes o agente é EMPURRADO quando para no meio para narrar
|
|
27
|
+
// ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
|
|
28
|
+
const MAX_EMPURROES = 3;
|
|
23
29
|
// Voltas seguidas sem executar nada antes de desistir.
|
|
24
30
|
const MAX_RODADAS_PARADAS = 3;
|
|
25
31
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
@@ -182,6 +188,20 @@ function logResult(kind, result, profundidade) {
|
|
|
182
188
|
}
|
|
183
189
|
return;
|
|
184
190
|
}
|
|
191
|
+
case 'buscar_audio': {
|
|
192
|
+
console.log(p + c.muted(`${result.tipo === 'efeito' ? 'efeito' : 'música'}: `) + c.white(`${result.titulo}`)
|
|
193
|
+
+ c.muted(` — ${result.autor || 'autor desconhecido'}${result.duracao ? ` · ${result.duracao}s` : ''} · ${result.licenca}`));
|
|
194
|
+
console.log(p + c.dim(`${result.url}`));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
case 'marca_da_empresa': {
|
|
198
|
+
const cores = (result.cores && result.cores.paleta || []).slice(0, 4).join(' ');
|
|
199
|
+
console.log(p + c.white(result.nome || '') + c.muted(` · ${result.site || ''}`));
|
|
200
|
+
console.log(p + c.dim(`cor ${(result.cores && result.cores.principal) || '—'}${cores ? ` · paleta ${cores}` : ''}`
|
|
201
|
+
+ `${(result.fontes || []).length ? ` · fonte ${result.fontes[0]}` : ''}`
|
|
202
|
+
+ `${result.logo ? ` · logo ${result.logo.formato} (${result.logo.fonte})` : ' · sem logo'}`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
185
205
|
case 'desktop_windows': {
|
|
186
206
|
console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
|
|
187
207
|
for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
|
|
@@ -338,6 +358,39 @@ async function pausar(ms, subagent) {
|
|
|
338
358
|
if (sp) sp.stop();
|
|
339
359
|
}
|
|
340
360
|
|
|
361
|
+
/**
|
|
362
|
+
* O modelo devolveu TEXTO, sem tool e sem finish. Isso é resposta — ou é o
|
|
363
|
+
* agente parando no meio para narrar o que ia fazer?
|
|
364
|
+
*
|
|
365
|
+
* Reclamação do dono: "ele tá parando sem concluir, ele deve continuar até o
|
|
366
|
+
* fim". Medido: o modelo escreve "Agora vou criar o vídeo com as cenas…" e
|
|
367
|
+
* para, porque texto sem tool encerra a rodada. Quem lê vê uma promessa no
|
|
368
|
+
* lugar da entrega. Três sinais dizem que foi parada, não resposta:
|
|
369
|
+
*
|
|
370
|
+
* 1. Há um plano (write_todos) com item por fazer — ele mesmo disse que
|
|
371
|
+
* não acabou.
|
|
372
|
+
* 2. O texto é INTENÇÃO: começa ou termina anunciando o próximo passo
|
|
373
|
+
* ("vou", "em seguida", "próximo passo", dois-pontos no fim, reticências).
|
|
374
|
+
* 3. É curto. Uma resposta de verdade a "explique este erro" tem corpo; um
|
|
375
|
+
* "vou abrir o arquivo agora" não tem.
|
|
376
|
+
*
|
|
377
|
+
* Pergunta ao usuário NÃO conta como parada: o prompt proíbe perguntar, mas
|
|
378
|
+
* se o modelo perguntou, empurrá-lo faria ele decidir sozinho o que talvez
|
|
379
|
+
* não devesse — e o texto de conversa ("oi, sou o PrimoCode") também não é
|
|
380
|
+
* parada. Só intenção declarada e plano aberto.
|
|
381
|
+
*/
|
|
382
|
+
const INTENCAO = /(^|[.!\n]\s*)(agora\s+)?(vou|irei|deixa eu|deixe-me|preciso (agora )?(de )?(criar|ler|abrir|rodar|ver|conferir|pesquisar|mandar|fazer|gerar|escrever|ajustar)|primeiro (vou|eu)|em seguida|pr[óo]ximo passo|a seguir,?\s+(vou|eu)|vamos (criar|ler|abrir|rodar|ver|conferir|come[çc]ar|fazer|gerar|escrever))\b/i;
|
|
383
|
+
function pareceParadaNoMeio(texto, { todosPendentes = 0, rodouTool = false } = {}) {
|
|
384
|
+
const t = String(texto || '').trim();
|
|
385
|
+
if (!t) return false;
|
|
386
|
+
if (todosPendentes > 0) return true;
|
|
387
|
+
if (t.length > 700) return false; // resposta com corpo
|
|
388
|
+
if (/\?\s*$/.test(t)) return false; // perguntou: não é parada
|
|
389
|
+
if (INTENCAO.test(t)) return true;
|
|
390
|
+
if (rodouTool && /[:…]\s*$|\.\.\.\s*$/.test(t)) return true; // "Criando as cenas:" e nada depois
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
341
394
|
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
342
395
|
if (falhasPendentes.length) {
|
|
343
396
|
return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
|
|
@@ -391,6 +444,13 @@ async function runAct(userPrompt, opts) {
|
|
|
391
444
|
// assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
|
|
392
445
|
let pesquisasSecas = 0;
|
|
393
446
|
let rodadasParadas = 0;
|
|
447
|
+
// Para o `pareceParadaNoMeio`: alguma tool já rodou nesta tarefa, e o
|
|
448
|
+
// último plano que o modelo escreveu (write_todos) — desta tarefa, não da
|
|
449
|
+
// sessão: um plano velho de um pedido interrompido não pode empurrar o
|
|
450
|
+
// pedido seguinte.
|
|
451
|
+
let rodouTool = false;
|
|
452
|
+
let todosDaTarefa = [];
|
|
453
|
+
let empurroes = 0;
|
|
394
454
|
|
|
395
455
|
const messages = [];
|
|
396
456
|
if (!subagent) {
|
|
@@ -579,6 +639,29 @@ async function runAct(userPrompt, opts) {
|
|
|
579
639
|
iteration--;
|
|
580
640
|
continue;
|
|
581
641
|
}
|
|
642
|
+
// PAROU NO MEIO? Texto de intenção ("agora vou criar…") ou plano
|
|
643
|
+
// com item aberto não é entrega. Em vez de encerrar, a tarefa é
|
|
644
|
+
// devolvida: "isso foi uma promessa — faça". Três empurrões; se
|
|
645
|
+
// depois disso ele insistir em só falar, a resposta é dele.
|
|
646
|
+
const pendentes = todosDaTarefa.filter((t) => t.status !== 'completed').length;
|
|
647
|
+
if (finalText && empurroes < MAX_EMPURROES && !PARAR
|
|
648
|
+
&& pareceParadaNoMeio(finalText, { todosPendentes: pendentes, rodouTool })) {
|
|
649
|
+
empurroes++;
|
|
650
|
+
if (!subagent) {
|
|
651
|
+
if (fluxo) fluxo.fechar();
|
|
652
|
+
else fala.pensamento(finalText, profundidade);
|
|
653
|
+
}
|
|
654
|
+
messages.push({ role: 'assistant', content: finalText });
|
|
655
|
+
messages.push({
|
|
656
|
+
role: 'user',
|
|
657
|
+
content: 'Isso foi uma intenção, não uma entrega: você parou no meio. Não narre o próximo '
|
|
658
|
+
+ 'passo — CHAME a tool agora e siga até o fim'
|
|
659
|
+
+ (pendentes ? `, o seu plano ainda tem ${pendentes} item(ns) por fazer` : '')
|
|
660
|
+
+ '. Só pare quando tudo estiver feito, com finish("o que foi feito").',
|
|
661
|
+
});
|
|
662
|
+
finalText = '';
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
582
665
|
if (!subagent) {
|
|
583
666
|
if (fluxo) fluxo.fechar();
|
|
584
667
|
else fala.resposta(finalText);
|
|
@@ -711,9 +794,11 @@ async function runAct(userPrompt, opts) {
|
|
|
711
794
|
}
|
|
712
795
|
|
|
713
796
|
algoRodou = true;
|
|
797
|
+
rodouTool = true;
|
|
714
798
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
715
799
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
716
800
|
logResult(tc.name, result, profundidade);
|
|
801
|
+
if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
|
|
717
802
|
|
|
718
803
|
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
719
804
|
// arquivo. Escrever algo sem relação (ou um run_shell qualquer)
|
|
@@ -910,4 +995,4 @@ async function rodarPipeline(args, ctx) {
|
|
|
910
995
|
}
|
|
911
996
|
|
|
912
997
|
module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
913
|
-
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
|
998
|
+
anotacaoDoQueJaFoi, encolherParaContinuar, pareceParadaNoMeio, MAX_ITERATIONS, MAX_EMPURROES };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audio-livre.js — música e efeitos sonoros da internet, com licença.
|
|
3
|
+
*
|
|
4
|
+
* Pedido do dono: "selecione músicas da internet, efeitos sonoros". O que
|
|
5
|
+
* existia era a trilha SINTETIZADA (quatro osciladores e um ruído) e o
|
|
6
|
+
* arquivo do próprio usuário. Faltava o meio: uma faixa de verdade, tocada
|
|
7
|
+
* por gente, que ele não precise ter no disco.
|
|
8
|
+
*
|
|
9
|
+
* ── DE ONDE VEM ──────────────────────────────────────────────────────────
|
|
10
|
+
* Da API do Openverse (openverse.org, mantida pela WordPress Foundation):
|
|
11
|
+
* ela indexa áudio com licença Creative Commons de fontes abertas — música
|
|
12
|
+
* do Jamendo, efeitos do Freesound, arquivos da Wikimedia — e devolve, para
|
|
13
|
+
* cada faixa, a URL do arquivo, a licença e o crédito pronto. Sem chave, sem
|
|
14
|
+
* conta. Não é YouTube nem Spotify: aqui a licença permite usar, e o crédito
|
|
15
|
+
* vai junto para a peça não sair devendo.
|
|
16
|
+
*
|
|
17
|
+
* ── O QUE ELA DEVOLVE ────────────────────────────────────────────────────
|
|
18
|
+
* O arquivo já BAIXADO para a pasta do projeto e ENVIADO ao estúdio: a URL
|
|
19
|
+
* "/midia/…" que entra direto em `trilha.src` ou em `som: {arquivo}`, mais o
|
|
20
|
+
* crédito ("Título — Autor, CC BY 4.0"). A licença decide o que o agente
|
|
21
|
+
* tem de fazer: BY pede o crédito na peça; CC0/PDM não pede nada. Licença
|
|
22
|
+
* NC (não comercial) não entra por padrão — a peça pode ser um anúncio.
|
|
23
|
+
*
|
|
24
|
+
* Zero dependências: o `pegar` do referencia.js, em modo binário.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const referencia = require('./referencia.js');
|
|
32
|
+
|
|
33
|
+
const API = 'https://api.openverse.org/v1/audio/';
|
|
34
|
+
|
|
35
|
+
// Licenças que permitem uso comercial sem pedir nada além do crédito.
|
|
36
|
+
const LIVRES = new Set(['by', 'by-sa', 'cc0', 'pdm']);
|
|
37
|
+
const NOME_LICENCA = {
|
|
38
|
+
by: 'CC BY', 'by-sa': 'CC BY-SA', 'by-nc': 'CC BY-NC', 'by-nd': 'CC BY-ND',
|
|
39
|
+
'by-nc-sa': 'CC BY-NC-SA', 'by-nc-nd': 'CC BY-NC-ND', cc0: 'CC0', pdm: 'Domínio público',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function slug(t) {
|
|
43
|
+
return String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
44
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'audio';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function credito(f) {
|
|
48
|
+
const lic = NOME_LICENCA[f.license] || String(f.license || '').toUpperCase();
|
|
49
|
+
const versao = f.license_version && !/cc0|pdm/.test(f.license) ? ' ' + f.license_version : '';
|
|
50
|
+
return `${f.title || 'Sem título'} — ${f.creator || 'autor desconhecido'} (${lic}${versao}, via ${f.provider || 'Openverse'})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pergunta ao Openverse. `tipo` "musica" restringe à categoria music; "efeito"
|
|
55
|
+
* pede coisa curta (até ~12 s). `dur` em segundos aproxima a duração desejada
|
|
56
|
+
* da trilha — uma faixa de 4 min para um reels de 30 s é desperdício, mas
|
|
57
|
+
* uma de 20 s para um vídeo de 60 s acaba antes; a nota pesa isso.
|
|
58
|
+
*/
|
|
59
|
+
async function procurar({ consulta, tipo = 'musica', dur = 0, comercial = true, pagina = 1 }) {
|
|
60
|
+
const q = new URLSearchParams({ q: consulta, page_size: '20', page: String(pagina) });
|
|
61
|
+
if (tipo === 'musica') q.set('category', 'music');
|
|
62
|
+
if (comercial) q.set('license_type', 'commercial');
|
|
63
|
+
const bruto = await referencia.pegar(`${API}?${q}`, { timeout: 12000 });
|
|
64
|
+
if (!bruto) return { ok: false, error: 'o Openverse não respondeu (rede?)' };
|
|
65
|
+
let j;
|
|
66
|
+
try { j = JSON.parse(bruto); } catch { return { ok: false, error: 'resposta ilegível do Openverse' }; }
|
|
67
|
+
const itens = (j.results || []).filter((f) => f.url && LIVRES.has(String(f.license || '').toLowerCase()) || !comercial);
|
|
68
|
+
const alvoMs = tipo === 'efeito' ? 2500 : (dur ? dur * 1000 : 90000);
|
|
69
|
+
const notas = itens.map((f) => {
|
|
70
|
+
let n = 0;
|
|
71
|
+
const d = Number(f.duration) || 0;
|
|
72
|
+
if (tipo === 'efeito') {
|
|
73
|
+
if (d > 15000) n -= 50; // não é efeito, é faixa
|
|
74
|
+
else n += 20 - Math.abs(d - alvoMs) / 500;
|
|
75
|
+
} else {
|
|
76
|
+
if (d && d < alvoMs * 0.6) n -= 20; // acaba antes do vídeo
|
|
77
|
+
n -= Math.abs(d - alvoMs) / 20000; // perto da duração pedida
|
|
78
|
+
if (f.category === 'music') n += 5;
|
|
79
|
+
}
|
|
80
|
+
if (/cc0|pdm/.test(f.license)) n += 3; // sem crédito obrigatório
|
|
81
|
+
if (/mp3/i.test(f.filetype || '') || /\.mp3(\?|$)/i.test(f.url)) n += 4; // toca em qualquer lugar
|
|
82
|
+
// As palavras da consulta no título/tags contam.
|
|
83
|
+
const alvo = `${f.title} ${(f.tags || []).map((t) => t.name).join(' ')} ${(f.genres || []).join(' ')}`.toLowerCase();
|
|
84
|
+
for (const p of consulta.toLowerCase().split(/\s+/)) if (p.length > 2 && alvo.includes(p)) n += 3;
|
|
85
|
+
return { f, n };
|
|
86
|
+
}).sort((a, b) => b.n - a.n);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
total: j.result_count || itens.length,
|
|
90
|
+
itens: notas.map(({ f }) => ({
|
|
91
|
+
id: f.id, titulo: f.title, autor: f.creator, licenca: f.license, licencaUrl: f.license_url,
|
|
92
|
+
duracao: f.duration ? Math.round(f.duration / 1000) : null, url: f.url, pagina: f.foreign_landing_url,
|
|
93
|
+
provedor: f.provider, tipoArquivo: f.filetype || null, generos: f.genres || [], credito: credito(f),
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Baixa a faixa para <projeto>/midia/ e manda ao estúdio. */
|
|
99
|
+
async function trazer(item, { projectDir, studio }) {
|
|
100
|
+
const bytes = await referencia.pegar(item.url, { timeout: 45000, binario: true });
|
|
101
|
+
if (!bytes || bytes.length < 2000) return { ok: false, error: `não consegui baixar "${item.titulo}" de ${item.provedor}` };
|
|
102
|
+
// A extensão pelo conteúdo, não pela URL: o Jamendo entrega mp3 numa URL
|
|
103
|
+
// sem extensão ("?trackid=…&format=mp32").
|
|
104
|
+
const cab = bytes.subarray(0, 12);
|
|
105
|
+
const ext = cab.subarray(0, 3).toString() === 'ID3' || (cab[0] === 0xff && (cab[1] & 0xe0) === 0xe0) ? '.mp3'
|
|
106
|
+
: cab.subarray(0, 4).toString() === 'RIFF' ? '.wav'
|
|
107
|
+
: cab.subarray(0, 4).toString() === 'OggS' ? '.ogg'
|
|
108
|
+
: cab.subarray(0, 4).toString() === 'fLaC' ? '.flac'
|
|
109
|
+
: (item.tipoArquivo ? '.' + item.tipoArquivo.replace(/^\./, '') : '.mp3');
|
|
110
|
+
const pasta = path.join(projectDir || process.cwd(), 'midia');
|
|
111
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
112
|
+
const nome = `${slug(item.titulo)}-${String(item.id || '').slice(0, 6)}${ext}`;
|
|
113
|
+
const caminho = path.join(pasta, nome);
|
|
114
|
+
fs.writeFileSync(caminho, bytes);
|
|
115
|
+
// FLAC/OGG do Freesound: o navegador toca, mas o MP4 exportado quer AAC —
|
|
116
|
+
// o estúdio cuida disso na exportação; aqui só entregamos o arquivo.
|
|
117
|
+
const envio = await studio.midiaEnviar({ arquivo: caminho, nome });
|
|
118
|
+
if (!envio || envio.ok === false) return { ok: false, error: (envio && envio.error) || 'o estúdio não aceitou o arquivo', arquivoLocal: caminho };
|
|
119
|
+
return { ok: true, url: envio.url, arquivoLocal: path.relative(projectDir || process.cwd(), caminho), bytes: bytes.length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A tool: procura, escolhe a melhor, baixa e devolve a URL pronta — e as
|
|
124
|
+
* alternativas, para o agente trocar sem pesquisar de novo.
|
|
125
|
+
*/
|
|
126
|
+
async function buscarAudio(args, { projectDir, studio }) {
|
|
127
|
+
const consulta = String(args.consulta || args.query || '').trim();
|
|
128
|
+
if (!consulta) return { ok: false, error: 'Diga o que procurar: o clima da música ("upbeat corporate", "piano calmo") ou o efeito ("whoosh", "ding", "click").' };
|
|
129
|
+
const tipo = /^(efeito|som|sfx|sound)/i.test(String(args.tipo || '')) ? 'efeito' : 'musica';
|
|
130
|
+
const dur = Number(args.duracao) || 0;
|
|
131
|
+
const busca = await procurar({ consulta, tipo, dur, comercial: args.comercial !== false });
|
|
132
|
+
if (!busca.ok) return busca;
|
|
133
|
+
if (!busca.itens.length) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `Nenhuma faixa livre para "${consulta}".`,
|
|
137
|
+
comoCorrigir: tipo === 'efeito'
|
|
138
|
+
? 'Use um som SINTETIZADO do estúdio, que não precisa de arquivo: som: "whoosh" | "impacto" | "click" | "pop" | "ding" | "subida" | "tique" | "brilho".'
|
|
139
|
+
: 'Tente a consulta em inglês e mais genérica ("upbeat electronic", "calm piano", "epic cinematic"), ou use a trilha GERADA: "trilha": {"tipo":"gerada","estilo":"pulso"}.',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Baixa a melhor; se ela falhar, a próxima. Três tentativas.
|
|
143
|
+
let trazido = null;
|
|
144
|
+
let escolhida = null;
|
|
145
|
+
for (const item of busca.itens.slice(0, 3)) {
|
|
146
|
+
trazido = await trazer(item, { projectDir, studio });
|
|
147
|
+
if (trazido.ok) { escolhida = item; break; }
|
|
148
|
+
}
|
|
149
|
+
if (!trazido || !trazido.ok) return { ok: false, error: (trazido && trazido.error) || 'não consegui baixar nenhuma faixa', alternativas: busca.itens.slice(0, 5) };
|
|
150
|
+
const precisaCredito = !/cc0|pdm/.test(escolhida.licenca);
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
tipo,
|
|
154
|
+
url: trazido.url,
|
|
155
|
+
titulo: escolhida.titulo,
|
|
156
|
+
autor: escolhida.autor,
|
|
157
|
+
duracao: escolhida.duracao,
|
|
158
|
+
licenca: NOME_LICENCA[escolhida.licenca] || escolhida.licenca,
|
|
159
|
+
licencaUrl: escolhida.licencaUrl,
|
|
160
|
+
credito: escolhida.credito,
|
|
161
|
+
pagina: escolhida.pagina,
|
|
162
|
+
arquivoLocal: trazido.arquivoLocal,
|
|
163
|
+
comoUsar: tipo === 'efeito'
|
|
164
|
+
? `No elemento ou na cena: "som": {"arquivo": "${trazido.url}", "volume": 0.8}.`
|
|
165
|
+
: `No documento: "trilha": {"tipo":"arquivo","src":"${trazido.url}","volume":0.55,"repetir":true}.`
|
|
166
|
+
+ (precisaCredito ? ` A licença pede CRÉDITO: ponha "${escolhida.credito}" na assinatura ou numa legenda da última cena.` : ' Licença sem exigência de crédito.'),
|
|
167
|
+
alternativas: busca.itens.filter((i) => i.id !== escolhida.id).slice(0, 4)
|
|
168
|
+
.map((i) => ({ titulo: i.titulo, autor: i.autor, duracao: i.duracao, licenca: i.licenca, id: i.id })),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { buscarAudio, procurar, trazer, credito, LIVRES, NOME_LICENCA };
|
package/lib/catalogo.js
CHANGED
|
@@ -143,6 +143,13 @@ const GRUPOS = [
|
|
|
143
143
|
args: 'todos: lista de {content: a tarefa curta no imperativo, status: pending|in_progress|completed}. Reescreve a lista inteira a cada chamada; só uma "in_progress" por vez.' },
|
|
144
144
|
{ tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade',
|
|
145
145
|
args: 'consulta: o que pesquisar, de preferência em inglês. paginas (opcional, número, padrão 3, máximo 5).' },
|
|
146
|
+
// Baixa o arquivo para <projeto>/midia/ — por isso `escreve: true`.
|
|
147
|
+
{ tool: 'buscar_audio', escreve: true, faz: 'acha música ou efeito sonoro livre na internet (Creative Commons), baixa e manda para o estúdio, com o crédito',
|
|
148
|
+
args: 'consulta: o clima ou o som, em inglês ("upbeat corporate", "calm piano", "whoosh", "ding"). tipo (opcional): "musica" (padrão) ou "efeito". '
|
|
149
|
+
+ 'duracao (opcional, segundos): quanto o vídeo dura, para a faixa combinar. Devolve url para trilha.src ou som.arquivo, e o crédito a pôr na peça quando a licença pedir.' },
|
|
150
|
+
{ tool: 'marca_da_empresa', escreve: true, faz: 'puxa do site da empresa a logo, a cor principal, a paleta, as fontes e a frase — pronto para `marca` no estúdio',
|
|
151
|
+
args: 'nome: a empresa como o usuário falou. site (opcional): o endereço, se souber — sem ele, o site é descoberto. '
|
|
152
|
+
+ 'Devolve marca:{cor, logo} para colar no documento, mais cores.paleta, fontes e frase. Chame ANTES de criar uma peça de marca.' },
|
|
146
153
|
// Vai para memory-geral.md, FORA da pasta do projeto — por isso não
|
|
147
154
|
// é write_file/edit_file (a sandbox de resolveInProject bloqueia
|
|
148
155
|
// qualquer caminho fora do projeto, e é assim mesmo que tem que
|
package/lib/marca.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marca.js — a logo e a identidade visual de uma empresa, puxadas do site.
|
|
3
|
+
*
|
|
4
|
+
* Pedido do dono: "puxe a logo e identidade visual da empresa". O agente
|
|
5
|
+
* fazia peça de marca com `marca.cor` chutada e sem logo, ou pedia o arquivo
|
|
6
|
+
* ao usuário — e o usuário, que só disse "faz um vídeo para a Paris Group",
|
|
7
|
+
* não estava ali para mandar arquivo nenhum. O site da empresa TEM a logo,
|
|
8
|
+
* as cores e as fontes; é só ler como uma pessoa leria.
|
|
9
|
+
*
|
|
10
|
+
* ── O QUE SE LÊ, E EM QUE ORDEM ──────────────────────────────────────────
|
|
11
|
+
* nome og:site_name → <title> (sem o " | slogan")
|
|
12
|
+
* frase meta description / og:description (o "sobre" em uma linha)
|
|
13
|
+
* logo <img> com "logo" no src/alt/class (a de verdade, no cabeçalho) →
|
|
14
|
+
* apple-touch-icon (180 px, o melhor ícone que todo site tem) →
|
|
15
|
+
* og:image (é a imagem de compartilhar, às vezes é a logo, às vezes
|
|
16
|
+
* uma foto — por isso vem por último) → favicon.
|
|
17
|
+
* SVG ganha de PNG no mesmo lugar: escala sem borrar.
|
|
18
|
+
* cores meta theme-color primeiro (a empresa DISSE qual é a cor), depois
|
|
19
|
+
* as cores mais repetidas no CSS (inline e nas folhas ligadas),
|
|
20
|
+
* fora branco/preto/cinza — o que sobra é a paleta.
|
|
21
|
+
* fontes os "family=" do Google Fonts e os font-family do CSS, fora as
|
|
22
|
+
* genéricas (sans-serif, Arial, system-ui).
|
|
23
|
+
*
|
|
24
|
+
* A logo é BAIXADA para o projeto e ENVIADA ao estúdio: volta como
|
|
25
|
+
* "/midia/…", pronta para `marca.logo`. O resto volta como `marca` pronta
|
|
26
|
+
* para colar no documento.
|
|
27
|
+
*
|
|
28
|
+
* Zero dependências: o `pegar` do referencia.js (texto e binário), e o
|
|
29
|
+
* chute de domínio dele quando a pessoa deu só o nome.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const referencia = require('./referencia.js');
|
|
37
|
+
|
|
38
|
+
const GENERICAS_FONTE = /^(sans-serif|serif|monospace|system-ui|-apple-system|blinkmacsystemfont|segoe ui|arial|helvetica( neue)?|roboto|inherit|initial|ui-sans-serif|ui-serif|ui-monospace|apple color emoji|segoe ui emoji|noto color emoji|times new roman|courier new|verdana|tahoma|georgia)$/i;
|
|
39
|
+
|
|
40
|
+
function absoluta(href, base) {
|
|
41
|
+
try { return new URL(String(href || '').replace(/&/g, '&'), base).href; } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function atributo(tag, nome) {
|
|
45
|
+
const m = new RegExp(`\\b${nome}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(tag);
|
|
46
|
+
return m ? (m[2] != null ? m[2] : m[3] != null ? m[3] : m[4]) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function metas(html) {
|
|
50
|
+
const saida = {};
|
|
51
|
+
for (const tag of html.match(/<meta\b[^>]*>/gi) || []) {
|
|
52
|
+
const chave = atributo(tag, 'property') || atributo(tag, 'name');
|
|
53
|
+
const valor = atributo(tag, 'content');
|
|
54
|
+
if (chave && valor && saida[chave.toLowerCase()] == null) saida[chave.toLowerCase()] = valor.trim();
|
|
55
|
+
}
|
|
56
|
+
return saida;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** As candidatas a logo, da melhor para a pior. */
|
|
60
|
+
function candidatasALogo(html, base) {
|
|
61
|
+
const lista = [];
|
|
62
|
+
// 1. <img> que se chama logo — no cabeçalho é a logo de verdade.
|
|
63
|
+
for (const tag of html.match(/<img\b[^>]*>/gi) || []) {
|
|
64
|
+
const src = atributo(tag, 'src') || atributo(tag, 'data-src') || '';
|
|
65
|
+
const alt = atributo(tag, 'alt') || '';
|
|
66
|
+
const cls = atributo(tag, 'class') || '';
|
|
67
|
+
const id = atributo(tag, 'id') || '';
|
|
68
|
+
if (/logo|brand|marca/i.test(`${src} ${alt} ${cls} ${id}`) && !/sponsor|partner|cliente|client|parceir/i.test(`${alt} ${cls} ${src}`)) {
|
|
69
|
+
const u = absoluta(src, base);
|
|
70
|
+
if (u && !/^data:/.test(u)) lista.push({ url: u, fonte: 'img-logo', nota: /\.svg(\?|$)/i.test(u) ? 30 : 20 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 1b. SVG inline no cabeçalho com "logo" no nome — não dá para baixar,
|
|
74
|
+
// mas o agente pode saber que existe (e usar o nome em texto).
|
|
75
|
+
// 2. Ícones declarados.
|
|
76
|
+
for (const tag of html.match(/<link\b[^>]*>/gi) || []) {
|
|
77
|
+
const rel = (atributo(tag, 'rel') || '').toLowerCase();
|
|
78
|
+
const href = atributo(tag, 'href');
|
|
79
|
+
if (!href) continue;
|
|
80
|
+
const u = absoluta(href, base);
|
|
81
|
+
if (!u) continue;
|
|
82
|
+
if (/apple-touch-icon/.test(rel)) lista.push({ url: u, fonte: 'apple-touch-icon', nota: 15 });
|
|
83
|
+
else if (/\bicon\b/.test(rel)) {
|
|
84
|
+
const tam = parseInt((atributo(tag, 'sizes') || '0').split('x')[0], 10) || 0;
|
|
85
|
+
lista.push({ url: u, fonte: 'icon', nota: /\.svg(\?|$)/i.test(u) ? 14 : (tam >= 128 ? 10 : 4) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 3. og:image — a imagem de compartilhar. Às vezes é a logo, às vezes é
|
|
89
|
+
// uma foto: fica atrás das declaradas.
|
|
90
|
+
const m = metas(html);
|
|
91
|
+
const og = m['og:image'] || m['twitter:image'];
|
|
92
|
+
if (og) { const u = absoluta(og, base); if (u) lista.push({ url: u, fonte: 'og:image', nota: 6 }); }
|
|
93
|
+
// 4. O favicon padrão, quando nada foi declarado.
|
|
94
|
+
lista.push({ url: absoluta('/favicon.ico', base), fonte: 'favicon', nota: 1 });
|
|
95
|
+
// Ordena e tira repetidos.
|
|
96
|
+
const vistos = new Set();
|
|
97
|
+
return lista.filter((c) => c.url && !vistos.has(c.url) && vistos.add(c.url)).sort((a, b) => b.nota - a.nota);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** As cores de um CSS, contadas. Fora branco, preto e cinza. */
|
|
101
|
+
function coresDe(css, contagem) {
|
|
102
|
+
const re = /#([0-9a-f]{6}|[0-9a-f]{3})\b|rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/gi;
|
|
103
|
+
let m;
|
|
104
|
+
while ((m = re.exec(css))) {
|
|
105
|
+
let hex;
|
|
106
|
+
if (m[1]) {
|
|
107
|
+
hex = m[1].length === 3 ? m[1].split('').map((c) => c + c).join('') : m[1];
|
|
108
|
+
} else {
|
|
109
|
+
hex = [m[2], m[3], m[4]].map((n) => Math.min(255, +n).toString(16).padStart(2, '0')).join('');
|
|
110
|
+
}
|
|
111
|
+
hex = '#' + hex.toLowerCase();
|
|
112
|
+
const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
|
|
113
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
114
|
+
const sat = max === 0 ? 0 : (max - min) / max;
|
|
115
|
+
// Cinza (pouca saturação), quase branco e quase preto não são
|
|
116
|
+
// identidade — são o papel e a tinta de todo site.
|
|
117
|
+
if (sat < 0.18 || max < 40 || (min > 225)) continue;
|
|
118
|
+
contagem.set(hex, (contagem.get(hex) || 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function fontesDe(html, css) {
|
|
123
|
+
const achadas = new Map();
|
|
124
|
+
// Google Fonts: family=Plus+Jakarta+Sans:wght@400;700&family=Inter
|
|
125
|
+
for (const m of html.matchAll(/fonts\.googleapis\.com\/css2?\?([^"' >]+)/gi)) {
|
|
126
|
+
for (const f of m[1].matchAll(/family=([^&:]+)/g)) {
|
|
127
|
+
const nome = decodeURIComponent(f[1].replace(/\+/g, ' ')).trim();
|
|
128
|
+
if (nome) achadas.set(nome, (achadas.get(nome) || 0) + 10);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Para em ; } e aspas: o CSS aqui inclui atributos style="…" da página,
|
|
132
|
+
// e sem a aspa como fim o nome da fonte engolia o atributo seguinte
|
|
133
|
+
// (medido: 'sans-serif" style="color:#7fa6ff').
|
|
134
|
+
for (const m of css.matchAll(/font-family\s*:\s*([^;}]+)/gi)) {
|
|
135
|
+
for (const parte of m[1].split(',')) {
|
|
136
|
+
const nome = parte.trim().replace(/\s*!important/, '').replace(/^["']|["']$/g, '').trim();
|
|
137
|
+
if (!nome || nome.length > 40 || GENERICAS_FONTE.test(nome) || /[(){}:="']/.test(nome)) continue;
|
|
138
|
+
achadas.set(nome, (achadas.get(nome) || 0) + 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...achadas.entries()].sort((a, b) => b[1] - a[1]).map(([n]) => n).slice(0, 3);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Descobre o site quando veio só o nome: o chute de domínio do referencia.js. */
|
|
145
|
+
async function acharSite(nome) {
|
|
146
|
+
const palpites = referencia.sitesProvaveis(`site da empresa "${nome}"`);
|
|
147
|
+
const { vivos, html } = await referencia.chutesVivos(palpites, 1);
|
|
148
|
+
if (vivos.length) return { url: vivos[0].url, html: html.get(vivos[0].url) };
|
|
149
|
+
// Sem chute vivo, o buscador.
|
|
150
|
+
const achados = await referencia.buscar(`${nome} site oficial`, 4);
|
|
151
|
+
for (const a of achados) {
|
|
152
|
+
if (/wikipedia|linkedin|instagram|facebook|youtube|twitter|x\.com/i.test(a.url)) continue;
|
|
153
|
+
const h = await referencia.pegar(a.url, { timeout: 8000 });
|
|
154
|
+
if (h && h.length > 500) return { url: a.url, html: h };
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A tool. `site` (URL) ou `nome` (a empresa). Devolve a identidade e a
|
|
161
|
+
* `marca` pronta para o documento do estúdio.
|
|
162
|
+
*/
|
|
163
|
+
async function marcaDaEmpresa(args, { projectDir, studio }) {
|
|
164
|
+
const nome = String(args.nome || args.empresa || '').trim();
|
|
165
|
+
let site = String(args.site || args.url || '').trim();
|
|
166
|
+
if (!nome && !site) return { ok: false, error: 'Diga o nome da empresa (nome) ou o endereço do site (site).' };
|
|
167
|
+
if (site && !/^https?:\/\//i.test(site)) site = 'https://' + site;
|
|
168
|
+
|
|
169
|
+
let html = null;
|
|
170
|
+
if (site) html = await referencia.pegar(site, { timeout: 12000 });
|
|
171
|
+
if (!html && site && !/\/\/www\./i.test(site)) {
|
|
172
|
+
const comWww = site.replace(/^(https?:\/\/)/i, '$1www.');
|
|
173
|
+
html = await referencia.pegar(comWww, { timeout: 12000 });
|
|
174
|
+
if (html) site = comWww;
|
|
175
|
+
}
|
|
176
|
+
if (!html && nome) {
|
|
177
|
+
const achado = await acharSite(nome);
|
|
178
|
+
if (achado) { site = achado.url; html = achado.html; }
|
|
179
|
+
}
|
|
180
|
+
if (!html) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: `Não achei o site de "${nome || site}".`,
|
|
184
|
+
comoCorrigir: 'Se souber o endereço, chame de novo com site:"https://…". Se não, siga SEM a logo: '
|
|
185
|
+
+ 'use o nome em texto na assinatura ("tipo":"assinatura","nome":…) e uma cor de tema — '
|
|
186
|
+
+ 'e diga ao usuário que a logo não foi encontrada para ele mandar o arquivo (studio_midia).',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const m = metas(html);
|
|
191
|
+
const titulo = (html.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1] || '';
|
|
192
|
+
// Sem og:site_name nem <title> (site montado por JavaScript), o nome vem
|
|
193
|
+
// do domínio: "nubank.com.br" → "Nubank". Melhor que campo vazio.
|
|
194
|
+
const dominio = ((site.match(/^https?:\/\/(?:www\.)?([^/]+)/i) || [])[1] || '').split('.')[0];
|
|
195
|
+
const nomeSite = (m['og:site_name'] || m['application-name'] || titulo.split(/\s[|–—-]\s|\s·\s/)[0] || nome
|
|
196
|
+
|| (dominio ? dominio.charAt(0).toUpperCase() + dominio.slice(1) : '')).trim().slice(0, 80);
|
|
197
|
+
const frase = (m['description'] || m['og:description'] || '').trim().slice(0, 240);
|
|
198
|
+
|
|
199
|
+
// Cores: theme-color declarada, depois o CSS (inline + até 3 folhas).
|
|
200
|
+
const contagem = new Map();
|
|
201
|
+
let css = (html.match(/<style\b[^>]*>([\s\S]*?)<\/style>/gi) || []).join('\n');
|
|
202
|
+
// Só o MIOLO do style="…": com o `style="` junto, o fim de um atributo
|
|
203
|
+
// virava começo do nome da fonte seguinte (medido: 'sans-serif" style=…').
|
|
204
|
+
css += ' ' + (html.match(/\sstyle\s*=\s*"[^"]*"/gi) || []).map((a) => a.replace(/^\sstyle\s*=\s*"|"$/g, '')).join('; ');
|
|
205
|
+
const folhas = [];
|
|
206
|
+
for (const tag of html.match(/<link\b[^>]*>/gi) || []) {
|
|
207
|
+
if (/stylesheet/i.test(atributo(tag, 'rel') || '')) {
|
|
208
|
+
const u = absoluta(atributo(tag, 'href'), site);
|
|
209
|
+
if (u && !/fonts\.googleapis/i.test(u)) folhas.push(u);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const conteudos = await Promise.all(folhas.slice(0, 3).map((u) => referencia.pegar(u, { timeout: 8000 })));
|
|
213
|
+
for (const c of conteudos) if (c) css += '\n' + c.slice(0, 300000);
|
|
214
|
+
coresDe(css, contagem);
|
|
215
|
+
const paleta = [...contagem.entries()].sort((a, b) => b[1] - a[1]).map(([c]) => c).slice(0, 6);
|
|
216
|
+
let principal = null;
|
|
217
|
+
const tema = m['theme-color'];
|
|
218
|
+
if (tema && /^#?[0-9a-f]{3,8}$/i.test(tema.replace('#', ''))) {
|
|
219
|
+
const h = tema.replace('#', '').toLowerCase();
|
|
220
|
+
principal = '#' + (h.length === 3 ? h.split('').map((c) => c + c).join('') : h.slice(0, 6));
|
|
221
|
+
const r = parseInt(principal.slice(1, 3), 16), g = parseInt(principal.slice(3, 5), 16), b = parseInt(principal.slice(5, 7), 16);
|
|
222
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
223
|
+
if (max === 0 || (max - min) / max < 0.18 || min > 225) principal = null; // theme-color branco/preto não é a cor da marca
|
|
224
|
+
}
|
|
225
|
+
if (!principal) principal = paleta[0] || null;
|
|
226
|
+
|
|
227
|
+
const fontes = fontesDe(html, css);
|
|
228
|
+
|
|
229
|
+
// A logo: tenta as candidatas em ordem até uma baixar de verdade.
|
|
230
|
+
let logo = null;
|
|
231
|
+
const candidatas = candidatasALogo(html, site);
|
|
232
|
+
const tentadas = [];
|
|
233
|
+
for (const c of candidatas.slice(0, 6)) {
|
|
234
|
+
const bytes = await referencia.pegar(c.url, { timeout: 10000, binario: true });
|
|
235
|
+
if (!bytes || bytes.length < 200) { tentadas.push(`${c.fonte}: não abriu`); continue; }
|
|
236
|
+
const cab = bytes.subarray(0, 16);
|
|
237
|
+
const ext = /^<\?xml|^<svg/i.test(bytes.subarray(0, 64).toString('utf8').trim()) ? '.svg'
|
|
238
|
+
: cab[0] === 0x89 && cab[1] === 0x50 ? '.png'
|
|
239
|
+
: cab[0] === 0xff && cab[1] === 0xd8 ? '.jpg'
|
|
240
|
+
: cab.subarray(0, 4).toString() === 'RIFF' ? '.webp'
|
|
241
|
+
: cab[0] === 0x00 && cab[1] === 0x00 && cab[2] === 0x01 && cab[3] === 0x00 ? '.ico'
|
|
242
|
+
: cab.subarray(0, 3).toString() === 'GIF' ? '.gif' : null;
|
|
243
|
+
if (!ext) { tentadas.push(`${c.fonte}: não é imagem`); continue; }
|
|
244
|
+
if (ext === '.ico' && candidatas.length > 1 && c.fonte === 'favicon') { tentadas.push('favicon: só .ico'); }
|
|
245
|
+
const pasta = path.join(projectDir || process.cwd(), 'midia');
|
|
246
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
247
|
+
const arquivo = path.join(pasta, `logo-${nomeSite.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'marca'}${ext}`);
|
|
248
|
+
fs.writeFileSync(arquivo, bytes);
|
|
249
|
+
let url = null;
|
|
250
|
+
if (studio && ext !== '.ico') {
|
|
251
|
+
const envio = await studio.midiaEnviar({ arquivo });
|
|
252
|
+
if (envio && envio.ok !== false) url = envio.url;
|
|
253
|
+
}
|
|
254
|
+
logo = { url: url || null, arquivoLocal: path.relative(projectDir || process.cwd(), arquivo), origem: c.url, fonte: c.fonte, formato: ext.slice(1), bytes: bytes.length };
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const marca = {};
|
|
259
|
+
if (principal) marca.cor = principal;
|
|
260
|
+
if (logo && logo.url) marca.logo = logo.url;
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
nome: nomeSite || nome,
|
|
264
|
+
site,
|
|
265
|
+
frase: frase || null,
|
|
266
|
+
cores: { principal, paleta },
|
|
267
|
+
fontes,
|
|
268
|
+
logo,
|
|
269
|
+
marca,
|
|
270
|
+
comoUsar: (Object.keys(marca).length
|
|
271
|
+
? `No documento do estúdio: "marca": ${JSON.stringify(marca)}. `
|
|
272
|
+
: 'Sem cor nem logo confiáveis: use o nome em texto na assinatura e o tema pelo assunto. ')
|
|
273
|
+
+ (fontes.length ? `Fonte da marca: ${fontes[0]} (o estúdio usa a do tema; cite a fonte só se for pedir código/HTML). ` : '')
|
|
274
|
+
+ (logo && !logo.url && logo.formato === 'ico' ? 'A única logo encontrada foi o favicon .ico, que o estúdio não usa — peça o arquivo ao usuário se a peça for de marca. ' : '')
|
|
275
|
+
+ (frase ? `Uma linha sobre a empresa, do próprio site: "${frase}".` : ''),
|
|
276
|
+
tentativasDeLogo: tentadas.length ? tentadas : undefined,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { marcaDaEmpresa, candidatasALogo, coresDe, fontesDe, metas, atributo };
|