primocode 9.8.0 → 9.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/estudio/cinema.js +5 -3
- package/lib/estudio/edicao.js +32 -7
- package/lib/estudio/ende.js +107 -0
- package/lib/estudio/plano.js +20 -1
- package/lib/estudio/producao.js +106 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.
|
|
1
|
+
# PrimoCode v9.9.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v9.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.9.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/cinema.js
CHANGED
|
@@ -31,8 +31,10 @@ const { caber } = require('./composicao.js');
|
|
|
31
31
|
const r2 = (n) => Math.round(n * 100) / 100;
|
|
32
32
|
const tokens = (t) => String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
33
33
|
|
|
34
|
-
/** Quanto tempo um texto precisa ficar na tela para ser lido.
|
|
35
|
-
|
|
34
|
+
/** Quanto tempo um texto precisa ficar na tela para ser lido. Nunca menos que
|
|
35
|
+
0,3 s por palavra: a regra de legibilidade do /brag (Arthur, 28/09). */
|
|
36
|
+
const nPalavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
37
|
+
const leitura = (t) => Math.max(0.55 + 0.22 * nPalavras(t), 0.3 * nPalavras(t));
|
|
36
38
|
|
|
37
39
|
/** A fala desta parte (o que a voz diz): a "fala", ou os textos da tela. */
|
|
38
40
|
/* A voz lê EXATAMENTE o que está na tela, na ordem da tela ("a narração tem
|
|
@@ -229,7 +231,7 @@ function montarCinema({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
229
231
|
com a voz é bom, ler é obrigatório. Se a leitura pede mais que a fala,
|
|
230
232
|
a cena cresce (medido: "EDIÇÕES" ficava meio segundo na tela). */
|
|
231
233
|
// Texto curto (um número, "3 DIAS.") se lê de relance: não espera a leitura cheia e não atrasa a voz.
|
|
232
|
-
const minimo = (x) => Math.min(leitura(x),
|
|
234
|
+
const minimo = (x) => Math.max(0.3 * nPalavras(x), Math.min(leitura(x), nPalavras(x) <= 3 ? 0.75 : 1.6));
|
|
233
235
|
for (let k = 1; k < inicios.length; k++) inicios[k] = r2(Math.max(inicios[k], inicios[k - 1] + minimo(textosDaParte[k - 1])));
|
|
234
236
|
if (inicios.length) dur = r2(Math.max(dur, inicios[inicios.length - 1] + leitura(textosDaParte[inicios.length - 1]) + 0.3));
|
|
235
237
|
const fimDe = (k) => (k < inicios.length - 1 ? inicios[k + 1] : dur);
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -145,13 +145,36 @@ function forteDe(g) {
|
|
|
145
145
|
return forte;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/* O B-ROLL EM QUATRO ROTAS (Arthur, do b-roll-finder, 28/09): a frase diz qual
|
|
149
|
+
imagem pede, na ordem abaixo. A primeira que casa decide.
|
|
150
|
+
marca a frase diz o nome da marca → a logo / o site da marca
|
|
151
|
+
numero a frase tem número → sem foto: o número já é a imagem
|
|
152
|
+
concreto uma palavra tem foto no acervo → a foto de estoque dela
|
|
153
|
+
pessoa ideia abstrata → fica no rosto de quem fala
|
|
154
|
+
`palavra` é onde o corte se ancora: ele entra 0,2 s DEPOIS dela. */
|
|
155
|
+
const DEPOIS_DA_PALAVRA = 0.2;
|
|
156
|
+
|
|
157
|
+
function rotaDeApoio(g, { marca = null } = {}) {
|
|
158
|
+
const ws = g || [];
|
|
159
|
+
const nomes = (Array.isArray(marca) ? marca : [marca]).filter(Boolean).map((m) => String(m).split(/\s+/).map(limpa));
|
|
160
|
+
for (const nome of nomes) {
|
|
161
|
+
const k = ws.findIndex((_, i) => nome.every((n, j) => ws[i + j] && limpa(ws[i + j].texto) === n));
|
|
162
|
+
if (k >= 0) return { rota: 'marca', termo: null, palavra: ws[k] };
|
|
163
|
+
}
|
|
164
|
+
const num = ws.find((p) => /\d/.test(p.texto));
|
|
165
|
+
if (num) return { rota: 'numero', termo: null, palavra: num };
|
|
166
|
+
let conc = null;
|
|
167
|
+
for (const p of ws) if (termoDeApoio(p.texto) && (!conc || peso(p.texto) > peso(conc.texto))) conc = p;
|
|
168
|
+
if (conc) return { rota: 'concreto', termo: termoDeApoio(conc.texto), palavra: conc };
|
|
169
|
+
return { rota: 'pessoa', termo: null, palavra: null };
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
/** Os termos das imagens de apoio que o plano vai pedir — para baixar antes. */
|
|
149
|
-
function termosDeApoio(palavras) {
|
|
173
|
+
function termosDeApoio(palavras, opcoes) {
|
|
150
174
|
const lista = (palavras || []).filter((p) => p && p.texto && p.ate > p.de).sort((a, b) => a.de - b.de);
|
|
151
175
|
const termos = [];
|
|
152
176
|
frases(lista).forEach((g, i) => {
|
|
153
|
-
const
|
|
154
|
-
const t = f && temApoio(i) ? termoDeApoio(f.texto) : null;
|
|
177
|
+
const t = temApoio(i) ? rotaDeApoio(g, opcoes).termo : null;
|
|
155
178
|
if (t && !termos.includes(t)) termos.push(t);
|
|
156
179
|
});
|
|
157
180
|
return termos;
|
|
@@ -231,7 +254,7 @@ const ESTILOS = {
|
|
|
231
254
|
// Em quais cenas a pessoa é recortada, com a palavra gigante atrás dela.
|
|
232
255
|
const temRecorte = (i) => i % 4 === 2;
|
|
233
256
|
|
|
234
|
-
function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {} }) {
|
|
257
|
+
function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {}, marca = null }) {
|
|
235
258
|
const e = ESTILOS[estilo] || ESTILOS.viral;
|
|
236
259
|
const lista = (palavras || []).filter((p) => p && p.texto && p.ate > p.de).sort((a, b) => a.de - b.de);
|
|
237
260
|
if (!lista.length) return { ok: false, error: 'a gravação não tem fala transcrita para editar.' };
|
|
@@ -290,9 +313,11 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
290
313
|
/* A IMAGEM DE APOIO: na hora da palavra forte, a tela vira a imagem do
|
|
291
314
|
que está sendo dito, com um zoom lento, e a legenda continua por
|
|
292
315
|
cima. É o "b-roll" das edições de videomaker. */
|
|
293
|
-
const
|
|
316
|
+
const r = e.apoio && temApoio(i) && !rec ? rotaDeApoio(g, { marca }) : null;
|
|
317
|
+
const apoio = !r ? null : r.rota === 'concreto' ? imagens[r.termo] : r.rota === 'marca' ? imagens.marca : null;
|
|
294
318
|
if (apoio) {
|
|
295
|
-
|
|
319
|
+
// O corte entra DEPOIS da palavra, nunca antes: quem ouve "cachorro" vê o cachorro.
|
|
320
|
+
const de = local(r.palavra.de + DEPOIS_DA_PALAVRA);
|
|
296
321
|
const vida = Math.min(2.2, dur - de);
|
|
297
322
|
if (vida > 0.8) {
|
|
298
323
|
// O apoio é foto ou vídeo (o Pexels dá os dois; vídeo é melhor).
|
|
@@ -447,4 +472,4 @@ async function prepararSons(pasta, video) {
|
|
|
447
472
|
return sons;
|
|
448
473
|
}
|
|
449
474
|
|
|
450
|
-
module.exports = { montarEdicao, temRecorte, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, ESTILOS, ICONES, FOTO };
|
|
475
|
+
module.exports = { montarEdicao, temRecorte, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, rotaDeApoio, DEPOIS_DA_PALAVRA, ESTILOS, ICONES, FOTO };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ende.js — trilha da ende.app: CC BY 4.0, uso comercial, sem Content ID.
|
|
3
|
+
*
|
|
4
|
+
* "Usar músicas CC BY 4.0 sem Content ID, da ende.app." — Arthur, do
|
|
5
|
+
* /brag (28/09/2026)
|
|
6
|
+
*
|
|
7
|
+
* Por que esta fonte: música do Openverse com licença comercial às vezes está
|
|
8
|
+
* registrada no Content ID, e o vídeo do cliente leva reivindicação no
|
|
9
|
+
* YouTube. A página da ende.app diz: "Important: NO Content ID. [...]
|
|
10
|
+
* Everything is under CC BY 4.0. [...] Commercial use is allowed as well."
|
|
11
|
+
* O crédito é opcional ("Attribution is nice but no longer required"), mas
|
|
12
|
+
* gravamos mesmo assim: custa uma linha e protege o cliente.
|
|
13
|
+
*
|
|
14
|
+
* ── SEM API ──────────────────────────────────────────────────────────────
|
|
15
|
+
* A ende.app não tem API, e a busca por texto (?q=) não filtra: devolve
|
|
16
|
+
* sempre a mesma lista. Então lemos os cartões da listagem, que trazem
|
|
17
|
+
* `musicPlayer(id, 'mp3', duração)`, o título, a página e as tags. A escolha é
|
|
18
|
+
* pelas tags e pela duração. `robots.txt` só fecha /user-api/ e /challenge.
|
|
19
|
+
*
|
|
20
|
+
* Se a página mudar, `lerLista` devolve [] e o provedor devolve null, e a
|
|
21
|
+
* corrente do producao.js cai no próximo provedor (o Openverse). A rede e o
|
|
22
|
+
* disco são injetados: o teste roda com um recorte do HTML real.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
const LISTA = 'https://ende.app/en/search'; // 25 cartões; a /en mostra 11
|
|
31
|
+
const CREDITO = 'Música: Sascha Ende (ende.app), CC BY 4.0';
|
|
32
|
+
|
|
33
|
+
// O clima do roteiro (pt) → palavras que aparecem nas tags da ende.app (en).
|
|
34
|
+
const CLIMAS = {
|
|
35
|
+
emocional: ['emotional', 'acoustic', 'atmospheric', 'piano', 'cinematic', 'ambient', 'calm', 'strings'],
|
|
36
|
+
cinema: ['cinematic', 'epic', 'orchestral', 'atmospheric', 'emotional', 'strings'],
|
|
37
|
+
energetico: ['energetic', 'upbeat', 'beat', 'drums', 'rock', 'arcade', 'breakcore', 'epic'],
|
|
38
|
+
viral: ['upbeat', 'beat', 'energetic', 'pop', 'electronic'],
|
|
39
|
+
impacto: ['epic', 'drums', 'trailer', 'energetic', 'beat'],
|
|
40
|
+
neutro: ['background', 'corporate', 'chilled', 'calm', 'ambient'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const limpar = (s) => String(s || '').replace(/<[^>]*>/g, '').replace(/&/g, '&').replace(/�?39;/g, "'").replace(/\s+/g, ' ').trim();
|
|
44
|
+
|
|
45
|
+
/** Os cartões de música da listagem. Página diferente do esperado: []. */
|
|
46
|
+
function lerLista(html) {
|
|
47
|
+
const texto = String(html || '');
|
|
48
|
+
const cortes = [...texto.matchAll(/musicPlayer\(\s*(\d+)\s*,\s*'([^']+\.mp3)'\s*,\s*([\d.]+)/g)];
|
|
49
|
+
return cortes.map((m, k) => {
|
|
50
|
+
const fim = k + 1 < cortes.length ? cortes[k + 1].index : texto.length;
|
|
51
|
+
const cartao = texto.slice(m.index, fim);
|
|
52
|
+
const link = cartao.match(/<a href="([^"]+)" class="ap-title-link">([\s\S]*?)<\/a>/);
|
|
53
|
+
const tags = [...cartao.matchAll(/class="ap-subline-tag">([^<]+)</g)].map((t) => limpar(t[1]).toLowerCase());
|
|
54
|
+
return {
|
|
55
|
+
id: Number(m[1]), mp3: m[2], segundos: Number(m[3]),
|
|
56
|
+
titulo: link ? limpar(link[2]) : null, pagina: link ? link[1] : null, tags,
|
|
57
|
+
};
|
|
58
|
+
}).filter((f) => f.titulo && f.segundos > 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A faixa que cobre o vídeo e mais combina com o clima. Nenhuma cobre: null. */
|
|
62
|
+
function escolher(faixas, { clima = 'neutro', segundos = 0 } = {}) {
|
|
63
|
+
const palavras = CLIMAS[clima] || CLIMAS.neutro;
|
|
64
|
+
const nota = (f) => f.tags.reduce((n, t) => n + palavras.filter((p) => t.includes(p)).length, 0);
|
|
65
|
+
const cabem = (faixas || []).filter((f) => f.segundos >= segundos);
|
|
66
|
+
if (!cabem.length) return null;
|
|
67
|
+
return cabem.reduce((a, b) => (nota(b) > nota(a) ? b : a));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function buscarPadrao(url) {
|
|
71
|
+
const r = await fetch(url, { headers: { 'User-Agent': 'PrimoCode-Estudio' } });
|
|
72
|
+
if (!r.ok) throw new Error(`ende.app respondeu ${r.status}`);
|
|
73
|
+
return r.text();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Baixa na pasta de mídia e grava o .json da licença ao lado, como o acervo faz. */
|
|
77
|
+
function baixarEm(pasta) {
|
|
78
|
+
return async (url, nome, meta) => {
|
|
79
|
+
const r = await fetch(url);
|
|
80
|
+
if (!r.ok) throw new Error(`download da ende.app respondeu ${r.status}`);
|
|
81
|
+
const destino = path.join(pasta, nome);
|
|
82
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
83
|
+
fs.writeFileSync(destino, Buffer.from(await r.arrayBuffer()));
|
|
84
|
+
fs.writeFileSync(destino + '.json', JSON.stringify(meta, null, 2));
|
|
85
|
+
return destino;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** O provedor de `musica` para o producao.js. */
|
|
90
|
+
function provedor({ buscar = buscarPadrao, baixar, pasta = 'midia' } = {}) {
|
|
91
|
+
const salvar = baixar || baixarEm(pasta);
|
|
92
|
+
return {
|
|
93
|
+
nome: 'ende', tipo: 'musica',
|
|
94
|
+
async gerar({ clima, segundos = 0 } = {}) {
|
|
95
|
+
const faixa = escolher(lerLista(await buscar(LISTA)), { clima, segundos });
|
|
96
|
+
if (!faixa) return null;
|
|
97
|
+
const meta = {
|
|
98
|
+
fonte: faixa.pagina, titulo: faixa.titulo, autor: 'Sascha Ende',
|
|
99
|
+
licenca: 'CC BY 4.0', contentId: false, credito: CREDITO,
|
|
100
|
+
};
|
|
101
|
+
const src = await salvar(faixa.mp3, `ende-${faixa.id}.mp3`, meta);
|
|
102
|
+
return { src, segundos: faixa.segundos, ...meta };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { lerLista, escolher, provedor, CLIMAS, CREDITO };
|
package/lib/estudio/plano.js
CHANGED
|
@@ -305,6 +305,25 @@ function quadros(plano) {
|
|
|
305
305
|
return Math.max(1, Math.round(segundos * fps));
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/**
|
|
309
|
+
* A capa queimada no QUADRO 0.
|
|
310
|
+
*
|
|
311
|
+
* "Discord, X e Slack ignoram a thumbnail" (Arthur, do /brag, 28/09): quem
|
|
312
|
+
* compartilha o mp4 vê o primeiro quadro, não a capa que o YouTube guardou.
|
|
313
|
+
* Então a capa vira o quadro 0, por cima da primeira cena, e some no quadro
|
|
314
|
+
* 1. Não é cena nova: a duração e o áudio ficam como estavam.
|
|
315
|
+
*/
|
|
316
|
+
function queimarCapa(plano, capa) {
|
|
317
|
+
if (!capa || !capa.src || !plano || !Array.isArray(plano.cenas) || !plano.cenas.length) return plano;
|
|
318
|
+
const fps = Number((plano.formato || {}).fps) || 30;
|
|
319
|
+
const [primeira, ...resto] = plano.cenas;
|
|
320
|
+
const quadro0 = {
|
|
321
|
+
tipo: 'imagem', src: capa.src, x: 0, y: 0, largura: '100%', altura: '100%',
|
|
322
|
+
ajuste: capa.ajuste || 'cover', de: 0, ate: 1 / fps,
|
|
323
|
+
};
|
|
324
|
+
return { ...plano, cenas: [{ ...primeira, elementos: [...(primeira.elementos || []), quadro0] }, ...resto] };
|
|
325
|
+
}
|
|
326
|
+
|
|
308
327
|
/** O que existe no plano, em texto curto — para o agente se situar sem reler tudo. */
|
|
309
328
|
function resumo(plano) {
|
|
310
329
|
if (!plano || !Array.isArray(plano.cenas)) return 'plano vazio';
|
|
@@ -325,6 +344,6 @@ function resumo(plano) {
|
|
|
325
344
|
}
|
|
326
345
|
|
|
327
346
|
module.exports = {
|
|
328
|
-
conferir, quadros, resumo,
|
|
347
|
+
conferir, quadros, resumo, queimarCapa,
|
|
329
348
|
TIPOS, FORMAS, ANIMAVEIS, ANIMAVEIS_CAMERA, SUAVIZACOES, TRANSICOES, AJUSTES,
|
|
330
349
|
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* producao.js — o que cada cena PRECISA, separado de QUEM gera cada coisa.
|
|
3
|
+
*
|
|
4
|
+
* "Separar o que cada cena precisa (a fala, a foto, o clipe) de quem gera
|
|
5
|
+
* cada coisa. Assim, trocar Gemini por Edge na voz vira trocar um
|
|
6
|
+
* provider, e o cinema.js não muda." — Arthur, sobre o Hypit (28/09/2026)
|
|
7
|
+
*
|
|
8
|
+
* Só a IDEIA do Hypit entra aqui: a licença dele proíbe embutir o código.
|
|
9
|
+
*
|
|
10
|
+
* ── AS TRÊS PEÇAS ────────────────────────────────────────────────────────
|
|
11
|
+
* necessidades(roteiro) o que o roteiro pede: narração, foto, clipe,
|
|
12
|
+
* música. Não sabe quem vai gerar nada.
|
|
13
|
+
* produzir(pedidos, lista) para cada pedido, tenta os provedores daquele
|
|
14
|
+
* tipo NA ORDEM da lista. Erro, vazio ou
|
|
15
|
+
* indisponível cai no próximo. Registra quem gerou.
|
|
16
|
+
* paraCinema(resultado) devolve no formato que o montarCinema já recebe
|
|
17
|
+
* (narracao, tempos, fotos, trilha).
|
|
18
|
+
*
|
|
19
|
+
* Um provedor é { nome, tipo, disponivel?(), gerar(pedido) }. `gerar` devolve
|
|
20
|
+
* o que produziu, ou null quando não conseguiu. Trocar Gemini por Edge é
|
|
21
|
+
* mudar a ordem da lista: nenhum estilo (cinema, impacto, edição) conhece
|
|
22
|
+
* provedor nenhum.
|
|
23
|
+
*
|
|
24
|
+
* Puro de propósito: a rede e o disco moram nos provedores, então o teste roda
|
|
25
|
+
* sem chave e sem internet.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
// Quantas fotos cada tipo de parte do cinema costuma consumir.
|
|
31
|
+
const FOTOS = { montagem: 3, climax: 4, numeros: 2, escada: 2, dia: 1 };
|
|
32
|
+
|
|
33
|
+
const textoPadrao = (p) => (p.fala != null ? p.fala : (p.linhas || []).join(' '));
|
|
34
|
+
const palavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
35
|
+
|
|
36
|
+
/** O que o roteiro pede, sem dizer quem gera. */
|
|
37
|
+
function necessidades(roteiro, { falaDe = textoPadrao } = {}) {
|
|
38
|
+
const partes = (roteiro && roteiro.partes) || [];
|
|
39
|
+
const pedidos = [];
|
|
40
|
+
const falas = [];
|
|
41
|
+
let segundos = 0;
|
|
42
|
+
|
|
43
|
+
partes.forEach((p, i) => {
|
|
44
|
+
if (p.tipo === 'depoimento') {
|
|
45
|
+
for (const c of p.clipes || []) {
|
|
46
|
+
pedidos.push({ tipo: 'clipe', parte: i, src: c.src, de: c.de, ate: c.ate });
|
|
47
|
+
segundos += Math.max(0, (c.ate || 0) - (c.de || 0));
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const fala = falaDe(p);
|
|
52
|
+
if (fala) { falas.push({ parte: i, texto: fala }); segundos += 0.4 + 0.34 * palavras(fala); }
|
|
53
|
+
if (FOTOS[p.tipo]) {
|
|
54
|
+
const termo = [p.antes, p.titulo, ...(p.linhas || []), ...(p.itens || [])].filter(Boolean).join(' ');
|
|
55
|
+
pedidos.push({ tipo: 'foto', parte: i, termo, quantas: FOTOS[p.tipo] });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (falas.length) {
|
|
60
|
+
pedidos.unshift({ tipo: 'narracao', partes: falas.map((f) => f.parte), falas: falas.map((f) => f.texto), idioma: roteiro.idioma || 'pt-BR' });
|
|
61
|
+
}
|
|
62
|
+
pedidos.push({ tipo: 'musica', clima: roteiro.clima || roteiro.estilo || 'neutro', segundos: Math.round(segundos) });
|
|
63
|
+
return pedidos;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Cada pedido pela corrente dos provedores do tipo dele, na ordem da lista. */
|
|
67
|
+
async function produzir(pedidos, provedores, { log = () => {} } = {}) {
|
|
68
|
+
const itens = [];
|
|
69
|
+
for (const pedido of pedidos || []) {
|
|
70
|
+
const tentou = [];
|
|
71
|
+
let feito = null;
|
|
72
|
+
for (const pv of (provedores || []).filter((x) => x && x.tipo === pedido.tipo)) {
|
|
73
|
+
if (pv.disponivel && !pv.disponivel()) continue;
|
|
74
|
+
try {
|
|
75
|
+
const r = await pv.gerar(pedido);
|
|
76
|
+
if (r) { feito = { ...pedido, ...r, ok: true, quem: pv.nome, tentou }; break; }
|
|
77
|
+
tentou.push({ nome: pv.nome, motivo: 'não devolveu nada' });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
tentou.push({ nome: pv.nome, motivo: (e && e.message) || String(e) });
|
|
80
|
+
}
|
|
81
|
+
log(`${pedido.tipo}: ${pv.nome} falhou, próximo`);
|
|
82
|
+
}
|
|
83
|
+
itens.push(feito || { ...pedido, ok: false, tentou });
|
|
84
|
+
}
|
|
85
|
+
return { itens, faltou: itens.filter((i) => !i.ok) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** O resultado no formato que o montarCinema já recebe. */
|
|
89
|
+
function paraCinema({ itens = [] } = {}) {
|
|
90
|
+
const ok = itens.filter((i) => i.ok);
|
|
91
|
+
const narr = ok.find((i) => i.tipo === 'narracao');
|
|
92
|
+
const musica = ok.find((i) => i.tipo === 'musica');
|
|
93
|
+
const fotos = ok.filter((i) => i.tipo === 'foto')
|
|
94
|
+
.flatMap((i) => (Array.isArray(i.fotos) ? i.fotos : [{ src: i.src, tipo: 'imagem' }]))
|
|
95
|
+
.filter((f) => f && f.src);
|
|
96
|
+
const creditos = [...new Set(ok.map((i) => i.credito).filter(Boolean))];
|
|
97
|
+
return {
|
|
98
|
+
narracao: narr ? narr.src : null,
|
|
99
|
+
tempos: narr ? narr.tempos || [] : [],
|
|
100
|
+
fotos,
|
|
101
|
+
trilha: musica ? musica.src : null,
|
|
102
|
+
creditos,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { necessidades, produzir, paraCinema, FOTOS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.9.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|