primocode 10.14.1 → 10.15.1
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/LICENSES/robo.txt +19 -0
- package/README.md +6 -2
- package/bin/primocode.js +54 -3
- package/lib/act.js +1 -0
- package/lib/conta.js +1 -0
- package/lib/estudio/exportar.js +1 -0
- package/lib/mcp.js +1 -0
- package/lib/preparar.js +2 -0
- package/lib/tools.js +1 -0
- package/lib/voz-ponte.js +15 -0
- package/lib/voz.js +99 -5
- package/package.json +3 -2
- package/voz/primo_voz/__main__.py +429 -57
- package/voz/primo_voz/app.py +350 -0
- package/voz/primo_voz/appmac.py +124 -0
- package/voz/primo_voz/assets/mascote.svg +1 -0
- package/voz/primo_voz/conversa.py +236 -0
- package/voz/primo_voz/escuta.py +56 -4
- package/voz/primo_voz/fala.py +87 -0
- package/voz/primo_voz/hud.html +584 -0
- package/voz/primo_voz/hud.py +275 -0
- package/voz/primo_voz/identidade.py +88 -0
- package/voz/primo_voz/luz.py +137 -0
- package/voz/primo_voz/painel.py +459 -0
- package/voz/primo_voz/painel_tui.py +26 -0
- package/voz/primo_voz/sons.py +96 -0
- package/voz/primo_voz/tela.py +319 -0
- package/voz/primo_voz/transcrever.py +13 -2
- package/voz/primo_voz/vendor/RobotExpressive.glb +0 -0
- package/voz/primo_voz/vendor/primo-icone.png +0 -0
- package/voz/primo_voz/vendor/spline-runtime.min.js +8219 -0
- package/voz/primo_voz/vendor/three-robo.min.js +3824 -0
- package/voz/primo_voz/vendor/three.LICENSE.txt +21 -0
- package/voz/primo_voz/vivo.py +34 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""tela.py — os dados REAIS da tela do Primo (calendário, clima, música, máquina) e as regras pequenas dela.
|
|
2
|
+
|
|
3
|
+
Nada aqui desenha. hud.py chama estas funções numa thread e empurra o resultado para hud.html;
|
|
4
|
+
a lógica que não depende de tela (texto de "em X min", troca de robô, quem fala nas barras) se testa sem microfone.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
from typing import Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from primo_voz import identidade
|
|
19
|
+
from primo_voz import painel as pn
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ── regras puras ─────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
def em_x_min(inicio: float, agora: float) -> str:
|
|
25
|
+
"""'em 25 min', 'em 1 h 10 min', 'agora' (começou há pouco), 'amanhã' passa da meia-noite."""
|
|
26
|
+
d = int((inicio - agora) // 60)
|
|
27
|
+
if d < -5:
|
|
28
|
+
return "já começou"
|
|
29
|
+
if d <= 0:
|
|
30
|
+
return "agora"
|
|
31
|
+
if d < 60:
|
|
32
|
+
return f"em {d} min"
|
|
33
|
+
h, m = divmod(d, 60)
|
|
34
|
+
if h >= 24:
|
|
35
|
+
return "amanhã"
|
|
36
|
+
return f"em {h} h" + (f" {m} min" if m else "")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def barras(estado: str, nivel: float) -> Dict[str, object]:
|
|
40
|
+
"""Quem fala e quão alto: a PESSOA (ciano) em 'ouvindo' e em espera (o mic escuta); o PRIMO (verde) só em 'falando'. O resto é silêncio."""
|
|
41
|
+
n = max(0.0, min(1.0, float(nivel or 0)))
|
|
42
|
+
if estado in ("ouvindo", "dormindo"):
|
|
43
|
+
return {"quem": "pessoa", "nivel": n}
|
|
44
|
+
if estado == "falando":
|
|
45
|
+
return {"quem": "primo", "nivel": n}
|
|
46
|
+
return {"quem": None, "nivel": 0.0}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def nivel_do_mic(rms: float) -> float:
|
|
50
|
+
return max(0.0, min(1.0, rms / 4000.0))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def envelope(arquivo, janela: float = 0.05) -> List[float]:
|
|
54
|
+
"""Volume real da fala do Primo (0..1 a cada 50 ms): converte o mp3 em wav com afconvert e mede. Sem Mac = []."""
|
|
55
|
+
if sys.platform != "darwin" or not shutil.which("afconvert"):
|
|
56
|
+
return []
|
|
57
|
+
import array
|
|
58
|
+
import wave
|
|
59
|
+
tmp = tempfile.mktemp(suffix=".wav")
|
|
60
|
+
try:
|
|
61
|
+
subprocess.run(["afconvert", "-f", "WAVE", "-d", "LEI16@16000", "-c", "1", str(arquivo), tmp],
|
|
62
|
+
capture_output=True, timeout=10, check=True)
|
|
63
|
+
with wave.open(tmp) as w:
|
|
64
|
+
dados = array.array("h", w.readframes(w.getnframes()))
|
|
65
|
+
taxa = w.getframerate()
|
|
66
|
+
passo = max(1, int(taxa * janela))
|
|
67
|
+
env = []
|
|
68
|
+
for i in range(0, len(dados), passo):
|
|
69
|
+
b = dados[i:i + passo]
|
|
70
|
+
env.append((sum(x * x for x in b) / len(b)) ** 0.5 if b else 0.0)
|
|
71
|
+
pico = max(env) if env else 0
|
|
72
|
+
return [round(min(1.0, e / pico * 1.1), 3) for e in env] if pico else []
|
|
73
|
+
except Exception:
|
|
74
|
+
return []
|
|
75
|
+
finally:
|
|
76
|
+
try:
|
|
77
|
+
os.unlink(tmp)
|
|
78
|
+
except OSError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def hoje_lista(eventos: List[dict], agora: float) -> List[dict]:
|
|
83
|
+
"""Os eventos de HOJE que ainda importam (não acabaram), o primeiro marcado como próximo, com 'em X min'."""
|
|
84
|
+
t = time.localtime(agora)
|
|
85
|
+
out = []
|
|
86
|
+
for e in eventos:
|
|
87
|
+
if time.localtime(e["inicio"]).tm_mday != t.tm_mday or e["fim"] <= agora and not e.get("dia_todo"):
|
|
88
|
+
continue
|
|
89
|
+
item = {"titulo": e["titulo"], "hora": "dia todo" if e.get("dia_todo") else time.strftime("%H:%M", time.localtime(e["inicio"])),
|
|
90
|
+
"em": "" if e.get("dia_todo") else em_x_min(e["inicio"], agora), "prox": False}
|
|
91
|
+
out.append(item)
|
|
92
|
+
for item in out:
|
|
93
|
+
if item["hora"] != "dia todo":
|
|
94
|
+
item["prox"] = True
|
|
95
|
+
break
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── calendário (EventKit; pede permissão uma vez) ────────────────────────
|
|
100
|
+
|
|
101
|
+
_ek: dict = {}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _store():
|
|
105
|
+
if "s" not in _ek:
|
|
106
|
+
from EventKit import EKEventStore
|
|
107
|
+
_ek["s"] = EKEventStore.alloc().init()
|
|
108
|
+
return _ek["s"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def calendario_permissao() -> str:
|
|
112
|
+
"""ok | pendente | negada | sem (sem EventKit/sem Mac)."""
|
|
113
|
+
if sys.platform != "darwin":
|
|
114
|
+
return "sem"
|
|
115
|
+
try:
|
|
116
|
+
from EventKit import EKEventStore
|
|
117
|
+
except ImportError:
|
|
118
|
+
return "sem"
|
|
119
|
+
st = int(EKEventStore.authorizationStatusForEntityType_(0))
|
|
120
|
+
# 4 = só escrita (um pedido antigo concedeu menos do que o Primo precisa): o macOS não deixa "subir" por código,
|
|
121
|
+
# então tratamos como pendente e o pedido zera a decisão do próprio app antes de pedir o acesso completo.
|
|
122
|
+
return {3: "ok", 0: "pendente", 4: "pendente", 1: "negada", 2: "negada"}.get(st, "negada")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def pedir_calendario() -> None:
|
|
126
|
+
"""Dispara o pedido do macOS (uma vez). Não bloqueia. O resultado vai para o log (stderr)."""
|
|
127
|
+
if time.time() - _ek.get("pedindo", 0) < 120: # o aviso do macOS já está na tela esperando resposta
|
|
128
|
+
return
|
|
129
|
+
_ek["pedindo"] = time.time()
|
|
130
|
+
|
|
131
|
+
def fim(ok, err):
|
|
132
|
+
_ek["pedindo"] = 0
|
|
133
|
+
sys.stderr.write(f"calendario: pedido respondido ok={bool(ok)} erro={err} status={calendario_permissao()}\n")
|
|
134
|
+
sys.stderr.flush()
|
|
135
|
+
try:
|
|
136
|
+
from EventKit import EKEventStore
|
|
137
|
+
if int(EKEventStore.authorizationStatusForEntityType_(0)) == 4:
|
|
138
|
+
r = subprocess.run(["tccutil", "reset", "Calendar", "ai.primocode.primo"], capture_output=True, text=True)
|
|
139
|
+
sys.stderr.write(f"calendario: acesso só de escrita; zerando a decisão do Primo ({r.returncode})\n")
|
|
140
|
+
_ek.pop("s", None)
|
|
141
|
+
s = _store()
|
|
142
|
+
sys.stderr.write(f"calendario: pedindo acesso (status antes={calendario_permissao()}, bundle={_bundle()})\n")
|
|
143
|
+
sys.stderr.flush()
|
|
144
|
+
if hasattr(s, "requestFullAccessToEventsWithCompletion_"):
|
|
145
|
+
s.requestFullAccessToEventsWithCompletion_(fim)
|
|
146
|
+
else:
|
|
147
|
+
s.requestAccessToEntityType_completion_(0, fim)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
sys.stderr.write(f"calendario: falhou ao pedir: {e}\n")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _bundle() -> str:
|
|
153
|
+
try:
|
|
154
|
+
from Foundation import NSBundle
|
|
155
|
+
return str(NSBundle.mainBundle().bundleIdentifier())
|
|
156
|
+
except Exception:
|
|
157
|
+
return "?"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def eventos_do_mes(agora: Optional[float] = None) -> dict:
|
|
161
|
+
"""{'permissao','eventos':[{titulo,inicio,fim,dia_todo,dia}]} do mês inteiro (hoje e dias com ponto)."""
|
|
162
|
+
perm = calendario_permissao()
|
|
163
|
+
if perm == "pendente":
|
|
164
|
+
pedir_calendario()
|
|
165
|
+
if perm != "ok":
|
|
166
|
+
return {"permissao": perm, "eventos": []}
|
|
167
|
+
from Foundation import NSDate
|
|
168
|
+
agora = agora or time.time()
|
|
169
|
+
t = time.localtime(agora)
|
|
170
|
+
ini = time.mktime((t.tm_year, t.tm_mon, 1, 0, 0, 0, 0, 0, -1))
|
|
171
|
+
fim = time.mktime((t.tm_year + (t.tm_mon == 12), t.tm_mon % 12 + 1, 1, 0, 0, 0, 0, 0, -1))
|
|
172
|
+
s = _store()
|
|
173
|
+
pred = s.predicateForEventsWithStartDate_endDate_calendars_(NSDate.dateWithTimeIntervalSince1970_(ini),
|
|
174
|
+
NSDate.dateWithTimeIntervalSince1970_(fim), None)
|
|
175
|
+
out = []
|
|
176
|
+
for e in sorted(s.eventsMatchingPredicate_(pred) or [], key=lambda e: e.startDate().timeIntervalSince1970()):
|
|
177
|
+
a, b = e.startDate().timeIntervalSince1970(), e.endDate().timeIntervalSince1970()
|
|
178
|
+
out.append({"titulo": str(e.title() or "(sem título)"), "inicio": a, "fim": b, "dia_todo": bool(e.isAllDay()),
|
|
179
|
+
"dia": time.localtime(a).tm_mday})
|
|
180
|
+
return {"permissao": perm, "eventos": out}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── clima (open-meteo, cidade por IP, sem chave) ─────────────────────────
|
|
184
|
+
|
|
185
|
+
_COD = [((0,), "Céu limpo", "☀︎"), ((1, 2), "Poucas nuvens", "⛅︎"), ((3,), "Nublado", "☁︎"), ((45, 48), "Neblina", "≋"),
|
|
186
|
+
((51, 53, 55, 56, 57), "Garoa", "☂︎"), ((61, 63, 65, 66, 67, 80, 81, 82), "Chuva", "☂︎"),
|
|
187
|
+
((71, 73, 75, 77, 85, 86), "Neve", "❄︎"), ((95, 96, 99), "Tempestade", "⚡︎")]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def condicao(cod: int):
|
|
191
|
+
for cods, txt, ico in _COD:
|
|
192
|
+
if cod in cods:
|
|
193
|
+
return txt, ico
|
|
194
|
+
return "—", "·"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def clima_widget(cache: dict) -> dict:
|
|
198
|
+
"""cidade, temperatura agora, condição, máx/mín e os próximos 4 dias. Cache de 20 min; offline = o último."""
|
|
199
|
+
if time.time() - cache.get("tw", 0) < 1200 and "w" in cache:
|
|
200
|
+
return cache["w"]
|
|
201
|
+
cache["tw"] = time.time()
|
|
202
|
+
v = cache.get("w", {})
|
|
203
|
+
try:
|
|
204
|
+
g = pn._json_url("https://ipapi.co/json/")
|
|
205
|
+
lat, lon = float(g["latitude"]), float(g["longitude"])
|
|
206
|
+
f = pn._json_url(f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
|
|
207
|
+
"¤t=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min"
|
|
208
|
+
"&timezone=auto&forecast_days=5")
|
|
209
|
+
d, c = f["daily"], f["current"]
|
|
210
|
+
txt, ico = condicao(int(c["weather_code"]))
|
|
211
|
+
v = {"cidade": g.get("city", ""), "temp": round(c["temperature_2m"]), "cond": txt, "icone": ico,
|
|
212
|
+
"max": round(d["temperature_2m_max"][0]), "min": round(d["temperature_2m_min"][0]),
|
|
213
|
+
"dias": [{"data": d["time"][i], "max": round(d["temperature_2m_max"][i]),
|
|
214
|
+
"icone": condicao(int(d["weather_code"][i]))[1]} for i in range(1, len(d["time"]))]}
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
cache["w"] = v
|
|
218
|
+
return v
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ── música (Spotify ou Music) ────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
_capa: dict = {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _capa_music() -> str:
|
|
227
|
+
"""O Music não dá URL: grava a arte da faixa num arquivo temporário e devolve como data URI."""
|
|
228
|
+
tmp = os.path.join(tempfile.gettempdir(), "primo-capa.dat")
|
|
229
|
+
r = pn._osa('tell application "Music" to tell artwork 1 of current track to set d to raw data\n'
|
|
230
|
+
f'set f to open for access POSIX file "{tmp}" with write permission\nset eof f to 0\nwrite d to f\nclose access f', 4)
|
|
231
|
+
try:
|
|
232
|
+
b = open(tmp, "rb").read()
|
|
233
|
+
os.unlink(tmp)
|
|
234
|
+
except OSError:
|
|
235
|
+
return ""
|
|
236
|
+
if not b:
|
|
237
|
+
return ""
|
|
238
|
+
tipo = "image/png" if b[:4] == b"\x89PNG" else "image/jpeg"
|
|
239
|
+
return f"data:{tipo};base64," + base64.b64encode(b).decode()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def musica() -> dict:
|
|
243
|
+
"""{'app','estado','faixa','artista','pos','dur','capa'}; {} se nada está aberto. A capa só é buscada quando a faixa muda."""
|
|
244
|
+
if sys.platform != "darwin":
|
|
245
|
+
return {}
|
|
246
|
+
for app in ("Spotify", "Music"):
|
|
247
|
+
div = 1000.0 if app == "Spotify" else 1.0
|
|
248
|
+
art = ' & "|" & (artwork url of current track)' if app == "Spotify" else ""
|
|
249
|
+
r = pn._osa(f'if application "{app}" is running then tell application "{app}" to return (player state as string) & "|" & '
|
|
250
|
+
f'(player position as string) & "|" & (duration of current track as string) & "|" & name of current track & "|" '
|
|
251
|
+
f'& artist of current track{art}', 2.5)
|
|
252
|
+
p = r.split("|")
|
|
253
|
+
if len(p) < 5:
|
|
254
|
+
continue
|
|
255
|
+
try:
|
|
256
|
+
pos, dur = float(p[1].replace(",", ".")), float(p[2].replace(",", ".")) / div
|
|
257
|
+
except ValueError:
|
|
258
|
+
continue
|
|
259
|
+
chave = (app, p[3], p[4])
|
|
260
|
+
if _capa.get("k") != chave:
|
|
261
|
+
_capa["k"] = chave
|
|
262
|
+
_capa["v"] = p[5] if app == "Spotify" and len(p) > 5 else (_capa_music() if app == "Music" else "")
|
|
263
|
+
return {"app": app, "estado": p[0], "faixa": p[3], "artista": p[4], "pos": pos, "dur": dur, "capa": _capa.get("v", "")}
|
|
264
|
+
return {}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def controle(acao: str) -> None:
|
|
268
|
+
pn.controle_midia(acao)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ── a máquina ────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
def memoria() -> dict:
|
|
274
|
+
"""{'usada_gb','total_gb','pct'} (usada = ativa + fixa + comprimida, como o painel antigo)."""
|
|
275
|
+
try:
|
|
276
|
+
total = int(pn._rodar(["sysctl", "-n", "hw.memsize"], 2.0).strip())
|
|
277
|
+
s = pn._rodar(["vm_stat"], 2.0)
|
|
278
|
+
tam = int(re.search(r"page size of (\d+)", s).group(1))
|
|
279
|
+
pag = {k: int(v) for k, v in re.findall(r"Pages ([a-z ]+):\s+(\d+)", s)}
|
|
280
|
+
usada = (pag.get("active", 0) + pag.get("wired down", 0) + pag.get("occupied by compressor", 0)) * tam
|
|
281
|
+
return {"usada_gb": round(usada / 1e9, 1), "total_gb": round(total / 1e9), "pct": round(100 * usada / total)}
|
|
282
|
+
except Exception:
|
|
283
|
+
return {}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def disco() -> dict:
|
|
287
|
+
try:
|
|
288
|
+
u = shutil.disk_usage("/")
|
|
289
|
+
return {"pct": round(100 * u.used / u.total), "livre_gb": round(u.free / 1e9)}
|
|
290
|
+
except Exception:
|
|
291
|
+
return {}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def bateria() -> dict:
|
|
295
|
+
s = pn._rodar(["pmset", "-g", "batt"], 2.0)
|
|
296
|
+
m = re.search(r"(\d+)%", s)
|
|
297
|
+
if not m:
|
|
298
|
+
return {}
|
|
299
|
+
return {"pct": int(m.group(1)), "tomada": "AC Power" in s}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def maquina(am: "pn.Amostrador") -> dict:
|
|
303
|
+
cores = [round(x, 3) for x in am.cores()]
|
|
304
|
+
r = {"cpu": round(100 * sum(cores) / len(cores)) if cores else 0, "nucleos": len(cores) or (os.cpu_count() or 0),
|
|
305
|
+
"mem": memoria(), "disco": disco(), "bat": bateria()}
|
|
306
|
+
r.update({k: round(v, 1) for k, v in am.rede().items()})
|
|
307
|
+
return r
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def acao(m: dict) -> bool:
|
|
311
|
+
"""Ações da tela que só ela entende. True = tratada."""
|
|
312
|
+
a, arg = m.get("acao"), str(m.get("arg") or "")
|
|
313
|
+
if a == "calendario":
|
|
314
|
+
if calendario_permissao() == "pendente":
|
|
315
|
+
pedir_calendario() # o pedido do macOS sai com o nome do app (Primo)
|
|
316
|
+
else:
|
|
317
|
+
subprocess.Popen(["open", "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"])
|
|
318
|
+
return True
|
|
319
|
+
return False
|
|
@@ -27,7 +27,7 @@ import threading
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from typing import Optional
|
|
29
29
|
|
|
30
|
-
MODELO = os.environ.get("PRIMOCODE_WHISPER_MODELO", "
|
|
30
|
+
MODELO = os.environ.get("PRIMOCODE_WHISPER_MODELO", "base").strip() or "base"
|
|
31
31
|
PASTA = Path.home() / ".primocode" / "modelos" / "whisper"
|
|
32
32
|
|
|
33
33
|
_modelo = None
|
|
@@ -69,6 +69,8 @@ def aquecer() -> None:
|
|
|
69
69
|
def _ir():
|
|
70
70
|
try:
|
|
71
71
|
carregar()
|
|
72
|
+
import numpy as np # uma passada em silêncio: a primeira frase de verdade já não paga o aquecimento
|
|
73
|
+
list(_modelo.transcribe(np.zeros(16000, dtype=np.float32), language="pt", beam_size=1)[0])
|
|
72
74
|
except Exception:
|
|
73
75
|
pass
|
|
74
76
|
threading.Thread(target=_ir, daemon=True).start()
|
|
@@ -112,9 +114,18 @@ def aproveitavel(segmento) -> bool:
|
|
|
112
114
|
|
|
113
115
|
def transcrever(wav: Path, idioma: str = "pt") -> str:
|
|
114
116
|
modelo = carregar()
|
|
117
|
+
entrada: object = str(wav)
|
|
118
|
+
try: # WAV 16 kHz mono: entrega o áudio já decodificado (não depende do `av`, que às vezes destoa do faster-whisper)
|
|
119
|
+
import wave
|
|
120
|
+
import numpy as np
|
|
121
|
+
with wave.open(str(wav), "rb") as w:
|
|
122
|
+
if w.getframerate() == 16000 and w.getnchannels() == 1 and w.getsampwidth() == 2:
|
|
123
|
+
entrada = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
|
|
124
|
+
except Exception:
|
|
125
|
+
entrada = str(wav)
|
|
115
126
|
# vad_filter corta o silêncio das pontas: sem ele, o Whisper às vezes
|
|
116
127
|
# "ouve" uma frase inteira inventada no silêncio ("Obrigado por assistir").
|
|
117
|
-
segmentos, _ = modelo.transcribe(
|
|
128
|
+
segmentos, _ = modelo.transcribe(entrada, language=idioma, beam_size=1,
|
|
118
129
|
vad_filter=True, condition_on_previous_text=False)
|
|
119
130
|
return limpar(" ".join(s.text.strip() for s in segmentos if aproveitavel(s)))
|
|
120
131
|
|
|
Binary file
|
|
Binary file
|