primocode 8.42.2 → 9.0.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.
Files changed (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
@@ -0,0 +1,216 @@
1
+ """orbe.py — a interface inteira: um núcleo pequeno e um fundo fosco.
2
+
3
+ "Redesenhar a interface: nada de fundo escuro cheio de elementos. Apenas
4
+ um núcleo pequeno, tipo uma orb, com um fundo fosco/desfocado por trás.
5
+ Ocultar transcrição, chat e qualquer painel. É só a orb e o áudio."
6
+
7
+ ── O QUE ISTO NÃO TEM ──────────────────────────────────────────────────────
8
+ Não há transcrição do que você falou, não há legenda do que ele está fazendo,
9
+ não há resposta escrita, não há janela de conversa, não há barra de comando.
10
+ Não é economia de trabalho: ler a resposta de um assistente de voz é trabalho,
11
+ e ouvir não é. Se a interface mostrasse o texto, a pessoa leria — e aí o modo
12
+ voz vira um chat com um microfone.
13
+
14
+ O que a orb comunica é o ESTADO, por cor e movimento: parada quando espera,
15
+ pulsando quando ouve, girando devagar quando pensa, brilhando quando fala.
16
+ Quatro estados, nenhuma palavra.
17
+
18
+ ── POR QUE tkinter ─────────────────────────────────────────────────────────
19
+ Ele vem com o Python. Qualquer outra escolha (Qt, GTK, pywebview, Electron)
20
+ significaria dezenas ou centenas de megabytes para desenhar um círculo — e o
21
+ `/voice` já precisa instalar sounddevice e numpy. Cada dependência a mais é
22
+ uma chance a mais de o modo voz não abrir na máquina de alguém.
23
+
24
+ ── O FUNDO FOSCO, E O QUE ELE É DE VERDADE ─────────────────────────────────
25
+ Desfoque real do que está ATRÁS da janela não existe de forma portátil: no
26
+ macOS é NSVisualEffectView, no Windows é DwmEnableBlurBehindWindow, no Linux
27
+ depende do compositor. Nenhum deles está no Python padrão.
28
+
29
+ O que dá para fazer em todo lugar é uma camada escura semitransparente por
30
+ cima da tela inteira. Não é desfoque; é escurecimento. A diferença aparece num
31
+ fundo cheio de detalhe, e some num fundo comum.
32
+
33
+ Onde o desfoque de verdade existe sem custo — macOS com pyobjc já instalado —
34
+ ele é usado. Onde não existe, a camada escura entra, e a orb continua sendo o
35
+ que importa. O que não se faz é fingir: nada aqui chama de "desfoque" o que é
36
+ só uma sombra.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import math
41
+ import queue
42
+ import threading
43
+ import tkinter as tk
44
+ from typing import Callable, Optional
45
+
46
+ # Os quatro estados, e a cor de cada um. A paleta é a do PrimoCode (ciano de
47
+ # marca, âmbar de aviso) para o modo voz não parecer outro produto.
48
+ CORES = {
49
+ "esperando": ("#0891b2", "#164e63"), # ciano recuado: pronto, sem pressa
50
+ "ouvindo": ("#22d3ee", "#0e7490"), # ciano vivo: a vez é sua
51
+ "pensando": ("#fbbf24", "#78350f"), # âmbar: ele está trabalhando
52
+ "falando": ("#67e8f9", "#0891b2"), # ciano claro: a voz é dele
53
+ }
54
+
55
+ TAMANHO = 220 # a janela da orb, em pixels
56
+ RAIO_BASE = 46 # o núcleo
57
+
58
+
59
+ class Orbe:
60
+ """A janela da orb. Roda na thread principal, como toda interface."""
61
+
62
+ def __init__(self, ao_fechar: Optional[Callable] = None):
63
+ self.raiz = tk.Tk()
64
+ self.raiz.title("Primo Code Voice")
65
+ self.estado = "esperando"
66
+ self.nivel = 0.0 # 0..1, o quanto a pessoa está falando
67
+ self._fase = 0.0
68
+ self._fila = queue.Queue()
69
+ self._ao_fechar = ao_fechar
70
+ self._vivo = True
71
+
72
+ self._montar_veu()
73
+ self._montar_orbe()
74
+ self.raiz.protocol("WM_DELETE_WINDOW", self.fechar)
75
+ self.raiz.bind("<Escape>", lambda e: self.fechar())
76
+ self._pintar()
77
+
78
+ # ── o véu ────────────────────────────────────────────────────────────
79
+ def _montar_veu(self) -> None:
80
+ """A camada escura sobre a tela inteira.
81
+
82
+ `-alpha` é o único jeito portátil de ter transparência de janela em
83
+ tkinter, e ele vale para a janela INTEIRA — por isso o véu é uma
84
+ janela separada da orb: se fossem a mesma, a orb ficaria translúcida
85
+ junto com o fundo, e um núcleo translúcido some.
86
+ """
87
+ self.veu = tk.Toplevel(self.raiz)
88
+ self.veu.overrideredirect(True)
89
+ largura = self.veu.winfo_screenwidth()
90
+ altura = self.veu.winfo_screenheight()
91
+ self.veu.geometry(f"{largura}x{altura}+0+0")
92
+ self.veu.configure(bg="#05070a")
93
+ try:
94
+ self.veu.attributes("-alpha", 0.72)
95
+ self.veu.attributes("-topmost", True)
96
+ except tk.TclError:
97
+ pass
98
+ # O véu não recebe clique: a pessoa continua usando o computador por
99
+ # baixo dele. Onde o sistema não deixa, ele simplesmente cobre.
100
+ try:
101
+ self.veu.attributes("-transparent", True)
102
+ except tk.TclError:
103
+ pass
104
+
105
+ # ── a orb ────────────────────────────────────────────────────────────
106
+ def _montar_orbe(self) -> None:
107
+ self.raiz.overrideredirect(True)
108
+ largura = self.raiz.winfo_screenwidth()
109
+ altura = self.raiz.winfo_screenheight()
110
+ x = (largura - TAMANHO) // 2
111
+ y = (altura - TAMANHO) // 2
112
+ self.raiz.geometry(f"{TAMANHO}x{TAMANHO}+{x}+{y}")
113
+ self.raiz.configure(bg="#05070a")
114
+ try:
115
+ self.raiz.attributes("-topmost", True)
116
+ # A cor de fundo vira transparente onde o sistema deixa (Windows),
117
+ # e no resto ela é a mesma do véu — o que dá o mesmo efeito.
118
+ self.raiz.attributes("-transparentcolor", "#05070a")
119
+ except tk.TclError:
120
+ pass
121
+
122
+ self.tela = tk.Canvas(self.raiz, width=TAMANHO, height=TAMANHO,
123
+ bg="#05070a", highlightthickness=0)
124
+ self.tela.pack()
125
+
126
+ # ── o desenho ────────────────────────────────────────────────────────
127
+ def _pintar(self) -> None:
128
+ if not self._vivo:
129
+ return
130
+ while True:
131
+ try:
132
+ estado, nivel = self._fila.get_nowait()
133
+ except queue.Empty:
134
+ break
135
+ if estado:
136
+ self.estado = estado
137
+ if nivel is not None:
138
+ self.nivel = max(0.0, min(1.0, nivel))
139
+
140
+ self.tela.delete("all")
141
+ vivo, fundo = CORES.get(self.estado, CORES["esperando"])
142
+ centro = TAMANHO / 2
143
+ self._fase += 0.06
144
+
145
+ # O movimento é diferente em cada estado, e é ele que diz o que está
146
+ # acontecendo — sem uma palavra escrita.
147
+ if self.estado == "ouvindo":
148
+ # Responde à VOZ: o núcleo cresce com o que ela está falando.
149
+ raio = RAIO_BASE * (1 + 0.35 * self.nivel)
150
+ pulso = 1.0
151
+ elif self.estado == "pensando":
152
+ # Respiração lenta: trabalho em andamento, sem pressa aparente.
153
+ raio = RAIO_BASE * (1 + 0.06 * math.sin(self._fase * 0.8))
154
+ pulso = 0.8 + 0.2 * math.sin(self._fase * 0.8)
155
+ elif self.estado == "falando":
156
+ raio = RAIO_BASE * (1 + 0.12 * math.sin(self._fase * 2.4))
157
+ pulso = 1.0
158
+ else:
159
+ raio = RAIO_BASE * (1 + 0.03 * math.sin(self._fase * 0.5))
160
+ pulso = 0.55
161
+
162
+ # O halo: anéis concêntricos cada vez mais fracos. É o que faz a orb
163
+ # parecer luz em vez de um círculo pintado — tkinter não tem gradiente,
164
+ # então o degradê é feito com camadas.
165
+ camadas = 7
166
+ for i in range(camadas, 0, -1):
167
+ r = raio * (1 + i * 0.28)
168
+ cor = self._misturar(fundo, "#05070a", i / camadas * 0.85)
169
+ self.tela.create_oval(centro - r, centro - r, centro + r, centro + r,
170
+ fill=cor, outline="")
171
+
172
+ self.tela.create_oval(centro - raio, centro - raio, centro + raio, centro + raio,
173
+ fill=self._misturar(vivo, fundo, 1 - pulso), outline="")
174
+ # O brilho de cima, deslocado: dá volume à esfera.
175
+ br = raio * 0.42
176
+ self.tela.create_oval(centro - br, centro - raio * 0.62,
177
+ centro + br * 0.5, centro - raio * 0.05,
178
+ fill=self._misturar("#ffffff", vivo, 0.62), outline="")
179
+
180
+ self.raiz.after(33, self._pintar)
181
+
182
+ @staticmethod
183
+ def _misturar(a: str, b: str, quanto: float) -> str:
184
+ """`a` misturado com `b`. tkinter não tem alpha por forma, então a
185
+ transparência é resolvida na cor."""
186
+ quanto = max(0.0, min(1.0, quanto))
187
+ ra, ga, ba = int(a[1:3], 16), int(a[3:5], 16), int(a[5:7], 16)
188
+ rb, gb, bb = int(b[1:3], 16), int(b[3:5], 16), int(b[5:7], 16)
189
+ r = round(ra + (rb - ra) * quanto)
190
+ g = round(ga + (gb - ga) * quanto)
191
+ bl = round(ba + (bb - ba) * quanto)
192
+ return f"#{r:02x}{g:02x}{bl:02x}"
193
+
194
+ # ── de fora ──────────────────────────────────────────────────────────
195
+ def mudar(self, estado: Optional[str] = None, nivel: Optional[float] = None) -> None:
196
+ """Chamado de OUTRAS threads. tkinter não é seguro fora da thread
197
+ principal, então o que chega vai para uma fila e é lido no desenho."""
198
+ self._fila.put((estado, nivel))
199
+
200
+ def fechar(self) -> None:
201
+ if not self._vivo:
202
+ return
203
+ self._vivo = False
204
+ if self._ao_fechar:
205
+ try:
206
+ self._ao_fechar()
207
+ except Exception:
208
+ pass
209
+ try:
210
+ self.raiz.quit()
211
+ self.raiz.destroy()
212
+ except Exception:
213
+ pass
214
+
215
+ def rodar(self) -> None:
216
+ self.raiz.mainloop()
@@ -1,177 +0,0 @@
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
- // `f.url &&` PRECISA valer sempre: sem o parêntese, `&&` liga mais forte
68
- // que `||` e `comercial: false` deixava passar item sem url (o download
69
- // falhava três vezes e a tool dizia "não consegui baixar") E, pior, faixa
70
- // NC dentro de peça comercial — o que a lista LIVRES existe para impedir.
71
- const itens = (j.results || []).filter((f) => f.url
72
- && (LIVRES.has(String(f.license || '').toLowerCase()) || !comercial));
73
- const alvoMs = tipo === 'efeito' ? 2500 : (dur ? dur * 1000 : 90000);
74
- const notas = itens.map((f) => {
75
- let n = 0;
76
- const d = Number(f.duration) || 0;
77
- if (tipo === 'efeito') {
78
- if (d > 15000) n -= 50; // não é efeito, é faixa
79
- else n += 20 - Math.abs(d - alvoMs) / 500;
80
- } else {
81
- if (d && d < alvoMs * 0.6) n -= 20; // acaba antes do vídeo
82
- n -= Math.abs(d - alvoMs) / 20000; // perto da duração pedida
83
- if (f.category === 'music') n += 5;
84
- }
85
- if (/cc0|pdm/.test(f.license)) n += 3; // sem crédito obrigatório
86
- if (/mp3/i.test(f.filetype || '') || /\.mp3(\?|$)/i.test(f.url)) n += 4; // toca em qualquer lugar
87
- // As palavras da consulta no título/tags contam.
88
- const alvo = `${f.title} ${(f.tags || []).map((t) => t.name).join(' ')} ${(f.genres || []).join(' ')}`.toLowerCase();
89
- for (const p of consulta.toLowerCase().split(/\s+/)) if (p.length > 2 && alvo.includes(p)) n += 3;
90
- return { f, n };
91
- }).sort((a, b) => b.n - a.n);
92
- return {
93
- ok: true,
94
- total: j.result_count || itens.length,
95
- itens: notas.map(({ f }) => ({
96
- id: f.id, titulo: f.title, autor: f.creator, licenca: f.license, licencaUrl: f.license_url,
97
- duracao: f.duration ? Math.round(f.duration / 1000) : null, url: f.url, pagina: f.foreign_landing_url,
98
- provedor: f.provider, tipoArquivo: f.filetype || null, generos: f.genres || [], credito: credito(f),
99
- })),
100
- };
101
- }
102
-
103
- /** Baixa a faixa para <projeto>/midia/ e manda ao estúdio. */
104
- async function trazer(item, { projectDir, studio }) {
105
- const bytes = await referencia.pegar(item.url, { timeout: 45000, binario: true });
106
- if (!bytes || bytes.length < 2000) return { ok: false, error: `não consegui baixar "${item.titulo}" de ${item.provedor}` };
107
- // A extensão pelo conteúdo, não pela URL: o Jamendo entrega mp3 numa URL
108
- // sem extensão ("?trackid=…&format=mp32").
109
- const cab = bytes.subarray(0, 12);
110
- const ext = cab.subarray(0, 3).toString() === 'ID3' || (cab[0] === 0xff && (cab[1] & 0xe0) === 0xe0) ? '.mp3'
111
- : cab.subarray(0, 4).toString() === 'RIFF' ? '.wav'
112
- : cab.subarray(0, 4).toString() === 'OggS' ? '.ogg'
113
- : cab.subarray(0, 4).toString() === 'fLaC' ? '.flac'
114
- : (item.tipoArquivo ? '.' + item.tipoArquivo.replace(/^\./, '') : '.mp3');
115
- const pasta = path.join(projectDir || process.cwd(), 'midia');
116
- fs.mkdirSync(pasta, { recursive: true });
117
- const nome = `${slug(item.titulo)}-${String(item.id || '').slice(0, 6)}${ext}`;
118
- const caminho = path.join(pasta, nome);
119
- fs.writeFileSync(caminho, bytes);
120
- // FLAC/OGG do Freesound: o navegador toca, mas o MP4 exportado quer AAC —
121
- // o estúdio cuida disso na exportação; aqui só entregamos o arquivo.
122
- const envio = await studio.midiaEnviar({ arquivo: caminho, nome });
123
- if (!envio || envio.ok === false) return { ok: false, error: (envio && envio.error) || 'o estúdio não aceitou o arquivo', arquivoLocal: caminho };
124
- return { ok: true, url: envio.url, arquivoLocal: path.relative(projectDir || process.cwd(), caminho), bytes: bytes.length };
125
- }
126
-
127
- /**
128
- * A tool: procura, escolhe a melhor, baixa e devolve a URL pronta — e as
129
- * alternativas, para o agente trocar sem pesquisar de novo.
130
- */
131
- async function buscarAudio(args, { projectDir, studio }) {
132
- const consulta = String(args.consulta || args.query || '').trim();
133
- 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").' };
134
- const tipo = /^(efeito|som|sfx|sound)/i.test(String(args.tipo || '')) ? 'efeito' : 'musica';
135
- const dur = Number(args.duracao) || 0;
136
- const busca = await procurar({ consulta, tipo, dur, comercial: args.comercial !== false });
137
- if (!busca.ok) return busca;
138
- if (!busca.itens.length) {
139
- return {
140
- ok: false,
141
- error: `Nenhuma faixa livre para "${consulta}".`,
142
- comoCorrigir: tipo === 'efeito'
143
- ? 'Use um som SINTETIZADO do estúdio, que não precisa de arquivo: som: "whoosh" | "impacto" | "click" | "pop" | "ding" | "subida" | "tique" | "brilho".'
144
- : '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"}.',
145
- };
146
- }
147
- // Baixa a melhor; se ela falhar, a próxima. Três tentativas.
148
- let trazido = null;
149
- let escolhida = null;
150
- for (const item of busca.itens.slice(0, 3)) {
151
- trazido = await trazer(item, { projectDir, studio });
152
- if (trazido.ok) { escolhida = item; break; }
153
- }
154
- if (!trazido || !trazido.ok) return { ok: false, error: (trazido && trazido.error) || 'não consegui baixar nenhuma faixa', alternativas: busca.itens.slice(0, 5) };
155
- const precisaCredito = !/cc0|pdm/.test(escolhida.licenca);
156
- return {
157
- ok: true,
158
- tipo,
159
- url: trazido.url,
160
- titulo: escolhida.titulo,
161
- autor: escolhida.autor,
162
- duracao: escolhida.duracao,
163
- licenca: NOME_LICENCA[escolhida.licenca] || escolhida.licenca,
164
- licencaUrl: escolhida.licencaUrl,
165
- credito: escolhida.credito,
166
- pagina: escolhida.pagina,
167
- arquivoLocal: trazido.arquivoLocal,
168
- comoUsar: tipo === 'efeito'
169
- ? `No elemento ou na cena: "som": {"arquivo": "${trazido.url}", "volume": 0.8}.`
170
- : `No documento: "trilha": {"tipo":"arquivo","src":"${trazido.url}","volume":0.55,"repetir":true}.`
171
- + (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.'),
172
- alternativas: busca.itens.filter((i) => i.id !== escolhida.id).slice(0, 4)
173
- .map((i) => ({ titulo: i.titulo, autor: i.autor, duracao: i.duracao, licenca: i.licenca, id: i.id })),
174
- };
175
- }
176
-
177
- module.exports = { buscarAudio, procurar, trazer, credito, LIVRES, NOME_LICENCA };
package/lib/chaves.js DELETED
@@ -1,149 +0,0 @@
1
- /**
2
- * chaves.js — a chave de API da própria pessoa.
3
- *
4
- * "Você vai usar com a API que já está ali, e pode configurar sua própria
5
- * API, por exemplo do Claude, do Codex, etc."
6
- *
7
- * ── ONDE ELAS MORAM ──────────────────────────────────────────────────────
8
- * Em `~/.primocode/chaves.json`, com permissão 600 — o mesmo lugar e o mesmo
9
- * cuidado da credencial da conta, e pela mesma razão: o `config.json` é o
10
- * arquivo que alguém abre para conferir um ajuste, cola num chamado de
11
- * suporte ou copia para outra máquina. Chave junto de preferência é chave que
12
- * vaza por hábito, não por ataque.
13
- *
14
- * ── COMO ELAS CHEGAM AO MOTOR ────────────────────────────────────────────
15
- * Como VARIÁVEL DE AMBIENTE do processo filho, que é o jeito que o Claude
16
- * Code e o Codex já leem chave. Não se escreve em arquivo de configuração
17
- * deles, não se mexe no login que a pessoa já fez: quem não configurar nada
18
- * aqui continua usando a conta em que entrou nas ferramentas, e quem
19
- * configurar passa a chave só naquela chamada.
20
- *
21
- * ── O QUE NUNCA APARECE ──────────────────────────────────────────────────
22
- * A chave inteira. Nem ao listar, nem ao salvar, nem numa mensagem de erro. O
23
- * que se mostra é o suficiente para a pessoa reconhecer qual é (o começo e o
24
- * fim), porque quem tem três chaves precisa saber qual está ali — e é isso
25
- * que evita ela colar a chave na tela para conferir.
26
- */
27
-
28
- 'use strict';
29
-
30
- const fs = require('fs');
31
- const os = require('os');
32
- const path = require('path');
33
-
34
- const ARQUIVO = () => path.join(os.homedir(), '.primocode', 'chaves.json');
35
-
36
- /**
37
- * Os provedores que o PrimoCode sabe usar, e a variável que cada um lê.
38
- *
39
- * O nome da variável é o do provedor, não um inventado aqui: é o que as
40
- * ferramentas já procuram, e inventar um significaria a chave certa no lugar
41
- * errado — configurada, aceita, e ignorada na hora de valer.
42
- */
43
- const PROVEDORES = {
44
- claude: {
45
- nome: 'Claude',
46
- variavel: 'ANTHROPIC_API_KEY',
47
- prefixo: 'sk-ant-',
48
- onde: 'https://console.anthropic.com/settings/keys',
49
- motor: 'claude-code',
50
- },
51
- codex: {
52
- nome: 'Codex',
53
- variavel: 'OPENAI_API_KEY',
54
- prefixo: 'sk-',
55
- onde: 'https://platform.openai.com/api-keys',
56
- motor: 'codex',
57
- },
58
- };
59
-
60
- function ler() {
61
- try { return JSON.parse(fs.readFileSync(ARQUIVO(), 'utf8')); }
62
- catch { return {}; }
63
- }
64
-
65
- function gravar(dados) {
66
- const caminho = ARQUIVO();
67
- fs.mkdirSync(path.dirname(caminho), { recursive: true });
68
- fs.writeFileSync(caminho, JSON.stringify(dados, null, 2), 'utf8');
69
- // 600 = só o dono lê. No Windows o modo é ignorado pelo sistema de
70
- // arquivos e a chamada não falha — lá quem protege é a pasta do perfil.
71
- try { fs.chmodSync(caminho, 0o600); } catch {}
72
- }
73
-
74
- /** O bastante para reconhecer, nunca o bastante para usar. */
75
- function disfarcar(chave) {
76
- const s = String(chave || '');
77
- if (s.length <= 12) return '•'.repeat(s.length);
78
- return `${s.slice(0, 7)}…${s.slice(-4)}`;
79
- }
80
-
81
- /**
82
- * Guarda uma chave. Recusa a que claramente não é do provedor pedido — o
83
- * erro mais comum é colar a do Claude no Codex, e sem esta conferência isso
84
- * só apareceria como "não autorizado" no meio de uma tarefa, longe da causa.
85
- */
86
- function guardar(provedor, chave) {
87
- const p = PROVEDORES[provedor];
88
- if (!p) return { ok: false, error: `não conheço "${provedor}". Use: ${Object.keys(PROVEDORES).join(', ')}` };
89
- const limpa = String(chave || '').trim();
90
- if (!limpa) return { ok: false, error: 'a chave veio vazia' };
91
- if (limpa.length < 20) return { ok: false, error: 'isso é curto demais para ser uma chave' };
92
- if (!limpa.startsWith(p.prefixo)) {
93
- return { ok: false,
94
- error: `uma chave do ${p.nome} começa com "${p.prefixo}", e essa não. Colou a do outro provedor?` };
95
- }
96
- // A do Claude também começa com "sk-", então o Codex precisa da recusa
97
- // explícita: sem ela, `sk-ant-…` passaria como chave da OpenAI.
98
- if (provedor === 'codex' && limpa.startsWith('sk-ant-')) {
99
- return { ok: false, error: `essa é uma chave do Claude. Use /chave claude para ela.` };
100
- }
101
- const tudo = ler();
102
- tudo[provedor] = limpa;
103
- gravar(tudo);
104
- return { ok: true, provedor, nome: p.nome, disfarce: disfarcar(limpa) };
105
- }
106
-
107
- function esquecer(provedor) {
108
- const tudo = ler();
109
- if (!(provedor in tudo)) return { ok: false, error: `não havia chave do ${provedor}` };
110
- delete tudo[provedor];
111
- gravar(tudo);
112
- return { ok: true };
113
- }
114
-
115
- /** O que está configurado, disfarçado. */
116
- function listar() {
117
- const tudo = ler();
118
- return Object.entries(PROVEDORES).map(([id, p]) => ({
119
- id, nome: p.nome, variavel: p.variavel, onde: p.onde,
120
- tem: typeof tudo[id] === 'string' && tudo[id].length > 0,
121
- disfarce: tudo[id] ? disfarcar(tudo[id]) : null,
122
- }));
123
- }
124
-
125
- /**
126
- * O ambiente para rodar o motor de um provedor: o de agora, mais a chave.
127
- *
128
- * Sem chave configurada devolve o ambiente INTACTO — nem a variável vazia,
129
- * que apagaria a chave que a pessoa já tem exportada no shell e faria a
130
- * ferramenta parar de funcionar por ter sido "configurada".
131
- */
132
- function ambienteDe(provedor, base = process.env) {
133
- const p = PROVEDORES[provedor];
134
- if (!p) return base;
135
- const chave = ler()[provedor];
136
- if (!chave) return base;
137
- return { ...base, [p.variavel]: chave };
138
- }
139
-
140
- /** Qual provedor um motor usa. */
141
- function provedorDoMotor(motor) {
142
- for (const [id, p] of Object.entries(PROVEDORES)) if (p.motor === motor) return id;
143
- return null;
144
- }
145
-
146
- module.exports = {
147
- PROVEDORES, guardar, esquecer, listar, ambienteDe, disfarcar,
148
- provedorDoMotor, ARQUIVO,
149
- };