primocode 8.42.2 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +42 -195
- package/lib/catalogo.js +100 -61
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +576 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +644 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +49 -6
- package/lib/marca.js +4 -1
- package/lib/markdown.js +376 -0
- package/lib/marketplace.js +189 -0
- package/lib/pasta.js +15 -4
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +106 -187
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +199 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +383 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- 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()
|
package/lib/audio-livre.js
DELETED
|
@@ -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
|
-
};
|