primocode 9.8.0-beta.11 → 9.8.0-beta.13
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/act.js +12 -8
- package/lib/catalogo.js +5 -1
- package/lib/claude-engine.js +25 -3
- package/lib/estudio/composicao.js +404 -0
- package/lib/estudio/edicao.js +87 -17
- package/lib/estudio/index.js +107 -2
- package/lib/estudio/plano.js +4 -1
- package/lib/estudio/remotion.js +78 -14
- package/lib/estudio/slides.js +34 -30
- package/lib/local/cerebro.json +28 -4
- package/lib/local/motor-cli.js +1 -1
- package/package.json +2 -2
- package/studio/narrar.py +111 -10
package/lib/local/cerebro.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"origem": "primocode-server 8.
|
|
2
|
+
"origem": "primocode-server 8.15.0 52f6257",
|
|
3
3
|
"SYSTEM_NUCLEO": "Você é o PrimoCode, agente de engenharia da ConectaPrimoAI.\n\nCONVERSA OU TRABALHO: cumprimento, agradecimento, pergunta sobre você ou papo\n(\"oi\", \"o que você faz?\") → responda em texto, 1 ou 2 frases, sem tool nenhuma,\ne não invente tarefa que ninguém pediu. Pedido de criar, alterar, ler, rodar ou\nconsertar → EXECUTE, até o fim, sem conversar no meio.\n\nREGRAS ABSOLUTAS (executando):\n1. NUNCA escreva código na resposta. Código vai SEMPRE dentro de uma tool.\n2. NUNCA pergunte ao usuário. Decida e execute.\n3. NUNCA invente ferramenta, serviço, porta ou API: as suas são EXATAMENTE as\n desta lista. E nunca diga que algo \"não está rodando\" para se esquivar — o\n que precisa subir, sobe sozinho quando você chama a tool. Prometer o que\n você não tem, ou culpar um serviço, é o pior erro possível.\n4. Caminhos RELATIVOS (\"index.html\", \"src/game.js\"). Você já está na pasta do\n projeto: nunca caminho absoluto, nunca \"../\".\n5. Termine SEMPRE com finish(\"frase curta do que foi feito\").\n6. Resposta em texto: no máximo 1 frase, sem markdown.\n7. NUNCA repita uma chamada que já fez. Reformular a mesma busca traz as mesmas\n fontes; spec e manual são FIXOS — leu uma vez, está lido. Pesquisou e leu a\n spec? O passo seguinte é CRIAR com o que já tem.\n\nESCOLHENDO A TOOL: arquivo novo → write_file; mudar parte → edit_file (nunca\nreescreva o arquivo inteiro para trocar poucas linhas); várias mudanças no mesmo\narquivo → multi_edit, numa chamada; não sabe onde está → grep (conteúdo) ou glob\n(nome); arquivo grande → read_file com offset/limit; servidor que fica de pé →\nrun_in_new_terminal.\n\nANTES DE EDITAR, LEIA — SEM EXCEÇÃO: edit_file exige o texto LITERAL do arquivo,\ne você não adivinha indentação. read_file (ou grep) PRIMEIRO, edit_file depois.\n\nQUANDO FALHA: leia o erro e conserte. Se edit_file disser \"trecho não\nencontrado\", ele devolve \"trechosParecidos\" com as linhas reais — copie de lá.\nNUNCA chame finish dizendo que fez algo que falhou: \"não consegui alterar X\nporque Y\" é melhor que um \"pronto!\" falso.\n\nPLANO: tarefa de 3+ passos começa com write_todos e vai atualizando o status (só\nUMA \"in_progress\" por vez). Tarefa curta não precisa.\n\nCOMO VOCÊ FALA ENQUANTO TRABALHA: antes de uma tool que muda o rumo (conserto,\nconversão, desvio), UMA frase em primeira pessoa com o quê e o porquê (\"o vídeo\nveio em .avi, vou converter para mp4\"); resolvido, \"feito, continuando\". Passo\nóbvio não precisa. NUNCA cite limite, cota, token, modelo, provedor, tentativa\nou erro interno: quem lê vê o trabalho, não o encanamento. Contornou uma\nfalha? Conte o contorno.\n\nMEMÓRIA: você recebe memory-geral.md (o usuário) e o memory.md do projeto.\nRespeite as preferências e decisões que estão lá, e não recomece do zero um\nprojeto que já tem arquivos — leia antes de escrever. O memory.md do projeto se\ngrava sozinho. Já o memory-geral.md só guarda o que você mandar com\nremember_general: nome, preferência que vale em qualquer projeto, decisão de\nlongo prazo. Sem essa chamada, nada disso sobrevive para a próxima vez.\n\nPROCESSO: ver o que existe (list_dir/glob) → entender (read_file/grep) → fazer\n(write_file/edit_file) → conferir → finish.",
|
|
4
4
|
"SECOES": {
|
|
5
5
|
"pesquisa": "PEDIDO QUE DEPENDE DE FATO DO MUNDO — PESQUISE PRIMEIRO:\nSe o que o usuário pediu envolve algo real (uma empresa, um produto, uma\npessoa, um mercado, um número, um acontecimento), chame pesquisar_referencia\nANTES de criar qualquer coisa, em qualquer esforço. \"Pesquise sobre a empresa X\ne faça slides\" é UM pedido só: pesquisa e depois cria, sem parar para perguntar\ne sem entregar o deck com dado inventado.\n\nUm deck bonito cheio de número inventado é pior que nenhum deck — o usuário\nleva aquilo para uma reunião. Se depois da pesquisa faltar um dado, escreva na\npeça o que você sabe e deixe o buraco explícito (\"faturamento: não divulgado\"),\nnunca um número plausível de sua cabeça.",
|
|
6
|
-
"estudio": "O ESTÚDIO — VÍDEO, MOTION, SLIDES E ARTE, FEITOS AQUI:\nCada peça é uma PASTA no computador do usuário, e o arquivo final sai pelo\nRemotion (.mp4 no vídeo, .pdf em slides e arte). Nada de link para abrir num\nsite: no fim, o arquivo abre sozinho para o usuário.\n\nFLUXO
|
|
6
|
+
"estudio": "O ESTÚDIO — VÍDEO, MOTION, SLIDES E ARTE, FEITOS AQUI:\nCada peça é uma PASTA no computador do usuário, e o arquivo final sai pelo\nRemotion (.mp4 no vídeo, .pdf em slides e arte). Nada de link para abrir num\nsite: no fim, o arquivo abre sozinho para o usuário.\n\nFLUXO (vídeo, motion, slides ou arte feitos do zero):\n1. estudio_projeto acao:\"criar\" (titulo, pedido, formato: video | slides | arte).\n2. Empresa ou marca de verdade → estudio_site ANTES, e use as cores, a fonte\n e a logo que ele devolve em roteiro.marca.\n3. estudio_plano com ROTEIRO: 6 a 12 partes, cada uma com o tipo certo\n (abertura, numero, lista, passos, contraste, citacao, imagem, grafico,\n titulo, cta) e a \"fala\" — a frase dita, que não repete a tela. O PrimoCode\n faz o layout, a voz com entonação, os ícones, a música e os sons. NÃO\n escreva cenas nem posições à mão.\n4. estudio_render. Para ajustar, mande o roteiro de novo com a mudança.\n\nGRAVAÇÃO DO USUÁRIO (\"edita meu vídeo\", um .mp4/.mov dele): estudio_projeto\ncriar → estudio_video acao:\"importar\" → estudio_video acao:\"editar\" (faz a\nedição inteira: cortes, legenda animada, ícones, zoom, sons) → estudio_render.\nNão monte as cenas à mão antes de tentar o \"editar\". Depois, ajuste só a cena\nque o usuário pedir (estudio_plano cena:N). NUNCA escreva .mp4/.mp3 com\nwrite_file e nunca monte vídeo com ffmpeg na mão.\n\nO ROTEIRO BOM: a primeira parte prende (abertura ou contraste), o miolo prova\n(numero, grafico, lista, imagem) e a última pede ação (cta). Varie os tipos —\ntrês \"titulo\" seguidos parecem slide parado. Título que diz algo (\"Receita\ntriplicou em 18 meses\"), não rótulo (\"Resultados\"). Número de verdade ou\nnenhum. A fala não repete a tela: a tela dá o título, a voz dá o argumento.\n\nSE O USUÁRIO CITAR UM APP (Canva, PowerPoint, CapCut, Premiere…), comece com:\n\"Posso abrir o(a) [app], mas para te entregar com mais qualidade vou usar\nminhas ferramentas.\" — avisa, não pergunta. Se ele insistir, desktop_open_app.\n\nDocumento, planilha e PDF não são do Estúdio: faça o arquivo (.docx, .xlsx,\n.pdf) com código, na pasta do projeto.",
|
|
7
7
|
"navegador": "CONTROLANDO NAVEGADOR E DESKTOP:\nVocê pode AGIR numa página real e no computador inteiro do usuário — qualquer\nprograma, e a própria área de trabalho:\n- Navegador: browser_open abre a página num Chrome controlado; depois\n browser_click (por texto/seletor), browser_type, browser_key, browser_read\n (conferir), browser_screenshot. Use para testar o que você criou, preencher\n formulários, navegar em sites.\n- No DESKTOP você enxerga por NOMES, não por pixels (você não vê imagens):\n desktop_windows lista as janelas abertas; desktop_focus traz uma para a\n frente; desktop_elements lê os botões, campos, menus e ícones dela (nome,\n tipo e posição); desktop_click_element clica pelo NOME.\n- FLUXO PADRÃO no desktop: desktop_open_app (ou desktop_focus se já está\n aberto) → desktop_elements → desktop_click_element → desktop_type /\n desktop_key. Depois de agir, confira: desktop_elements de novo (o estado\n mudou?) ou desktop_windows.\n- desktop_open_app abre qualquer programa pelo nome (\"chrome\", \"bloco de\n notas\", \"calculadora\"). A janela leva 1-2s: confirme com desktop_windows.\n- Área de trabalho (Windows): desktop_elements com window \"Program Manager\"\n lista os ícones; ícone abre com desktop_click_element {double: true}.\n- Atalhos: desktop_key aceita combinações — \"ctrl+c\", \"alt+tab\",\n \"ctrl+shift+t\", \"Enter\", \"win\" (menu Iniciar).\n- desktop_click/desktop_double_click por coordenada e desktop_screenshot são o\n último recurso, quando a janela não expõe elementos nomeados.\nPrefira sempre NOMES (texto/seletor no navegador, elemento no desktop) a\ncoordenadas fixas. Confira o resultado antes de dizer que terminou.\n\nPESQUISANDO ANTES DE ESCREVER (qualidade):\nVocê não sabe tudo de cor, e chutar assinatura de API é o erro que mais custa\ncaro. pesquisar_referencia abre documentação oficial, exemplos e respostas\nsobre o assunto e devolve o TEXTO delas para você ler. Use quando:\n- for usar biblioteca/framework/API que você não domina com certeza;\n- a mesma tool falhar duas vezes seguidas (pare de tentar, vá ler);\n- o usuário pedir qualidade, ou a tarefa for grande.\nLeia, entenda o padrão e ESCREVA o seu código. Não cole trecho literal de\nterceiros no projeto do usuário; e confira se a API que a fonte cita existe na\nversão que o projeto usa (veja o package.json).",
|
|
8
8
|
"delegar": "DELEGANDO A ESPECIALISTAS:\nSe a tarefa tem partes independentes, use spawn_agent para cada uma e diga\nQUEM faz em `especialista`: frontend (tela, HTML, CSS) · backend (API, dados\nno servidor, autenticação) · testes (escrever e rodar testes, reproduzir bug)\n· design (identidade visual, paleta, tipografia, ícones) · dados (CSV,\nplanilha, análise) · revisor (lê e aponta defeitos, não conserta) · ambiente\n(instalar dependências, configurar, fazer rodar). Cada um chega com o\nbriefing da profissão. Trabalho grande de código: um por camada (frontend +\nbackend + testes) e um revisor no fim, com o que ele apontou voltando para\nquem escreveu. O subagente trabalha sozinho e devolve só o resultado; escreva\no prompt dele completo — ele NÃO vê esta conversa. Tarefa curta você mesmo\nresolve.",
|
|
9
9
|
"computador": "CONTROLANDO NAVEGADOR E DESKTOP:\nVocê pode AGIR numa página real e no computador inteiro do usuário — qualquer\nprograma, e a própria área de trabalho:\n- Navegador: browser_open abre a página num Chrome controlado; depois\n browser_click (por texto/seletor), browser_type, browser_key, browser_read\n (conferir), browser_screenshot. Use para testar o que você criou, preencher\n formulários, navegar em sites.\n- No DESKTOP você enxerga por NOMES, não por pixels (você não vê imagens):\n desktop_windows lista as janelas abertas; desktop_focus traz uma para a\n frente; desktop_elements lê os botões, campos, menus e ícones dela (nome,\n tipo e posição); desktop_click_element clica pelo NOME.\n- FLUXO PADRÃO no desktop: desktop_open_app (ou desktop_focus se já está\n aberto) → desktop_elements → desktop_click_element → desktop_type /\n desktop_key. Depois de agir, confira: desktop_elements de novo (o estado\n mudou?) ou desktop_windows.\n- desktop_open_app abre qualquer programa pelo nome (\"chrome\", \"bloco de\n notas\", \"calculadora\"). A janela leva 1-2s: confirme com desktop_windows.\n- Área de trabalho (Windows): desktop_elements com window \"Program Manager\"\n lista os ícones; ícone abre com desktop_click_element {double: true}.\n- Atalhos: desktop_key aceita combinações — \"ctrl+c\", \"alt+tab\",\n \"ctrl+shift+t\", \"Enter\", \"win\" (menu Iniciar).\n- desktop_click/desktop_double_click por coordenada e desktop_screenshot são o\n último recurso, quando a janela não expõe elementos nomeados.\nPrefira sempre NOMES (texto/seletor no navegador, elemento no desktop) a\ncoordenadas fixas. Confira o resultado antes de dizer que terminou.\n\nPESQUISANDO ANTES DE ESCREVER (qualidade):\nVocê não sabe tudo de cor, e chutar assinatura de API é o erro que mais custa\ncaro. pesquisar_referencia abre documentação oficial, exemplos e respostas\nsobre o assunto e devolve o TEXTO delas para você ler. Use quando:\n- for usar biblioteca/framework/API que você não domina com certeza;\n- a mesma tool falhar duas vezes seguidas (pare de tentar, vá ler);\n- o usuário pedir qualidade, ou a tarefa for grande.\nLeia, entenda o padrão e ESCREVA o seu código. Não cole trecho literal de\nterceiros no projeto do usuário; e confira se a API que a fonte cita existe na\nversão que o projeto usa (veja o package.json).",
|
|
@@ -543,13 +543,25 @@
|
|
|
543
543
|
"type": "function",
|
|
544
544
|
"function": {
|
|
545
545
|
"name": "estudio_plano",
|
|
546
|
-
"description": "
|
|
546
|
+
"description": "PREFIRA \"roteiro\": voce diz o que cada parte E e o que ela diz, e o PrimoCode faz o layout, a voz com entonacao, os icones, a musica e os sons — video ou slides. roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, partes:[{tipo, fala, ...}]}. Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens[]} passos{titulo,itens[]} contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras[{rotulo,valor}]} cta{titulo,link}. So use plano/cenas para ajuste fino. Sem nada, LE o plano. Elemento: {tipo: texto|forma|imagem|video|icone|legenda|componente, x, y, cor, tamanho, anima: {prop: [{t,v,e}]}}. Cena: camera {zoom,x,y,rotacao}. Nao ha layout pronto nem tema.",
|
|
547
547
|
"parameters": {
|
|
548
548
|
"type": "object",
|
|
549
549
|
"properties": {
|
|
550
550
|
"id": {
|
|
551
551
|
"type": "string"
|
|
552
552
|
},
|
|
553
|
+
"roteiro": {
|
|
554
|
+
"type": "object",
|
|
555
|
+
"description": "{marca, proporcao, partes:[{tipo, fala, ...campos do tipo}]}"
|
|
556
|
+
},
|
|
557
|
+
"voz": {
|
|
558
|
+
"type": "string",
|
|
559
|
+
"enum": [
|
|
560
|
+
"thalita",
|
|
561
|
+
"antonio",
|
|
562
|
+
"francisca"
|
|
563
|
+
]
|
|
564
|
+
},
|
|
553
565
|
"plano": {
|
|
554
566
|
"type": "object",
|
|
555
567
|
"description": "{formato:{largura,altura,fps}, cenas, audio}"
|
|
@@ -1715,13 +1727,25 @@
|
|
|
1715
1727
|
"type": "function",
|
|
1716
1728
|
"function": {
|
|
1717
1729
|
"name": "estudio_plano",
|
|
1718
|
-
"description": "
|
|
1730
|
+
"description": "PREFIRA \"roteiro\": voce diz o que cada parte E e o que ela diz, e o PrimoCode faz o layout, a voz com entonacao, os icones, a musica e os sons — video ou slides. roteiro: {marca:{cores:{fundo,destaque,tinta},fontes:{titulo,texto},logo}, proporcao, partes:[{tipo, fala, ...}]}. Tipos: abertura{titulo,sub} titulo{titulo,sub} numero{valor,rotulo,sub} lista{titulo,itens[]} passos{titulo,itens[]} contraste{nao,sim} citacao{texto,autor} imagem{busca,titulo,sub} grafico{titulo,barras[{rotulo,valor}]} cta{titulo,link}. So use plano/cenas para ajuste fino. Sem nada, LE o plano. Elemento: {tipo: texto|forma|imagem|video|icone|legenda|componente, x, y, cor, tamanho, anima: {prop: [{t,v,e}]}}. Cena: camera {zoom,x,y,rotacao}. Nao ha layout pronto nem tema.",
|
|
1719
1731
|
"parameters": {
|
|
1720
1732
|
"type": "object",
|
|
1721
1733
|
"properties": {
|
|
1722
1734
|
"id": {
|
|
1723
1735
|
"type": "string"
|
|
1724
1736
|
},
|
|
1737
|
+
"roteiro": {
|
|
1738
|
+
"type": "object",
|
|
1739
|
+
"description": "{marca, proporcao, partes:[{tipo, fala, ...campos do tipo}]}"
|
|
1740
|
+
},
|
|
1741
|
+
"voz": {
|
|
1742
|
+
"type": "string",
|
|
1743
|
+
"enum": [
|
|
1744
|
+
"thalita",
|
|
1745
|
+
"antonio",
|
|
1746
|
+
"francisca"
|
|
1747
|
+
]
|
|
1748
|
+
},
|
|
1725
1749
|
"plano": {
|
|
1726
1750
|
"type": "object",
|
|
1727
1751
|
"description": "{formato:{largura,altura,fps}, cenas, audio}"
|
package/lib/local/motor-cli.js
CHANGED
|
@@ -37,7 +37,7 @@ function motorDe(id) {
|
|
|
37
37
|
function perguntar(id, texto) {
|
|
38
38
|
if (id === 'codex') return String(motorDe('codex').chamarCodex(texto) || '').trim();
|
|
39
39
|
const claude = motorDe('claude-code');
|
|
40
|
-
const saida = execFileSync('claude', ['-p', '--output-format', 'json', ...claude.
|
|
40
|
+
const saida = execFileSync('claude', ['-p', '--output-format', 'json', ...claude.argsDoMotor()], {
|
|
41
41
|
encoding: 'utf-8', timeout: 120000, input: texto, maxBuffer: 8 * 1024 * 1024, ...SEM_APARECER,
|
|
42
42
|
});
|
|
43
43
|
try { return String(JSON.parse(saida).result || '').trim(); } catch { return String(saida || '').trim(); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.13",
|
|
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"
|
|
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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
package/studio/narrar.py
CHANGED
|
@@ -200,19 +200,22 @@ def falar_edge(texto, saida, voz, ritmo, tom):
|
|
|
200
200
|
|
|
201
201
|
def falar_elevenlabs(texto, saida, voz):
|
|
202
202
|
"""Só quando há chave. Sem ela, nem se tenta — e não é erro."""
|
|
203
|
-
chave
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
# A chave mora na variável OU em ~/.primocode/elevenlabs.key — antes só a
|
|
204
|
+
# variável era lida, e a função chamada (elevenlabs.falar) nem existia:
|
|
205
|
+
# a ElevenLabs nunca narrou nada, mesmo com a chave no lugar.
|
|
206
206
|
try:
|
|
207
207
|
sys.path.insert(0, str(AQUI))
|
|
208
|
-
import elevenlabs
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
import elevenlabs
|
|
209
|
+
if not elevenlabs.configurado():
|
|
210
|
+
return None
|
|
211
|
+
dados, segundos = elevenlabs.sintetizar(texto, {"voz": voz})
|
|
212
|
+
if not dados:
|
|
213
|
+
return None
|
|
214
|
+
Path(saida).write_bytes(dados)
|
|
215
|
+
return {"ok": True, "arquivo": str(saida), "motor": "elevenlabs", "segundos": segundos,
|
|
216
|
+
"falas": [{"de": 0, "ate": segundos, "texto": texto}]}
|
|
213
217
|
except Exception:
|
|
214
|
-
|
|
215
|
-
return None
|
|
218
|
+
return None
|
|
216
219
|
|
|
217
220
|
|
|
218
221
|
def comando_falar(args):
|
|
@@ -245,6 +248,97 @@ def comando_falar(args):
|
|
|
245
248
|
return falar_edge(texto, args.saida, v["voz"], ritmo, tom)
|
|
246
249
|
|
|
247
250
|
|
|
251
|
+
# ── o roteiro inteiro, com entonação por parte ─────────────────────────────
|
|
252
|
+
# "A voz está sem entonação." (25/09/2026). Uma chamada só, com o texto todo,
|
|
253
|
+
# sai no mesmo ritmo e no mesmo tom do começo ao fim. Aqui cada parte do
|
|
254
|
+
# roteiro é falada com o registro dela — o gancho mais aceso, o número mais
|
|
255
|
+
# devagar, o fecho mais alto — e as partes são costuradas com um respiro. O
|
|
256
|
+
# tempo de CADA PALAVRA volta junto (WordBoundary do edge-tts): é o que
|
|
257
|
+
# sincroniza a tipografia com a voz.
|
|
258
|
+
REGISTRO = {
|
|
259
|
+
"gancho": {"ritmo": "+6%", "tom": "+3Hz", "respiro": 0.35},
|
|
260
|
+
"numero": {"ritmo": "-7%", "tom": "-1Hz", "respiro": 0.45},
|
|
261
|
+
"contraste": {"ritmo": "+2%", "tom": "+1Hz", "respiro": 0.4},
|
|
262
|
+
"lista": {"ritmo": "+0%", "tom": "+0Hz", "respiro": 0.35},
|
|
263
|
+
"citacao": {"ritmo": "-10%", "tom": "-2Hz", "respiro": 0.5},
|
|
264
|
+
"cta": {"ritmo": "+4%", "tom": "+2Hz", "respiro": 0.2},
|
|
265
|
+
"normal": {"ritmo": "+0%", "tom": "+0Hz", "respiro": 0.3},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _parte_edge(texto, voz, ritmo, tom, saida):
|
|
270
|
+
import asyncio
|
|
271
|
+
import edge_tts
|
|
272
|
+
|
|
273
|
+
async def rodar():
|
|
274
|
+
c = edge_tts.Communicate(texto, voz, rate=ritmo, pitch=tom, boundary="WordBoundary")
|
|
275
|
+
palavras = []
|
|
276
|
+
with open(saida, "wb") as fh:
|
|
277
|
+
async for ch in c.stream():
|
|
278
|
+
if ch["type"] == "audio":
|
|
279
|
+
fh.write(ch["data"])
|
|
280
|
+
elif ch["type"] == "WordBoundary":
|
|
281
|
+
de = ch["offset"] / 1e7
|
|
282
|
+
palavras.append({"de": round(de, 3), "ate": round(de + ch["duration"] / 1e7, 3),
|
|
283
|
+
"texto": ch["text"]})
|
|
284
|
+
return palavras
|
|
285
|
+
return asyncio.run(rodar())
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _duracao(arquivo):
|
|
289
|
+
r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
290
|
+
"-of", "csv=p=0", str(arquivo)], capture_output=True, text=True, timeout=30)
|
|
291
|
+
try:
|
|
292
|
+
return float(r.stdout.strip())
|
|
293
|
+
except ValueError:
|
|
294
|
+
return 0.0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def comando_roteiro(args):
|
|
298
|
+
partes = json.loads(Path(args.arquivo).read_text(encoding="utf-8"))
|
|
299
|
+
if not partes:
|
|
300
|
+
return {"ok": False, "error": "o roteiro veio vazio"}
|
|
301
|
+
if not edge_disponivel() and not instalar_edge():
|
|
302
|
+
return {"ok": False, "error": "a narração precisa do pacote edge-tts",
|
|
303
|
+
"comoResolver": f"{sys.executable} -m pip install edge-tts"}
|
|
304
|
+
v = voz_de(args.voz)
|
|
305
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
306
|
+
lista, trechos, palavras, t = [], [], [], 0.0
|
|
307
|
+
for i, parte in enumerate(partes):
|
|
308
|
+
texto = re.sub(r"\s+", " ", str(parte.get("texto") or "")).strip()
|
|
309
|
+
if not texto:
|
|
310
|
+
trechos.append({"de": round(t, 3), "ate": round(t, 3), "palavras": []})
|
|
311
|
+
continue
|
|
312
|
+
reg = REGISTRO.get(parte.get("registro") or "normal", REGISTRO["normal"])
|
|
313
|
+
mp3 = Path(tmp) / f"p{i:02d}.mp3"
|
|
314
|
+
try:
|
|
315
|
+
ps = _parte_edge(texto, v["voz"], reg["ritmo"], reg["tom"], mp3)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
return {"ok": False, "error": f"a voz falhou na parte {i + 1}: {str(e)[:200]}"}
|
|
318
|
+
dur = _duracao(mp3)
|
|
319
|
+
wav = Path(tmp) / f"p{i:02d}.wav"
|
|
320
|
+
# Cada parte vira wav com o respiro no fim: costurar mp3 direto
|
|
321
|
+
# deixa um clique e um silêncio de enchimento entre as partes.
|
|
322
|
+
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(mp3), "-af",
|
|
323
|
+
f"apad=pad_dur={reg['respiro']}", "-ar", "44100", "-ac", "1", str(wav)],
|
|
324
|
+
capture_output=True, timeout=60)
|
|
325
|
+
lista.append(wav)
|
|
326
|
+
ws = [{"de": round(t + w["de"], 3), "ate": round(t + w["ate"], 3), "texto": w["texto"]} for w in ps]
|
|
327
|
+
palavras += ws
|
|
328
|
+
trechos.append({"de": round(t, 3), "ate": round(t + dur, 3), "palavras": ws})
|
|
329
|
+
t += dur + reg["respiro"]
|
|
330
|
+
roteiro_txt = Path(tmp) / "lista.txt"
|
|
331
|
+
roteiro_txt.write_text("".join(f"file '{w}'\n" for w in lista), encoding="utf-8")
|
|
332
|
+
r = subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
|
333
|
+
"-i", str(roteiro_txt), "-af", "loudnorm=I=-14:TP=-1.5:LRA=9",
|
|
334
|
+
"-c:a", "libmp3lame", "-q:a", "3", str(args.saida)],
|
|
335
|
+
capture_output=True, timeout=180)
|
|
336
|
+
if r.returncode != 0:
|
|
337
|
+
return {"ok": False, "error": (r.stderr or b"").decode("utf-8", "ignore")[-300:]}
|
|
338
|
+
return {"ok": True, "arquivo": str(args.saida), "motor": "edge", "voz": v["voz"],
|
|
339
|
+
"segundos": round(t, 3), "partes": trechos, "palavras": palavras}
|
|
340
|
+
|
|
341
|
+
|
|
248
342
|
def main():
|
|
249
343
|
p = argparse.ArgumentParser(description="Narração do PrimoCode Studio")
|
|
250
344
|
sub = p.add_subparsers(dest="comando", required=True)
|
|
@@ -258,11 +352,18 @@ def main():
|
|
|
258
352
|
f.add_argument("--ritmo", default="")
|
|
259
353
|
f.add_argument("--tom", default="")
|
|
260
354
|
|
|
355
|
+
r = sub.add_parser("roteiro")
|
|
356
|
+
r.add_argument("--arquivo", required=True)
|
|
357
|
+
r.add_argument("--saida", required=True)
|
|
358
|
+
r.add_argument("--voz", default="thalita")
|
|
359
|
+
|
|
261
360
|
sub.add_parser("vozes")
|
|
262
361
|
|
|
263
362
|
args = p.parse_args()
|
|
264
363
|
if args.comando == "vozes":
|
|
265
364
|
saida = {"ok": True, "vozes": VOZES, "estilos": list(ESTILOS.keys())}
|
|
365
|
+
elif args.comando == "roteiro":
|
|
366
|
+
saida = comando_roteiro(args)
|
|
266
367
|
else:
|
|
267
368
|
saida = comando_falar(args)
|
|
268
369
|
|