primocode 10.15.1 → 10.16.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 +2 -2
- package/bin/primocode.js +73 -10
- package/lib/act.js +2 -0
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +3 -1
- package/lib/config.js +1 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +264 -0
- package/lib/estudio/index.js +9 -1
- package/lib/estudio/maquina-diretor.js +9 -5
- package/lib/estudio/maquina.js +5 -1
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +93 -1
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +22 -1
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/tools.js +20 -4
- package/lib/ui.js +1 -1
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +119 -46
- package/voz/primo_voz/app.py +582 -24
- package/voz/primo_voz/appmac.py +2 -1
- package/voz/primo_voz/chat.html +1762 -0
- package/voz/primo_voz/conversa.py +17 -32
- package/voz/primo_voz/escuta.py +15 -11
- package/voz/primo_voz/fala_pt.py +151 -0
- package/voz/primo_voz/hud.html +10 -4
- package/voz/primo_voz/hud.py +152 -7
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/transcrever.py +9 -3
|
@@ -6,7 +6,6 @@ teste mede. O que toca hardware (escuta.py, hud.py, __main__.py) só chama isto.
|
|
|
6
6
|
Maquina ouvindo → pensando → falando → (interrompido) → ouvindo
|
|
7
7
|
Fatiador texto em streaming → frases prontas para falar
|
|
8
8
|
resumo resposta longa → (fala curta, o que vai para o painel)
|
|
9
|
-
ativacao "Primo, abre o projeto" → (True, "abre o projeto")
|
|
10
9
|
comando "esconde o painel" → "esconder"
|
|
11
10
|
confirmacao ferramenta → "rodando os testes"
|
|
12
11
|
"""
|
|
@@ -136,10 +135,22 @@ def limpar_para_voz(texto: str) -> str:
|
|
|
136
135
|
return re.sub(r"\s+", " ", t).strip()
|
|
137
136
|
|
|
138
137
|
|
|
139
|
-
def resumo(texto: str, frases: int =
|
|
140
|
-
"""(fala, painel). A voz
|
|
138
|
+
def resumo(texto: str, frases: int = 60, teto: int = 3000) -> Tuple[str, str]:
|
|
139
|
+
"""(fala, painel). A voz diz a resposta COMPLETA (sem código, links nem caminhos); o painel guarda o texto
|
|
140
|
+
inteiro. Só código e lista enorme ficam fora da fala — e aí a prosa em volta já diz o que foi feito."""
|
|
141
141
|
painel = str(texto or "").strip()
|
|
142
|
-
|
|
142
|
+
sem_codigo = re.sub(r"```[\s\S]*?```", " ", painel)
|
|
143
|
+
# lista ENORME (mais de 8 itens seguidos) não é lida item a item
|
|
144
|
+
linhas, saida, run = sem_codigo.splitlines(), [], []
|
|
145
|
+
for l in linhas + [""]:
|
|
146
|
+
if re.match(r"^\s*(?:[-*•]|\d+[.)])\s+", l):
|
|
147
|
+
run.append(l)
|
|
148
|
+
continue
|
|
149
|
+
if run:
|
|
150
|
+
saida += run if len(run) <= 8 else [f"São {len(run)} itens, a lista completa está no painel."]
|
|
151
|
+
run = []
|
|
152
|
+
saida.append(l)
|
|
153
|
+
limpo = limpar_para_voz("\n".join(saida))
|
|
143
154
|
partes = [p.strip() for p in re.findall(r"[^.!?…]+[.!?…]*", limpo) if p.strip()]
|
|
144
155
|
fala = ""
|
|
145
156
|
for p in partes[:frases]:
|
|
@@ -148,37 +159,11 @@ def resumo(texto: str, frases: int = 2, teto: int = 260) -> Tuple[str, str]:
|
|
|
148
159
|
fala = (fala + " " + p).strip()
|
|
149
160
|
if not fala:
|
|
150
161
|
fala = limpo[:teto].strip()
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
fala += " O resto está no painel."
|
|
162
|
+
if not fala and re.search(r"```", painel):
|
|
163
|
+
fala = "Mandei o código para o painel."
|
|
154
164
|
return fala, painel
|
|
155
165
|
|
|
156
166
|
|
|
157
|
-
# ── a palavra de ativação ────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
_NOMES = ("primo", "primu", "prima", "primocode", "primo code")
|
|
160
|
-
_ANTES = ("ei", "ey", "hey", "oi", "ola", "fala", "e ai", "opa", "salve")
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def ativacao(texto: str, nome: str = "Primo") -> Tuple[bool, str]:
|
|
164
|
-
"""Começa com "Primo" (ou "ei Primo")? Devolve (sim?, o resto do pedido).
|
|
165
|
-
|
|
166
|
-
Só vale no INÍCIO da fala: "o primo da Ana ligou" não acorda ninguém."""
|
|
167
|
-
cru = str(texto or "").strip()
|
|
168
|
-
n = normalizar(cru)
|
|
169
|
-
meus = _NOMES if normalizar(nome) in ("primo", "") else (normalizar(nome),)
|
|
170
|
-
for ant in ("",) + _ANTES:
|
|
171
|
-
for nm in meus:
|
|
172
|
-
pre = (ant + " " + nm).strip()
|
|
173
|
-
if n == pre or n.startswith(pre + " "):
|
|
174
|
-
resto = re.sub(r"^\W*(?:" + r"\W+".join(re.escape(w) for w in pre.split()) + r")\W*", "",
|
|
175
|
-
_sem_acento(cru), flags=re.I)
|
|
176
|
-
# o corte vale no texto sem acento; o tamanho é o mesmo
|
|
177
|
-
resto = cru[len(cru) - len(resto):] if len(resto) <= len(cru) else resto
|
|
178
|
-
return True, resto.strip(" ,.!?;:-")
|
|
179
|
-
return False, cru
|
|
180
|
-
|
|
181
|
-
|
|
182
167
|
# ── comandos de voz que o painel e o modo resolvem sozinhos ──────────────
|
|
183
168
|
|
|
184
169
|
_COMANDOS = [
|
package/voz/primo_voz/escuta.py
CHANGED
|
@@ -47,10 +47,10 @@ TETO_FALA = 700.0 # nunca exige mais que isso (não ficar surdo)
|
|
|
47
47
|
FATOR_CONTINUAR = 0.5 # a histerese: depois de começar, o limiar cai
|
|
48
48
|
PRE_ROLL = 4 # blocos guardados ANTES do início da fala
|
|
49
49
|
|
|
50
|
-
RABO_SILENCIO = 1.
|
|
51
|
-
RABO_SILENCIO_LONGA = 1.
|
|
50
|
+
RABO_SILENCIO = 1.4 # silêncio que encerra a frase
|
|
51
|
+
RABO_SILENCIO_LONGA = 1.8 # …e quando a fala já vem longa
|
|
52
52
|
FALA_LONGA = 6.0
|
|
53
|
-
RABO_VAD =
|
|
53
|
+
RABO_VAD = 1.1 # silêncio que encerra a frase quando o VAD confirma
|
|
54
54
|
ESPERA_COMECAR = 8.0 # tempo para a pessoa começar a falar
|
|
55
55
|
MAX_FRASE = 45.0
|
|
56
56
|
MAX_FRASE_ESTENDIDA = 100.0
|
|
@@ -98,6 +98,7 @@ class Gravador:
|
|
|
98
98
|
# VAD opcional (Silero, via pysilero-vad): bloco → True se for voz.
|
|
99
99
|
# Com ele, a frase fecha em 0,9 s de silêncio em vez de 2 s.
|
|
100
100
|
self.vad = None
|
|
101
|
+
self.espera = ESPERA_COMECAR # quanto espera a fala começar (encurta quando há frase pendente)
|
|
101
102
|
|
|
102
103
|
# O ambiente é medido ENQUANTO ele espera a pessoa começar. Medir antes de
|
|
103
104
|
# abrir o microfone daria o silêncio do processo, não o do cômodo.
|
|
@@ -143,7 +144,7 @@ class Gravador:
|
|
|
143
144
|
self._falando = True
|
|
144
145
|
self._blocos = list(self._pre)
|
|
145
146
|
self._ultimo_som = agora
|
|
146
|
-
elif agora >
|
|
147
|
+
elif agora > self.espera:
|
|
147
148
|
self.encerrou_por = "ninguem-falou"
|
|
148
149
|
return True
|
|
149
150
|
return False
|
|
@@ -237,14 +238,17 @@ class Microfone:
|
|
|
237
238
|
import numpy as _np
|
|
238
239
|
|
|
239
240
|
def alimentar():
|
|
240
|
-
|
|
241
|
-
dados = _np.frombuffer(w.readframes(w.getnframes()), dtype=_np.int16)
|
|
242
|
-
_t.sleep(float(os.environ.get("PRIMOCODE_TESTE_ATRASO", "12")))
|
|
241
|
+
# "a.wav@12;b.wav@90": cada arquivo entra N s depois do anterior (ou da abertura)
|
|
243
242
|
silencio = _np.zeros(BLOCO, dtype=_np.int16)
|
|
244
|
-
for
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
for item in caminho.split(";"):
|
|
244
|
+
arq, _, espera = item.partition("@")
|
|
245
|
+
with wave.open(arq, "rb") as w:
|
|
246
|
+
dados = _np.frombuffer(w.readframes(w.getnframes()), dtype=_np.int16)
|
|
247
|
+
_t.sleep(float(espera or os.environ.get("PRIMOCODE_TESTE_ATRASO", "12")))
|
|
248
|
+
for i in range(0, len(dados) + BLOCO * 20, BLOCO):
|
|
249
|
+
bloco = dados[i:i + BLOCO]
|
|
250
|
+
self._ao_receber(bloco if len(bloco) == BLOCO else (_np.concatenate([bloco, silencio])[:BLOCO]))
|
|
251
|
+
_t.sleep(BLOCO_MS / 1000)
|
|
248
252
|
threading.Thread(target=alimentar, daemon=True).start()
|
|
249
253
|
|
|
250
254
|
def abrir(self) -> None:
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""fala_pt.py — o texto vira o que se FALA, em português do Brasil, antes de ir para a voz.
|
|
2
|
+
|
|
3
|
+
Rede de segurança: o cérebro já devolve um `falar` escrito para ser ouvido (app.py), e isto limpa o resto —
|
|
4
|
+
markdown, links, caminhos, código, emojis — e diz por extenso números, unidades, dinheiro, horas, datas,
|
|
5
|
+
versões e siglas ("32 s" → "trinta e dois segundos", "R$ 99" → "noventa e nove reais", "PR" → "pê érre").
|
|
6
|
+
Puro: sem rede, sem Mac. Python 3.9+.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
_UN = ["zero", "um", "dois", "três", "quatro", "cinco", "seis", "sete", "oito", "nove", "dez", "onze", "doze", "treze",
|
|
14
|
+
"catorze", "quinze", "dezesseis", "dezessete", "dezoito", "dezenove"]
|
|
15
|
+
_DZ = ["", "", "vinte", "trinta", "quarenta", "cinquenta", "sessenta", "setenta", "oitenta", "noventa"]
|
|
16
|
+
_CT = ["", "cento", "duzentos", "trezentos", "quatrocentos", "quinhentos", "seiscentos", "setecentos", "oitocentos", "novecentos"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _ate_999(n: int) -> str:
|
|
20
|
+
if n < 20:
|
|
21
|
+
return _UN[n]
|
|
22
|
+
if n < 100:
|
|
23
|
+
d, u = divmod(n, 10)
|
|
24
|
+
return _DZ[d] + (" e " + _UN[u] if u else "")
|
|
25
|
+
if n == 100:
|
|
26
|
+
return "cem"
|
|
27
|
+
c, r = divmod(n, 100)
|
|
28
|
+
return _CT[c] + (" e " + _ate_999(r) if r else "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extenso(n: int, feminino: bool = False) -> str:
|
|
32
|
+
"""0..999.999.999.999 por extenso ("mil duzentos e trinta e quatro")."""
|
|
33
|
+
if n < 0:
|
|
34
|
+
return "menos " + extenso(-n, feminino)
|
|
35
|
+
if n < 1000:
|
|
36
|
+
t = _ate_999(n)
|
|
37
|
+
if feminino:
|
|
38
|
+
t = re.sub(r"\bum\b", "uma", t)
|
|
39
|
+
t = re.sub(r"\bdois\b", "duas", t)
|
|
40
|
+
t = re.sub(r"(\w+)entos\b", r"\1entas", t)
|
|
41
|
+
return t
|
|
42
|
+
partes: List[str] = []
|
|
43
|
+
for valor, sing, plur in ((10 ** 9, "bilhão", "bilhões"), (10 ** 6, "milhão", "milhões"), (1000, "mil", "mil")):
|
|
44
|
+
q, n = divmod(n, valor)
|
|
45
|
+
if not q:
|
|
46
|
+
continue
|
|
47
|
+
if valor == 1000:
|
|
48
|
+
partes.append("mil" if q == 1 else extenso(q, feminino) + " mil")
|
|
49
|
+
else:
|
|
50
|
+
partes.append(extenso(q) + " " + (sing if q == 1 else plur))
|
|
51
|
+
if n:
|
|
52
|
+
partes.append(("e " if n < 100 or n % 100 == 0 else "") + extenso(n, feminino))
|
|
53
|
+
return " ".join(partes).replace(" ", " ")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _inteiro(txt: str) -> int:
|
|
57
|
+
return int(txt.replace(".", "")) if re.fullmatch(r"\d{1,3}(?:\.\d{3})+", txt) else int(txt)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _decimal(parte_int: str, parte_dec: str) -> str:
|
|
61
|
+
return f"{extenso(_inteiro(parte_int))} vírgula " + " ".join(extenso(int(d)) for d in parte_dec) if parte_dec.startswith("0") \
|
|
62
|
+
else f"{extenso(_inteiro(parte_int))} vírgula {extenso(int(parte_dec))}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _num(txt: str) -> str:
|
|
66
|
+
if "," in txt:
|
|
67
|
+
i, d = txt.split(",", 1)
|
|
68
|
+
return _decimal(i, d)
|
|
69
|
+
return extenso(_inteiro(txt))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_UNIDADES = {
|
|
73
|
+
"ms": ("milissegundo", "milissegundos"), "s": ("segundo", "segundos"), "seg": ("segundo", "segundos"),
|
|
74
|
+
"min": ("minuto", "minutos"), "h": ("hora", "horas"), "px": ("pixel", "pixels"),
|
|
75
|
+
"kb": ("kilobyte", "kilobytes"), "mb": ("megabyte", "megabytes"), "gb": ("gigabyte", "gigabytes"), "tb": ("terabyte", "terabytes"),
|
|
76
|
+
"km": ("quilômetro", "quilômetros"), "kg": ("quilo", "quilos"), "g": ("grama", "gramas"), "cm": ("centímetro", "centímetros"),
|
|
77
|
+
"mm": ("milímetro", "milímetros"), "m": ("metro", "metros"), "fps": ("quadro por segundo", "quadros por segundo"),
|
|
78
|
+
"mhz": ("megahertz", "megahertz"), "ghz": ("gigahertz", "gigahertz"), "hz": ("hertz", "hertz"),
|
|
79
|
+
"kbps": ("kilobit por segundo", "kilobits por segundo"), "mbps": ("megabit por segundo", "megabits por segundo"),
|
|
80
|
+
}
|
|
81
|
+
_SIGLAS = {
|
|
82
|
+
"PR": "pê érre", "PRs": "pê érres", "npm": "ene pê eme", "CTA": "chamada", "API": "á pê í", "APIs": "á pê ís", "CSS": "cê ésse ésse",
|
|
83
|
+
"HTML": "agá tê eme éle", "JSON": "jêi són", "URL": "u érre éle", "CLI": "cê éle í", "CPU": "cê pê u", "RAM": "ram", "UI": "u í",
|
|
84
|
+
"UX": "u ékis", "SQL": "ésse quê éle", "SVG": "ésse vê gê", "PDF": "pê dê éfe", "MP3": "ême pê três", "MP4": "ême pê quatro",
|
|
85
|
+
"IA": "í á", "GPU": "gê pê u", "SSD": "ésse ésse dê", "TTS": "tê tê ésse", "STT": "ésse tê tê", "VAD": "vê á dê", "OK": "ôquei",
|
|
86
|
+
"TCC": "tê cê cê", "SDK": "ésse dê cá", "CI": "cê í", "CD": "cê dê", "ID": "í dê", "TI": "tê í", "QA": "quê á", "OKR": "ô cá érre",
|
|
87
|
+
"git": "guit", "GitHub": "guit rrâb", "README": "rídmi", "Spotify": "Spotifái", "WhatsApp": "uótsápi", "macOS": "mák ó ésse",
|
|
88
|
+
}
|
|
89
|
+
_LETRAS = dict(zip("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "á bê cê dê é éfe gê agá í jota cá éle ême ene ó pê quê érre ésse tê u vê dáblio ékis ípsilon zê".split()))
|
|
90
|
+
_MESES = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _dinheiro(m: "re.Match") -> str:
|
|
94
|
+
ints, cents = m.group(1), m.group(2)
|
|
95
|
+
n = _inteiro(ints)
|
|
96
|
+
t = extenso(n) + (" real" if n == 1 else " reais")
|
|
97
|
+
if cents and int(cents):
|
|
98
|
+
c = int(cents)
|
|
99
|
+
t += " e " + extenso(c) + (" centavo" if c == 1 else " centavos")
|
|
100
|
+
return t
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _unidade(m: "re.Match") -> str:
|
|
104
|
+
num, un = m.group(1), m.group(2)
|
|
105
|
+
forma = _UNIDADES.get(un.lower())
|
|
106
|
+
if not forma or (len(un) == 1 and un not in "smhg" and un != "m"):
|
|
107
|
+
return m.group(0)
|
|
108
|
+
singular = num in ("1", "01") or num == "1,0"
|
|
109
|
+
return f"{_num(num)} {forma[0] if singular else forma[1]}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def normalizar(texto: str) -> str:
|
|
113
|
+
t = str(texto or "")
|
|
114
|
+
t = re.sub(r"```[\s\S]*?```", " ", t) # bloco de código
|
|
115
|
+
t = re.sub(r"`([^`\n]*)`", lambda m: "" if re.search(r"[/\\.]|\(|\)", m.group(1)) else m.group(1), t)
|
|
116
|
+
t = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", t) # [texto](link) → texto
|
|
117
|
+
t = re.sub(r"https?://\S+|www\.\S+", "", t)
|
|
118
|
+
t = re.sub(r"(?<![\w])(?:~|\.{1,2})?/[\w.\-@%+/]+", " ", t) # caminhos /a/b.js ~/Primo/x
|
|
119
|
+
t = re.sub(r"\b[\w\-]+(?:/[\w.\-]+)+\.\w+\b", " ", t) # a/b/c.js
|
|
120
|
+
t = re.sub(r"\b[\w\-]+\.(?:js|ts|tsx|jsx|py|html|css|json|md|sh|yml|yaml|png|jpg|svg|mp4|wav|mp3|pdf)\b", "um arquivo", t)
|
|
121
|
+
t = re.sub(r"^\s*(?:#{1,6}|[-*•]|\d+[.)])\s+", "", t, flags=re.M) # títulos e marcadores
|
|
122
|
+
t = re.sub(r"[*_#>|~]+", " ", t)
|
|
123
|
+
t = re.sub(r"[\U0001F000-\U0001FAFF☀-➿⬀-⯿️←-⇿⌀-⏿●▸▪▣⎿]", " ", t) # emojis e símbolos
|
|
124
|
+
# dinheiro
|
|
125
|
+
t = re.sub(r"R\$\s*(\d{1,3}(?:\.\d{3})*|\d+)(?:,(\d{1,2}))?", _dinheiro, t)
|
|
126
|
+
# versões: v10.15.2 / 10.15.2 / v10.15
|
|
127
|
+
t = re.sub(r"\bv(\d+(?:\.\d+){1,3})\b", lambda m: "versão " + " ponto ".join(extenso(int(x)) for x in m.group(1).split(".")), t)
|
|
128
|
+
t = re.sub(r"\b(\d+(?:\.\d+){2,3})\b", lambda m: " ponto ".join(extenso(int(x)) for x in m.group(1).split(".")), t)
|
|
129
|
+
# hora e data
|
|
130
|
+
t = re.sub(r"\b([01]?\d|2[0-3]):([0-5]\d)\b", lambda m: extenso(int(m.group(1))) + (" horas" if int(m.group(1)) != 1 else " hora") + (" e " + extenso(int(m.group(2))) if int(m.group(2)) else ""), t)
|
|
131
|
+
t = re.sub(r"\b(0?[1-9]|[12]\d|3[01])/(0?[1-9]|1[0-2])(?:/(\d{4}))?\b",
|
|
132
|
+
lambda m: extenso(int(m.group(1))) + " de " + _MESES[int(m.group(2)) - 1] + (" de " + extenso(int(m.group(3))) if m.group(3) else ""), t)
|
|
133
|
+
# porcentagem
|
|
134
|
+
t = re.sub(r"(\d+(?:,\d+)?)\s?%", lambda m: _num(m.group(1)) + " por cento", t)
|
|
135
|
+
# número + unidade ("32 s", "10px", "1,5 GB")
|
|
136
|
+
t = re.sub(r"(?<![\w.,])(\d{1,3}(?:\.\d{3})+|\d+(?:,\d+)?)\s?([A-Za-z]{1,4})\b(?!\w)", _unidade, t)
|
|
137
|
+
# siglas e nomes técnicos (palavra inteira)
|
|
138
|
+
for sig in sorted(_SIGLAS, key=len, reverse=True):
|
|
139
|
+
t = re.sub(r"(?<![\w])" + re.escape(sig) + r"(?![\w])", _SIGLAS[sig], t)
|
|
140
|
+
t = re.sub(r"(?<![\w])([A-Z]{2,4})(?![\w])", lambda m: " ".join(_LETRAS[c] for c in m.group(1)), t) # sigla desconhecida: soletra
|
|
141
|
+
# números que sobraram
|
|
142
|
+
t = re.sub(r"(?<![\w.])(\d{1,3}(?:\.\d{3})+|\d+),(\d+)(?![\w])", lambda m: _decimal(m.group(1), m.group(2)), t)
|
|
143
|
+
t = re.sub(r"(?<![\w.,])(\d{1,3}(?:\.\d{3})+|\d+)(?![\w.,]\d)", lambda m: extenso(_inteiro(m.group(1))), t)
|
|
144
|
+
t = re.sub(r"\s*[()\[\]{}]\s*", ", ", t)
|
|
145
|
+
t = re.sub(r"\s*[—–]\s*", ", ", t)
|
|
146
|
+
t = re.sub(r"\s*[&]\s*", " e ", t)
|
|
147
|
+
t = re.sub(r"\s*\+\s*", " mais ", t)
|
|
148
|
+
t = re.sub(r"\s*=>?\s*", " ", t)
|
|
149
|
+
t = re.sub(r"\s+([,.!?;:])", r"\1", t)
|
|
150
|
+
t = re.sub(r"(,\s*){2,}", ", ", t)
|
|
151
|
+
return re.sub(r"\s+", " ", t).strip(" ,")
|
package/voz/primo_voz/hud.html
CHANGED
|
@@ -144,6 +144,10 @@
|
|
|
144
144
|
.leg span::before { content:""; display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; background:var(--marca); }
|
|
145
145
|
.leg span.p::before { background:var(--ok); }
|
|
146
146
|
.tecla { font-size:12px; color:var(--fraco); white-space:nowrap; }
|
|
147
|
+
.voltar-chat { display:inline-flex; align-items:center; gap:8px; height:38px; padding:0 16px; border:1px solid var(--borda); border-radius:999px; background:var(--fundo); color:var(--texto); font:500 13px var(--sans); cursor:pointer; white-space:nowrap; transition:border-color .2s; }
|
|
148
|
+
.voltar-chat:hover { border-color:var(--marca); }
|
|
149
|
+
.voltar-chat kbd { font:11px var(--mono); color:var(--fraco); }
|
|
150
|
+
.voltar-chat[hidden] { display:none; }
|
|
147
151
|
</style>
|
|
148
152
|
</head>
|
|
149
153
|
<body>
|
|
@@ -270,6 +274,7 @@
|
|
|
270
274
|
<div id="barras"></div>
|
|
271
275
|
<div class="leg"><span>você</span><span class="p" id="leg-nome">Primo</span></div>
|
|
272
276
|
<div class="tecla">⌨ luz do teclado no ritmo da voz</div>
|
|
277
|
+
<button class="voltar-chat" id="b-chat" hidden onclick="post({acao:'chat'})">← Voltar pro chat <kbd>esc</kbd></button>
|
|
273
278
|
</section>
|
|
274
279
|
</main>
|
|
275
280
|
|
|
@@ -529,7 +534,7 @@ function classeTela() {
|
|
|
529
534
|
function setEstado(e) {
|
|
530
535
|
if (!e || e === S.estado) return;
|
|
531
536
|
S.estado = e; classeTela(); $('nome').dataset.e = e;
|
|
532
|
-
$('estado-t').textContent = e === 'dormindo' ? '
|
|
537
|
+
$('estado-t').textContent = e === 'dormindo' ? 'clique para falar' : (ROTULO[e] || e);
|
|
533
538
|
R3.estado(e);
|
|
534
539
|
S.olhar = (e === 'pensando' || e === 'executando') ? 1 : 0;
|
|
535
540
|
$('eq').setAttribute('opacity', e === 'falando' ? 1 : 0);
|
|
@@ -557,16 +562,17 @@ function laco(t) { SP.quadro(t); if (!R3.ok && !SP.ok) animaEq(t); requestAnimat
|
|
|
557
562
|
/* ── a ponte com o Python ── */
|
|
558
563
|
window.primo = (m) => {
|
|
559
564
|
if (m.pasta !== undefined) $('pasta').textContent = m.pasta;
|
|
565
|
+
if (m.comChat) $('b-chat').hidden = false;
|
|
560
566
|
if (m.login !== undefined) $('b-entrar').hidden = !!m.login;
|
|
561
|
-
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome;
|
|
567
|
+
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome; }
|
|
562
568
|
if (m.robo === 'expressive' && !SP.ok) { if (SP.cancela) SP.cancela(); SP.falhou = true; classeTela(); }
|
|
563
569
|
if (m.evento) R3.evento(m.evento);
|
|
564
570
|
if (m.boot) montar();
|
|
565
571
|
if (m.estado) setEstado(m.estado);
|
|
566
572
|
if (m.barras) { S.quem = m.barras.quem; S.nivel = m.barras.nivel; R3.nivelFala(m.barras.quem === 'primo' ? m.barras.nivel : 0); }
|
|
567
573
|
if (m.transcricao !== undefined || m.resposta !== undefined) { const t = m.resposta || m.transcricao || ''; $('fala').textContent = t.length > 160 ? t.slice(0, 157) + '…' : t; }
|
|
568
|
-
if (m.calendario) { S.perm = m.calendario.permissao; diasComEvento = m.calendario.dias || []; desenharCalendario(); desenharHoje(); }
|
|
569
|
-
if (m.hoje) { S.hoje = m.hoje; desenharHoje(); }
|
|
574
|
+
if (m.calendario) { const k = JSON.stringify(m.calendario); if (k !== S.calKey) { S.calKey = k; S.perm = m.calendario.permissao; diasComEvento = m.calendario.dias || []; desenharCalendario(); desenharHoje(); } }
|
|
575
|
+
if (m.hoje) { const k = JSON.stringify(m.hoje); S.perm = 'ok'; if (k !== S.hojeKey) { S.hojeKey = k; S.hoje = m.hoje; desenharHoje(); } }
|
|
570
576
|
if (m.clima) desenharClima(m.clima);
|
|
571
577
|
if (m.musica !== undefined) desenharMusica(m.musica);
|
|
572
578
|
if (m.maquina) desenharMaquina(m.maquina);
|
package/voz/primo_voz/hud.py
CHANGED
|
@@ -17,6 +17,7 @@ import json
|
|
|
17
17
|
import math
|
|
18
18
|
import os
|
|
19
19
|
import queue
|
|
20
|
+
import subprocess
|
|
20
21
|
import sys
|
|
21
22
|
import threading
|
|
22
23
|
import time
|
|
@@ -29,8 +30,9 @@ from primo_voz.sons import Sons
|
|
|
29
30
|
AQUI = Path(__file__).resolve().parent
|
|
30
31
|
|
|
31
32
|
|
|
32
|
-
def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None) -> int:
|
|
33
|
-
"""entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout.
|
|
33
|
+
def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None, chat=None) -> int:
|
|
34
|
+
"""entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout.
|
|
35
|
+
chat: o app.Chat; com ele a janela abre no chat.html e a voz (hud.html) fica atrás, trocada pelo botão de onda."""
|
|
34
36
|
try:
|
|
35
37
|
from AppKit import (NSApplication, NSApplicationActivationPolicyRegular, NSImage, NSMenu, NSMenuItem, NSBackingStoreBuffered, NSColor,
|
|
36
38
|
NSEvent, NSScreen, NSWindow)
|
|
@@ -55,6 +57,40 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
55
57
|
def js(msg: dict) -> None:
|
|
56
58
|
fila.put("window.primo(" + json.dumps(msg, ensure_ascii=False) + ")")
|
|
57
59
|
|
|
60
|
+
fila_chat: "queue.Queue[str]" = queue.Queue()
|
|
61
|
+
fila_ui: "queue.Queue" = queue.Queue() # o que mexe na janela, rodado na thread principal
|
|
62
|
+
|
|
63
|
+
def js_chat(msg: dict) -> None:
|
|
64
|
+
fila_chat.put("window.chat&&window.chat(" + json.dumps(msg, ensure_ascii=False) + ")")
|
|
65
|
+
|
|
66
|
+
telas: dict = {}
|
|
67
|
+
|
|
68
|
+
def mostrar(qual: str) -> None:
|
|
69
|
+
"""Troca entre o chat e a voz na mesma janela, sem recarregar nenhuma das duas."""
|
|
70
|
+
if not telas:
|
|
71
|
+
return
|
|
72
|
+
telas["chat"].setHidden_(qual != "chat")
|
|
73
|
+
telas["hud"].setHidden_(qual != "hud")
|
|
74
|
+
telas["atual"] = qual
|
|
75
|
+
telas["jan"].makeFirstResponder_(telas[qual])
|
|
76
|
+
if qual == "hud":
|
|
77
|
+
js({"comChat": True})
|
|
78
|
+
js({"boot": True})
|
|
79
|
+
if chat is not None:
|
|
80
|
+
threading.Thread(target=chat.app.ligar_voz, daemon=True).start()
|
|
81
|
+
elif chat is not None: # voltou pro chat: o microfone fecha
|
|
82
|
+
threading.Thread(target=chat.app.desligar_voz, daemon=True).start()
|
|
83
|
+
|
|
84
|
+
def painel_arquivos(pastas: bool):
|
|
85
|
+
from AppKit import NSOpenPanel
|
|
86
|
+
pn_ = NSOpenPanel.openPanel()
|
|
87
|
+
pn_.setCanChooseFiles_(not pastas)
|
|
88
|
+
pn_.setCanChooseDirectories_(pastas)
|
|
89
|
+
pn_.setAllowsMultipleSelection_(not pastas)
|
|
90
|
+
if pn_.runModal() != 1:
|
|
91
|
+
return []
|
|
92
|
+
return [str(u.path()) for u in pn_.URLs()]
|
|
93
|
+
|
|
58
94
|
class Janela(NSWindow):
|
|
59
95
|
def canBecomeKeyWindow(self):
|
|
60
96
|
return True
|
|
@@ -72,6 +108,19 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
72
108
|
break
|
|
73
109
|
self.web.evaluateJavaScript_completionHandler_(cod, None)
|
|
74
110
|
n += 1
|
|
111
|
+
while telas:
|
|
112
|
+
try:
|
|
113
|
+
cod = fila_chat.get_nowait()
|
|
114
|
+
except queue.Empty:
|
|
115
|
+
break
|
|
116
|
+
telas["chat"].evaluateJavaScript_completionHandler_(cod, None)
|
|
117
|
+
while True:
|
|
118
|
+
try:
|
|
119
|
+
fila_ui.get_nowait()()
|
|
120
|
+
except queue.Empty:
|
|
121
|
+
break
|
|
122
|
+
except Exception as e:
|
|
123
|
+
sys.stderr.write(f"hud: {e}\n")
|
|
75
124
|
if self.jan.isMiniaturized():
|
|
76
125
|
return
|
|
77
126
|
if p.visivel != bool(self.jan.isVisible()):
|
|
@@ -88,6 +137,18 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
88
137
|
p.visivel = False
|
|
89
138
|
return True
|
|
90
139
|
|
|
140
|
+
def applicationShouldTerminate_(self, _a):
|
|
141
|
+
try: # quem pediu para sair? (vai para o log: ajuda a achar quem fecha o app)
|
|
142
|
+
from Foundation import NSAppleEventManager
|
|
143
|
+
ev = NSAppleEventManager.sharedAppleEventManager().currentAppleEvent()
|
|
144
|
+
pid = ev.attributeDescriptorForKeyword_(0x73706964).int32Value() if ev is not None else 0
|
|
145
|
+
quem = subprocess.run(["ps", "-p", str(pid), "-o", "command="], capture_output=True, text=True).stdout.strip()[:160] if pid else "interno"
|
|
146
|
+
sys.stderr.write(f"hud: pedido para SAIR de pid={pid} ({quem})\n")
|
|
147
|
+
sys.stderr.flush()
|
|
148
|
+
except Exception as e:
|
|
149
|
+
sys.stderr.write(f"hud: pedido para sair (não deu para ver quem: {e})\n")
|
|
150
|
+
return 1
|
|
151
|
+
|
|
91
152
|
def applicationWillTerminate_(self, _n):
|
|
92
153
|
if ao_sair:
|
|
93
154
|
try:
|
|
@@ -111,6 +172,8 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
111
172
|
sys.stderr.flush()
|
|
112
173
|
elif a == "sair":
|
|
113
174
|
p.visivel = False
|
|
175
|
+
elif a == "chat": # "Voltar pro chat", na tela de voz
|
|
176
|
+
mostrar("chat")
|
|
114
177
|
elif a in ("ptt", "entrar"):
|
|
115
178
|
emitir({"acao": a})
|
|
116
179
|
elif a == "print": # esconde o HUD para o print pegar a tela de verdade
|
|
@@ -127,6 +190,52 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
127
190
|
js({"musica": td.musica()})
|
|
128
191
|
threading.Thread(target=_fazer, daemon=True).start()
|
|
129
192
|
|
|
193
|
+
class MsgChat(NSObject, protocols=[objc.protocolNamed("WKScriptMessageHandler")]):
|
|
194
|
+
"""A ponte do chat.html. O que mexe na janela (trocar de tela, painel de arquivos, abrir link) é daqui; o resto vai para o app.Chat."""
|
|
195
|
+
def userContentController_didReceiveScriptMessage_(self, ucc, msg):
|
|
196
|
+
try:
|
|
197
|
+
m = json.loads(str(msg.body()))
|
|
198
|
+
except ValueError:
|
|
199
|
+
return
|
|
200
|
+
a = m.get("acao")
|
|
201
|
+
if a == "erro":
|
|
202
|
+
sys.stderr.write("chat.html: " + str(m.get("arg")) + "\n")
|
|
203
|
+
sys.stderr.flush()
|
|
204
|
+
elif a == "voz": # botão de onda: abre a voz e já começa a ouvir
|
|
205
|
+
mostrar("hud")
|
|
206
|
+
emitir({"acao": "ptt"})
|
|
207
|
+
elif a == "anexar":
|
|
208
|
+
caminhos = painel_arquivos(False)
|
|
209
|
+
if caminhos:
|
|
210
|
+
threading.Thread(target=chat.anexar_caminhos, args=(caminhos,), daemon=True).start()
|
|
211
|
+
elif a == "escolher_pasta":
|
|
212
|
+
caminhos = painel_arquivos(True)
|
|
213
|
+
if caminhos:
|
|
214
|
+
threading.Thread(target=chat.escolher_pasta, args=(caminhos[0],), daemon=True).start()
|
|
215
|
+
elif a == "abrir_link":
|
|
216
|
+
from AppKit import NSWorkspace
|
|
217
|
+
u = str(m.get("url") or "")
|
|
218
|
+
if u.startswith(("http://", "https://")):
|
|
219
|
+
NSWorkspace.sharedWorkspace().openURL_(NSURL.URLWithString_(u))
|
|
220
|
+
elif a == "haptico": # a travadinha do esforço: o trackpad dá um toque em cada nível
|
|
221
|
+
try:
|
|
222
|
+
from AppKit import NSHapticFeedbackManager
|
|
223
|
+
padrao = 2 if m.get("forte") else 1 # 1 = alinhamento (leve), 2 = troca de nível (mais firme)
|
|
224
|
+
NSHapticFeedbackManager.defaultPerformer().performFeedbackPattern_performanceTime_(padrao, 1)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
sys.stderr.write(f"hud: háptico: {e}\n")
|
|
227
|
+
elif a in ("abrir_arquivo", "mostrar_finder"): # só o que existe; o Mac abre com o app padrão
|
|
228
|
+
from AppKit import NSWorkspace
|
|
229
|
+
c = Path(os.path.expanduser(str(m.get("caminho") or "")))
|
|
230
|
+
if c.is_absolute() and c.exists():
|
|
231
|
+
ws = NSWorkspace.sharedWorkspace()
|
|
232
|
+
if a == "abrir_arquivo":
|
|
233
|
+
ws.openURL_(NSURL.fileURLWithPath_(str(c)))
|
|
234
|
+
else:
|
|
235
|
+
ws.activateFileViewerSelectingURLs_([NSURL.fileURLWithPath_(str(c))])
|
|
236
|
+
else:
|
|
237
|
+
chat.receber(m)
|
|
238
|
+
|
|
130
239
|
app = NSApplication.sharedApplication()
|
|
131
240
|
app.setActivationPolicy_(NSApplicationActivationPolicyRegular) # app de verdade: Dock e Cmd+Tab
|
|
132
241
|
try: # nome e ícone do Primo, mesmo fora do bundle
|
|
@@ -172,21 +281,49 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
172
281
|
web = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg)
|
|
173
282
|
web.setAutoresizingMask_(2 | 16)
|
|
174
283
|
web.setValue_forKey_(False, "drawsBackground")
|
|
175
|
-
jan.setContentView_(web)
|
|
176
284
|
url = NSURL.fileURLWithPath_(str(AQUI / "hud.html"))
|
|
177
285
|
web.loadFileURL_allowingReadAccessToURL_(url, NSURL.fileURLWithPath_(str(AQUI)))
|
|
286
|
+
if chat is None:
|
|
287
|
+
jan.setContentView_(web)
|
|
288
|
+
else: # o chat na frente; a voz atrás, já carregada (troca sem esperar)
|
|
289
|
+
from AppKit import NSView
|
|
290
|
+
fundo = NSView.alloc().initWithFrame_(NSMakeRect(0, 0, tela.size.width, tela.size.height))
|
|
291
|
+
fundo.setAutoresizingMask_(2 | 16)
|
|
292
|
+
cfg_c = WKWebViewConfiguration.alloc().init()
|
|
293
|
+
cfg_c.preferences().setValue_forKey_(True, "allowFileAccessFromFileURLs") # o vídeo/imagem/PDF citados tocam ali mesmo
|
|
294
|
+
cfg_c.userContentController().addScriptMessageHandler_name_(MsgChat.alloc().init(), "chat")
|
|
295
|
+
web_c = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg_c)
|
|
296
|
+
web_c.setAutoresizingMask_(2 | 16)
|
|
297
|
+
web_c.setValue_forKey_(False, "drawsBackground")
|
|
298
|
+
# leitura da pasta pessoal: os arquivos que o agente cita (~/Primo/…) aparecem no chat sem servidor
|
|
299
|
+
web_c.loadFileURL_allowingReadAccessToURL_(NSURL.fileURLWithPath_(str(AQUI / "chat.html")), NSURL.fileURLWithPath_(str(Path.home())))
|
|
300
|
+
fundo.addSubview_(web)
|
|
301
|
+
fundo.addSubview_(web_c)
|
|
302
|
+
jan.setContentView_(fundo)
|
|
303
|
+
telas.update({"chat": web_c, "hud": web, "jan": jan, "atual": "chat"})
|
|
304
|
+
web.setHidden_(True)
|
|
305
|
+
chat.js = js_chat
|
|
306
|
+
chat.mostrar_voz = lambda: fila_ui.put(lambda: mostrar("hud")) # um pedido falado com o chat na frente
|
|
178
307
|
jan.makeKeyAndOrderFront_(None)
|
|
179
308
|
app.activateIgnoringOtherApps_(True)
|
|
180
309
|
alvo = Alvo.alloc().init()
|
|
181
310
|
alvo.jan, alvo.web = jan, web
|
|
182
311
|
NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(1 / 30, alvo, b"tick:", None, True)
|
|
183
|
-
|
|
312
|
+
if chat is None: # sem chat a voz é a tela: o robô acorda com o som
|
|
313
|
+
threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}), js({"boot": True}), sons.tocar("boot"))).start()
|
|
314
|
+
else: # com chat: só o nome e o robô, em silêncio; o boot fica pra quando abrir a voz
|
|
315
|
+
threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}))).start()
|
|
184
316
|
|
|
185
317
|
def esconder():
|
|
186
318
|
p.visivel = False
|
|
187
319
|
|
|
188
320
|
def tecla(ev):
|
|
189
|
-
if ev.keyCode() == 53: # ESC
|
|
321
|
+
if ev.keyCode() == 53: # ESC: na voz com chat, volta pro chat; no chat, fica com a página
|
|
322
|
+
if telas:
|
|
323
|
+
if telas["atual"] == "hud":
|
|
324
|
+
mostrar("chat")
|
|
325
|
+
return None
|
|
326
|
+
return ev
|
|
190
327
|
esconder()
|
|
191
328
|
return None
|
|
192
329
|
return ev
|
|
@@ -220,11 +357,19 @@ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=
|
|
|
220
357
|
def agenda(): # em outra thread: o pedido de permissão do Calendário pode demorar
|
|
221
358
|
evs: dict = {"permissao": "pendente", "eventos": []}
|
|
222
359
|
k = 0
|
|
360
|
+
falhas = 0
|
|
223
361
|
while True:
|
|
224
362
|
try:
|
|
225
363
|
if k % 12 == 0 or evs["permissao"] != "ok": # sem acesso, olha a cada 5 s (a pessoa pode ter liberado)
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
novo = td.eventos_do_mes()
|
|
365
|
+
if evs["permissao"] == "ok" and novo["permissao"] != "ok":
|
|
366
|
+
falhas += 1 # erro transitório não derruba o cartão: segura os últimos eventos
|
|
367
|
+
if falhas >= 4:
|
|
368
|
+
evs, falhas = novo, 0
|
|
369
|
+
else:
|
|
370
|
+
evs, falhas = novo, 0
|
|
371
|
+
# SEMPRE manda o estado junto: o painel nunca depende de uma mensagem que pode ter chegado antes da página
|
|
372
|
+
js({"calendario": {"permissao": evs["permissao"], "dias": sorted({e["dia"] for e in evs["eventos"]})}})
|
|
228
373
|
if evs["permissao"] == "ok":
|
|
229
374
|
js({"hoje": td.hoje_lista(evs["eventos"], time.time())})
|
|
230
375
|
except Exception as e:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""identidade.py — quem é o assistente e quem é a pessoa.
|
|
2
2
|
|
|
3
3
|
O assistente se chama "Primo" até a pessoa dar outro nome ("seu nome agora é
|
|
4
|
-
Luna", ou `primocode voz nome Luna`).
|
|
4
|
+
Luna", ou `primocode voz nome Luna`). O nome NÃO ativa nada: o modo voz só abre no clique. Fica em
|
|
5
5
|
~/.primocode/voz.json, junto com o nome da pessoa e as chaves de luz e som.
|
|
6
6
|
|
|
7
7
|
Personalidade: curta, natural, humor leve. Cumprimenta pela hora, lembra o nome
|
|
@@ -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", "small").strip() or "small"
|
|
31
31
|
PASTA = Path.home() / ".primocode" / "modelos" / "whisper"
|
|
32
32
|
|
|
33
33
|
_modelo = None
|
|
@@ -112,6 +112,11 @@ def aproveitavel(segmento) -> bool:
|
|
|
112
112
|
return not (sem_fala > SEM_FALA and confianca < CONFIANCA_MINIMA)
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
# O vocabulário do Primo vai de dica ao Whisper: medido em 13 frases pt-BR, o WER do `small` caiu de 15,1% para 11,6% (e de 29,6% para 18,5% com ruído).
|
|
116
|
+
PROMPT_INICIAL = ("Primo, PrimoCode, commit, deploy, Spotify, Railway, projeto, testes, servidor, landing page, site, padaria, "
|
|
117
|
+
"agenda, volume, abre o projeto, cria um site, roda os testes.")
|
|
118
|
+
|
|
119
|
+
|
|
115
120
|
def transcrever(wav: Path, idioma: str = "pt") -> str:
|
|
116
121
|
modelo = carregar()
|
|
117
122
|
entrada: object = str(wav)
|
|
@@ -125,8 +130,9 @@ def transcrever(wav: Path, idioma: str = "pt") -> str:
|
|
|
125
130
|
entrada = str(wav)
|
|
126
131
|
# vad_filter corta o silêncio das pontas: sem ele, o Whisper às vezes
|
|
127
132
|
# "ouve" uma frase inteira inventada no silêncio ("Obrigado por assistir").
|
|
128
|
-
segmentos, _ = modelo.transcribe(entrada, language=idioma, beam_size=
|
|
129
|
-
vad_filter=True, condition_on_previous_text=False
|
|
133
|
+
segmentos, _ = modelo.transcribe(entrada, language=idioma, beam_size=2,
|
|
134
|
+
vad_filter=True, condition_on_previous_text=False,
|
|
135
|
+
initial_prompt=PROMPT_INICIAL)
|
|
130
136
|
return limpar(" ".join(s.text.strip() for s in segmentos if aproveitavel(s)))
|
|
131
137
|
|
|
132
138
|
|