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
package/studio/studio.log
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
[studio] 2026-09-11T18:38:32+00:00 — iniciando na porta 7777
|
|
2
|
-
|
|
3
|
-
██ PrimoCode Studio
|
|
4
|
-
────────────────────────────────────────────────────────
|
|
5
|
-
Estúdio http://localhost:7777
|
|
6
|
-
Manual da IA http://localhost:7777/api/manual
|
|
7
|
-
Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
|
|
8
|
-
────────────────────────────────────────────────────────
|
|
9
|
-
Ctrl+C para parar
|
|
10
|
-
|
|
11
|
-
GET /api/ferramentas
|
|
12
|
-
GET /api/ferramentas
|
|
13
|
-
GET /api/arquivos
|
|
14
|
-
GET /api/ferramentas
|
|
15
|
-
GET /api/arquivos
|
|
16
|
-
GET /api/ferramentas
|
|
17
|
-
GET /api/arquivos
|
|
18
|
-
POST /api/arquivos
|
|
19
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
20
|
-
GET /api/voz/status
|
|
21
|
-
GET /api/voz/status
|
|
22
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
23
|
-
GET /api/voz/status
|
|
24
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
25
|
-
GET /api/voz/status
|
|
26
|
-
[studio] 2026-09-11T18:40:34+00:00 — iniciando na porta 7777
|
|
27
|
-
|
|
28
|
-
██ PrimoCode Studio
|
|
29
|
-
────────────────────────────────────────────────────────
|
|
30
|
-
Estúdio http://localhost:7777
|
|
31
|
-
Manual da IA http://localhost:7777/api/manual
|
|
32
|
-
Ferramentas Prisma, Tela, Prosa, Grade, Corte, Traço
|
|
33
|
-
────────────────────────────────────────────────────────
|
|
34
|
-
Ctrl+C para parar
|
|
35
|
-
|
|
36
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
37
|
-
GET /api/voz/status
|
|
38
|
-
GET /api/voz/status
|
|
39
|
-
GET /api/voz/status
|
|
40
|
-
GET /api/voz/status
|
|
41
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
42
|
-
GET /api/voz/status
|
|
43
|
-
GET /api/voz/status
|
|
44
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
45
|
-
GET /api/voz/status
|
|
46
|
-
GET /api/voz/status
|
|
47
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
48
|
-
GET /api/voz/status
|
|
49
|
-
GET /api/voz/status
|
|
50
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
51
|
-
GET /api/voz/status
|
|
52
|
-
GET /api/voz/status
|
|
53
|
-
GET /api/arquivos/corte-teste-de-camera-fb823
|
|
54
|
-
GET /api/voz/status
|
|
55
|
-
GET /api/ferramentas
|
|
56
|
-
GET /api/versao
|
|
57
|
-
POST /api/sair
|
package/studio/voz.py
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
"""Narração de verdade, gerada aqui em Python.
|
|
2
|
-
|
|
3
|
-
── O PROBLEMA ────────────────────────────────────────────────────────────
|
|
4
|
-
A narração era feita no NAVEGADOR. Havia três caminhos lá: a voz neural do
|
|
5
|
-
Edge por WebSocket, o TTS do Google por este servidor, e o speechSynthesis
|
|
6
|
-
da própria máquina. O primeiro morreu — a Microsoft passou a responder 403
|
|
7
|
-
na síntese (a lista de vozes ainda responde; a síntese, não), e isso é uma
|
|
8
|
-
trava que eles impõem de propósito, não um bug para contornar. O terceiro é
|
|
9
|
-
o TTS do sistema: existe em todo computador e soa a computador. Resultado
|
|
10
|
-
prático: o usuário pedia vídeo institucional e ouvia voz de robô.
|
|
11
|
-
|
|
12
|
-
── A SAÍDA ───────────────────────────────────────────────────────────────
|
|
13
|
-
Trazer a síntese para cá, para o Python, onde ela não depende de navegador
|
|
14
|
-
nem de serviço de ninguém. O motor é o Piper (MIT, do projeto Rhasspy):
|
|
15
|
-
rede neural que roda na máquina, offline, sem chave, sem cota e sem limite.
|
|
16
|
-
Ele não vem no pacote — são ~90 MB entre programa e modelo — então é
|
|
17
|
-
baixado uma vez, sob demanda, e daí em diante a narração é instantânea e
|
|
18
|
-
funciona sem internet.
|
|
19
|
-
|
|
20
|
-
── AS VOZES ──────────────────────────────────────────────────────────────
|
|
21
|
-
As quatro vozes pt-BR do Piper foram sintetizadas e MEDIDAS: a frequência
|
|
22
|
-
fundamental de cada uma está na tabela abaixo, apurada por autocorrelação
|
|
23
|
-
sobre os quadros com tom. Não é adjetivo de catálogo, é medida. Por isso a
|
|
24
|
-
padrão é a Jeff, de 139 Hz — a mais grave, que é o que sustenta abertura de
|
|
25
|
-
vídeo. E por isso não há voz feminina listada: o Piper não tem nenhuma em
|
|
26
|
-
pt-BR, e inventar rótulo seria pior que admitir a falta.
|
|
27
|
-
|
|
28
|
-
── A CORRENTE ────────────────────────────────────────────────────────────
|
|
29
|
-
1. PIPER neural, offline, ilimitado, com escolha de voz. O bom.
|
|
30
|
-
2. VOICEBOX se o usuário tiver o app instalado — voz clonável.
|
|
31
|
-
3. GOOGLE uma voz só, robótica, mas responde na hora e sem instalar nada.
|
|
32
|
-
|
|
33
|
-
Cada um cai no seguinte quando falha. A narração nunca fica muda por falta
|
|
34
|
-
de motor: enquanto o Piper baixa, o Google cobre.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
import json
|
|
38
|
-
import os
|
|
39
|
-
import platform
|
|
40
|
-
import re
|
|
41
|
-
import shutil
|
|
42
|
-
import subprocess
|
|
43
|
-
import sys
|
|
44
|
-
import tarfile
|
|
45
|
-
import tempfile
|
|
46
|
-
import threading
|
|
47
|
-
import urllib.parse
|
|
48
|
-
import urllib.request
|
|
49
|
-
import wave
|
|
50
|
-
import zipfile
|
|
51
|
-
|
|
52
|
-
CASA = os.path.join(os.path.expanduser("~"), ".primocode", "vozes")
|
|
53
|
-
|
|
54
|
-
RELEASE = "https://github.com/rhasspy/piper/releases/download/2023.11.14-2/"
|
|
55
|
-
VOZES_URL = "https://huggingface.co/rhasspy/piper-voices/resolve/main/"
|
|
56
|
-
|
|
57
|
-
# O pacote do Piper para cada sistema. Os cinco nomes foram conferidos no ar.
|
|
58
|
-
PACOTES = {
|
|
59
|
-
("Linux", "x86_64"): "piper_linux_x86_64.tar.gz",
|
|
60
|
-
("Linux", "aarch64"): "piper_linux_aarch64.tar.gz",
|
|
61
|
-
("Linux", "arm64"): "piper_linux_aarch64.tar.gz",
|
|
62
|
-
("Darwin", "x86_64"): "piper_macos_x64.tar.gz",
|
|
63
|
-
("Darwin", "arm64"): "piper_macos_aarch64.tar.gz",
|
|
64
|
-
("Windows", "AMD64"): "piper_windows_amd64.zip",
|
|
65
|
-
("Windows", "x86_64"): "piper_windows_amd64.zip",
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
# hz: fundamental medida, em Hz. Quanto menor, mais grave.
|
|
69
|
-
CATALOGO = [
|
|
70
|
-
{"id": "jeff", "nome": "Jeff", "hz": 139, "grave": True, "padrao": True,
|
|
71
|
-
"tom": "grave, de locução", "caminho": "pt/pt_BR/jeff/medium/pt_BR-jeff-medium", "mb": 60},
|
|
72
|
-
{"id": "cadu", "nome": "Cadu", "hz": 140, "grave": True,
|
|
73
|
-
"tom": "grave, mais solta", "caminho": "pt/pt_BR/cadu/medium/pt_BR-cadu-medium", "mb": 60},
|
|
74
|
-
{"id": "faber", "nome": "Faber", "hz": 167,
|
|
75
|
-
"tom": "clara, de leitura", "caminho": "pt/pt_BR/faber/medium/pt_BR-faber-medium", "mb": 60},
|
|
76
|
-
{"id": "edresson", "nome": "Edresson", "hz": 151,
|
|
77
|
-
"tom": "média, mais leve de baixar", "caminho": "pt/pt_BR/edresson/low/pt_BR-edresson-low", "mb": 21},
|
|
78
|
-
{"id": "tugao", "nome": "Tugão", "hz": 0,
|
|
79
|
-
"tom": "português de Portugal", "caminho": "pt/pt_PT/tugão/medium/pt_PT-tugão-medium", "mb": 60},
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
POR_ID = {v["id"]: v for v in CATALOGO}
|
|
83
|
-
PADRAO = next(v["id"] for v in CATALOGO if v.get("padrao"))
|
|
84
|
-
|
|
85
|
-
# Estado do download, para a interface poder mostrar em vez de fingir que
|
|
86
|
-
# está tudo pronto. Um dicionário simples, protegido por tranca porque quem
|
|
87
|
-
# escreve é a thread do download e quem lê são as requisições.
|
|
88
|
-
_tranca = threading.Lock()
|
|
89
|
-
_baixando = {"ativo": False, "voz": None, "pct": 0, "etapa": "", "erro": None}
|
|
90
|
-
_thread = None
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _versao_pacote():
|
|
94
|
-
return PACOTES.get((platform.system(), platform.machine()))
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def pasta_piper():
|
|
98
|
-
return os.path.join(CASA, "piper")
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def binario():
|
|
102
|
-
"""O executável do Piper, se já foi baixado."""
|
|
103
|
-
nome = "piper.exe" if platform.system() == "Windows" else "piper"
|
|
104
|
-
p = os.path.join(pasta_piper(), nome)
|
|
105
|
-
return p if os.path.exists(p) else None
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def modelo(voz_id):
|
|
109
|
-
v = POR_ID.get(voz_id or PADRAO)
|
|
110
|
-
if not v:
|
|
111
|
-
return None
|
|
112
|
-
base = os.path.join(CASA, os.path.basename(v["caminho"]))
|
|
113
|
-
return base if os.path.exists(base + ".onnx") and os.path.exists(base + ".onnx.json") else None
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def instaladas():
|
|
117
|
-
return [v["id"] for v in CATALOGO if modelo(v["id"])]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# O binário EXISTIR não prova que ele RODA. O pacote macos_aarch64 do Piper
|
|
121
|
-
# 2023.11.14-2 veio com executável x86_64 dentro — num Mac Apple Silicon sem
|
|
122
|
-
# Rosetta, spawná-lo morre com "Bad CPU type" (errno 86). Sem esta checagem,
|
|
123
|
-
# `pronta()` dizia sim, o servidor esperava a voz que nunca viria, e cada
|
|
124
|
-
# pedido de narração travava a corrente inteira de motores por minutos.
|
|
125
|
-
# A prova roda UMA vez e o veredito fica em arquivo: reiniciar não re-testa,
|
|
126
|
-
# e reinstalar (apagar()) limpa o veredito junto.
|
|
127
|
-
_MARCA_INCOMPATIVEL = os.path.join(CASA, "binario-incompativel")
|
|
128
|
-
_roda_cache = None
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def marcar_incompativel(motivo):
|
|
132
|
-
global _roda_cache
|
|
133
|
-
_roda_cache = False
|
|
134
|
-
try:
|
|
135
|
-
os.makedirs(CASA, exist_ok=True)
|
|
136
|
-
with open(_MARCA_INCOMPATIVEL, "w") as fh:
|
|
137
|
-
fh.write(str(motivo))
|
|
138
|
-
except Exception:
|
|
139
|
-
pass
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def bloqueado():
|
|
143
|
-
"""Por que o Piper nunca vai funcionar nesta máquina — ou None."""
|
|
144
|
-
try:
|
|
145
|
-
if os.path.exists(_MARCA_INCOMPATIVEL):
|
|
146
|
-
with open(_MARCA_INCOMPATIVEL) as fh:
|
|
147
|
-
return fh.read().strip() or "binário incompatível com esta máquina"
|
|
148
|
-
except Exception:
|
|
149
|
-
pass
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _binario_roda():
|
|
154
|
-
global _roda_cache
|
|
155
|
-
if _roda_cache is not None:
|
|
156
|
-
return _roda_cache
|
|
157
|
-
if bloqueado():
|
|
158
|
-
_roda_cache = False
|
|
159
|
-
return False
|
|
160
|
-
exe = binario()
|
|
161
|
-
if not exe:
|
|
162
|
-
return None # ainda não baixou: sem veredito
|
|
163
|
-
try:
|
|
164
|
-
subprocess.run([exe, "--version"], stdout=subprocess.DEVNULL,
|
|
165
|
-
stderr=subprocess.DEVNULL, timeout=10)
|
|
166
|
-
_roda_cache = True
|
|
167
|
-
except OSError as e:
|
|
168
|
-
if getattr(e, "errno", None) == 86: # Bad CPU type (macOS)
|
|
169
|
-
marcar_incompativel("o pacote do Piper veio com binário de outra "
|
|
170
|
-
"arquitetura (x86_64 num Mac ARM sem Rosetta)")
|
|
171
|
-
else:
|
|
172
|
-
marcar_incompativel("o binário do Piper não executa: %s" % e)
|
|
173
|
-
except Exception:
|
|
174
|
-
_roda_cache = True # demorou mas executa: não é errno 86
|
|
175
|
-
return _roda_cache
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def pronta(voz_id=None):
|
|
179
|
-
return bool(binario() and modelo(voz_id or PADRAO) and _binario_roda())
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
# ------------------------------------------------------------------ baixar --
|
|
183
|
-
|
|
184
|
-
def _baixar(url, destino, fatia=(0, 100)):
|
|
185
|
-
"""Baixa mostrando progresso dentro da fatia que lhe cabe do total."""
|
|
186
|
-
ini, fim = fatia
|
|
187
|
-
with urllib.request.urlopen(url, timeout=60) as r:
|
|
188
|
-
total = int(r.headers.get("Content-Length") or 0)
|
|
189
|
-
feito = 0
|
|
190
|
-
tmp = destino + ".parcial"
|
|
191
|
-
with open(tmp, "wb") as fh:
|
|
192
|
-
while True:
|
|
193
|
-
p = r.read(262144)
|
|
194
|
-
if not p:
|
|
195
|
-
break
|
|
196
|
-
fh.write(p)
|
|
197
|
-
feito += len(p)
|
|
198
|
-
if total:
|
|
199
|
-
with _tranca:
|
|
200
|
-
_baixando["pct"] = int(ini + (fim - ini) * feito / total)
|
|
201
|
-
# Só vira o arquivo de verdade quando terminou inteiro: assim uma
|
|
202
|
-
# queda de rede não deixa um modelo pela metade que "existe".
|
|
203
|
-
os.replace(tmp, destino)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _instalar_piper():
|
|
207
|
-
pacote = _versao_pacote()
|
|
208
|
-
if not pacote:
|
|
209
|
-
raise RuntimeError("não há Piper para %s/%s" % (platform.system(), platform.machine()))
|
|
210
|
-
os.makedirs(CASA, exist_ok=True)
|
|
211
|
-
with _tranca:
|
|
212
|
-
_baixando["etapa"] = "baixando o motor de voz"
|
|
213
|
-
alvo = os.path.join(CASA, pacote)
|
|
214
|
-
_baixar(RELEASE + pacote, alvo, (0, 35))
|
|
215
|
-
|
|
216
|
-
with _tranca:
|
|
217
|
-
_baixando["etapa"] = "instalando o motor"
|
|
218
|
-
if pacote.endswith(".zip"):
|
|
219
|
-
with zipfile.ZipFile(alvo) as z:
|
|
220
|
-
z.extractall(CASA)
|
|
221
|
-
else:
|
|
222
|
-
with tarfile.open(alvo) as t:
|
|
223
|
-
t.extractall(CASA)
|
|
224
|
-
os.remove(alvo)
|
|
225
|
-
|
|
226
|
-
exe = binario()
|
|
227
|
-
if not exe:
|
|
228
|
-
raise RuntimeError("o pacote do Piper não trouxe o executável")
|
|
229
|
-
if platform.system() != "Windows":
|
|
230
|
-
os.chmod(exe, 0o755)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _instalar_voz(voz_id):
|
|
234
|
-
v = POR_ID[voz_id]
|
|
235
|
-
os.makedirs(CASA, exist_ok=True)
|
|
236
|
-
base = os.path.join(CASA, os.path.basename(v["caminho"]))
|
|
237
|
-
with _tranca:
|
|
238
|
-
_baixando["etapa"] = "baixando a voz %s" % v["nome"]
|
|
239
|
-
# O nome tem "ã" no caso do tugão: a URL precisa disso escapado.
|
|
240
|
-
url = VOZES_URL + urllib.parse.quote(v["caminho"])
|
|
241
|
-
_baixar(url + ".onnx.json", base + ".onnx.json", (35, 40))
|
|
242
|
-
_baixar(url + ".onnx", base + ".onnx", (40, 100))
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def _rotina(voz_id):
|
|
246
|
-
global _thread
|
|
247
|
-
try:
|
|
248
|
-
if not binario():
|
|
249
|
-
_instalar_piper()
|
|
250
|
-
if not modelo(voz_id):
|
|
251
|
-
_instalar_voz(voz_id)
|
|
252
|
-
with _tranca:
|
|
253
|
-
_baixando.update(ativo=False, pct=100, etapa="pronta", erro=None)
|
|
254
|
-
except Exception as e:
|
|
255
|
-
sys.stderr.write("[voz] instalação falhou: %s\n" % e)
|
|
256
|
-
with _tranca:
|
|
257
|
-
_baixando.update(ativo=False, etapa="", erro=str(e))
|
|
258
|
-
finally:
|
|
259
|
-
_thread = None
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def instalar(voz_id=None, esperar=0):
|
|
263
|
-
"""Começa (ou acompanha) a instalação. Não bloqueia, por padrão.
|
|
264
|
-
|
|
265
|
-
Quem chama é a interface e o próprio servidor quando vê um documento com
|
|
266
|
-
narração: preparar a voz enquanto o agente ainda escreve o roteiro é o
|
|
267
|
-
que faz ela estar pronta na hora de tocar."""
|
|
268
|
-
global _thread
|
|
269
|
-
voz_id = voz_id if voz_id in POR_ID else PADRAO
|
|
270
|
-
# Baixar de novo não conserta binário de arquitetura errada: viria o
|
|
271
|
-
# mesmo pacote. Sem esta guarda, cada pedido de narração re-disparava
|
|
272
|
-
# os ~90 MB de download para nada.
|
|
273
|
-
if bloqueado():
|
|
274
|
-
return estado()
|
|
275
|
-
with _tranca:
|
|
276
|
-
if _thread is None and not pronta(voz_id):
|
|
277
|
-
_baixando.update(ativo=True, voz=voz_id, pct=0, etapa="começando", erro=None)
|
|
278
|
-
_thread = threading.Thread(target=_rotina, args=(voz_id,), daemon=True)
|
|
279
|
-
_thread.start()
|
|
280
|
-
t = _thread
|
|
281
|
-
if t and esperar:
|
|
282
|
-
t.join(esperar)
|
|
283
|
-
return estado()
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def estado():
|
|
287
|
-
with _tranca:
|
|
288
|
-
b = dict(_baixando)
|
|
289
|
-
return {
|
|
290
|
-
"motor": "piper",
|
|
291
|
-
"pronta": pronta(),
|
|
292
|
-
"instaladas": instaladas(),
|
|
293
|
-
# "suportado" responde à pergunta de verdade: o Piper VAI funcionar
|
|
294
|
-
# aqui? Pacote inexistente para a plataforma e binário que não roda
|
|
295
|
-
# são o mesmo "não" — e `bloqueado` explica o porquê.
|
|
296
|
-
"suportado": bool(_versao_pacote()) and not bloqueado(),
|
|
297
|
-
"bloqueado": bloqueado(),
|
|
298
|
-
"baixando": b,
|
|
299
|
-
"vozes": [{
|
|
300
|
-
"id": v["id"], "nome": v["nome"], "tom": v["tom"], "hz": v["hz"], "mb": v["mb"],
|
|
301
|
-
"instalada": bool(modelo(v["id"])),
|
|
302
|
-
"grave": bool(v.get("grave")),
|
|
303
|
-
"padrao": bool(v.get("padrao")),
|
|
304
|
-
} for v in CATALOGO],
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
# --------------------------------------------------------------- sintetizar --
|
|
309
|
-
|
|
310
|
-
def _mais_grave(bruto, razao):
|
|
311
|
-
"""Abaixa o tom reescrevendo a taxa declarada do WAV.
|
|
312
|
-
|
|
313
|
-
Não há reamostragem: os mesmos quadros, lidos mais devagar, soam mais
|
|
314
|
-
graves na mesma proporção. Como isso também alonga o áudio, a síntese já
|
|
315
|
-
foi feita acelerada pelo mesmo fator, e a duração volta ao lugar.
|
|
316
|
-
Medido: a voz Jeff sai de 139 Hz para 125 Hz com razão 0.9."""
|
|
317
|
-
with wave.open(bruto, "rb") as w:
|
|
318
|
-
p = w.getparams()
|
|
319
|
-
quadros = w.readframes(p.nframes)
|
|
320
|
-
saida = bruto + ".grave.wav"
|
|
321
|
-
with wave.open(saida, "wb") as s:
|
|
322
|
-
s.setnchannels(p.nchannels)
|
|
323
|
-
s.setsampwidth(p.sampwidth)
|
|
324
|
-
s.setframerate(max(8000, int(p.framerate * razao)))
|
|
325
|
-
s.writeframes(quadros)
|
|
326
|
-
return saida
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def sintetizar(texto, voz_id=None, grave=False, velocidade=1.0, timeout=120):
|
|
330
|
-
"""O texto vira WAV. None quando o Piper não está pronto — aí quem chamou
|
|
331
|
-
cai para o próximo motor da corrente."""
|
|
332
|
-
exe = binario()
|
|
333
|
-
voz_id = voz_id if voz_id in POR_ID else PADRAO
|
|
334
|
-
mod = modelo(voz_id)
|
|
335
|
-
if not exe or not mod or not _binario_roda():
|
|
336
|
-
return None
|
|
337
|
-
|
|
338
|
-
# Sem isto, um texto com quebra de linha vira várias falas e o Piper
|
|
339
|
-
# devolve só a última.
|
|
340
|
-
limpo = re.sub(r"\s+", " ", str(texto or "")).strip()
|
|
341
|
-
if not limpo:
|
|
342
|
-
return None
|
|
343
|
-
|
|
344
|
-
razao = 0.9 if grave else 1.0
|
|
345
|
-
# length_scale > 1 é mais devagar. A razão do grave entra aqui para a
|
|
346
|
-
# duração não crescer quando a taxa for reescrita lá embaixo.
|
|
347
|
-
escala = (1.0 / max(0.5, min(2.0, velocidade))) * razao
|
|
348
|
-
|
|
349
|
-
pasta = tempfile.mkdtemp(prefix="primo-voz-")
|
|
350
|
-
try:
|
|
351
|
-
saida = os.path.join(pasta, "voz.wav")
|
|
352
|
-
try:
|
|
353
|
-
r = subprocess.run(
|
|
354
|
-
[exe, "--model", mod + ".onnx", "--config", mod + ".onnx.json",
|
|
355
|
-
"--length_scale", "%.3f" % escala, "--sentence_silence", "0.25",
|
|
356
|
-
"--output_file", saida],
|
|
357
|
-
input=limpo.encode("utf-8"),
|
|
358
|
-
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=timeout,
|
|
359
|
-
)
|
|
360
|
-
except OSError as e:
|
|
361
|
-
# errno 86 = binário de outra arquitetura (macOS). Marca de vez:
|
|
362
|
-
# a corrente cai para o próximo motor e ninguém trava de novo.
|
|
363
|
-
if getattr(e, "errno", None) == 86:
|
|
364
|
-
marcar_incompativel("o pacote do Piper veio com binário de "
|
|
365
|
-
"outra arquitetura (x86_64 num Mac ARM sem Rosetta)")
|
|
366
|
-
return None
|
|
367
|
-
raise
|
|
368
|
-
if r.returncode != 0 or not os.path.exists(saida):
|
|
369
|
-
raise RuntimeError((r.stderr or b"").decode("utf-8", "replace")[-300:] or "piper falhou")
|
|
370
|
-
if grave:
|
|
371
|
-
saida = _mais_grave(saida, razao)
|
|
372
|
-
with open(saida, "rb") as fh:
|
|
373
|
-
return fh.read()
|
|
374
|
-
finally:
|
|
375
|
-
shutil.rmtree(pasta, ignore_errors=True)
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def apagar():
|
|
379
|
-
"""Some com tudo que foi baixado. Existe para o usuário poder desfazer:
|
|
380
|
-
são 90 MB na máquina dele, e ele tem direito de recuperá-los.
|
|
381
|
-
O veredito de incompatibilidade vai junto (mora dentro de CASA): quem
|
|
382
|
-
instalar de novo — num Mac com Rosetta, num pacote corrigido — merece
|
|
383
|
-
uma prova nova, não a sentença antiga."""
|
|
384
|
-
global _roda_cache
|
|
385
|
-
_roda_cache = None
|
|
386
|
-
if os.path.isdir(CASA):
|
|
387
|
-
shutil.rmtree(CASA, ignore_errors=True)
|
|
388
|
-
return {"ok": True}
|
package/studio/web/analise.html
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="pt-BR">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>PrimoCode — ouvindo o vídeo</title>
|
|
7
|
-
<link rel="stylesheet" href="/core.css">
|
|
8
|
-
<link rel="stylesheet" href="/fontes.css">
|
|
9
|
-
<style>
|
|
10
|
-
/* Uma aba que abre sozinha, trabalha e some. Ela não pede nada da pessoa —
|
|
11
|
-
só conta o que está fazendo, para o segundo de espera não parecer travado. */
|
|
12
|
-
body {
|
|
13
|
-
margin: 0; min-height: 100vh;
|
|
14
|
-
display: flex; align-items: center; justify-content: center;
|
|
15
|
-
background: var(--c-fundo, #0b0e14); color: var(--c-texto, #e7ecf3);
|
|
16
|
-
font-family: var(--f-corpo, system-ui, sans-serif);
|
|
17
|
-
}
|
|
18
|
-
.caixa { width: min(560px, 92vw); padding: 34px 32px; }
|
|
19
|
-
h1 { font-size: 21px; font-weight: 640; margin: 0 0 4px; letter-spacing: -.01em; }
|
|
20
|
-
#qual {
|
|
21
|
-
display: block; font-size: 13px; opacity: .62;
|
|
22
|
-
font-family: var(--f-mono, ui-monospace, monospace);
|
|
23
|
-
margin-bottom: 22px; word-break: break-all;
|
|
24
|
-
}
|
|
25
|
-
.trilho { height: 4px; border-radius: 4px; background: rgba(255,255,255,.1); overflow: hidden; }
|
|
26
|
-
#barra {
|
|
27
|
-
height: 100%; width: 0;
|
|
28
|
-
background: linear-gradient(90deg, #2f6df6, #59a0ff);
|
|
29
|
-
transition: width .45s cubic-bezier(.22,1,.36,1);
|
|
30
|
-
}
|
|
31
|
-
[data-estado="erro"] #barra { background: #d9584f; }
|
|
32
|
-
[data-estado="pronto"] #barra { background: linear-gradient(90deg, #1f9d63, #35c584); }
|
|
33
|
-
#painel { margin-top: 20px; max-height: 40vh; overflow: auto; }
|
|
34
|
-
#painel p {
|
|
35
|
-
margin: 0 0 7px; font-size: 14px; line-height: 1.5; opacity: .88;
|
|
36
|
-
font-family: var(--f-mono, ui-monospace, monospace);
|
|
37
|
-
}
|
|
38
|
-
#painel p:last-child { opacity: 1; }
|
|
39
|
-
</style>
|
|
40
|
-
</head>
|
|
41
|
-
<body>
|
|
42
|
-
|
|
43
|
-
<div class="caixa">
|
|
44
|
-
<h1>Ouvindo o vídeo para achar as pausas</h1>
|
|
45
|
-
<code id="qual"></code>
|
|
46
|
-
<div class="trilho"><div id="barra"></div></div>
|
|
47
|
-
<div id="painel"></div>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<script src="/js/analise.js"></script>
|
|
51
|
-
</body>
|
|
52
|
-
</html>
|