primocode 10.15.0 → 10.15.2
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 +18 -3
- package/lib/config.js +1 -1
- package/lib/conta.js +1 -0
- package/lib/estudio/exportar.js +1 -0
- package/lib/mcp.js +1 -0
- package/lib/tools.js +1 -0
- package/lib/voz.js +27 -3
- package/package.json +1 -1
- package/voz/primo_voz/__main__.py +201 -24
- package/voz/primo_voz/app.py +451 -0
- package/voz/primo_voz/appmac.py +125 -0
- package/voz/primo_voz/assets/mascote.svg +1 -0
- package/voz/primo_voz/conversa.py +17 -6
- package/voz/primo_voz/escuta.py +30 -4
- package/voz/primo_voz/fala.py +87 -0
- package/voz/primo_voz/fala_pt.py +151 -0
- package/voz/primo_voz/hud.html +60 -26
- package/voz/primo_voz/hud.py +84 -12
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/tela.py +36 -54
- package/voz/primo_voz/transcrever.py +19 -2
- package/voz/primo_voz/vendor/primo-icone.png +0 -0
package/voz/primo_voz/escuta.py
CHANGED
|
@@ -30,6 +30,7 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
import threading
|
|
32
32
|
import time
|
|
33
|
+
import os
|
|
33
34
|
import wave
|
|
34
35
|
from pathlib import Path
|
|
35
36
|
from typing import List, Optional
|
|
@@ -46,10 +47,10 @@ TETO_FALA = 700.0 # nunca exige mais que isso (não ficar surdo)
|
|
|
46
47
|
FATOR_CONTINUAR = 0.5 # a histerese: depois de começar, o limiar cai
|
|
47
48
|
PRE_ROLL = 4 # blocos guardados ANTES do início da fala
|
|
48
49
|
|
|
49
|
-
RABO_SILENCIO =
|
|
50
|
-
RABO_SILENCIO_LONGA =
|
|
50
|
+
RABO_SILENCIO = 1.4 # silêncio que encerra a frase
|
|
51
|
+
RABO_SILENCIO_LONGA = 1.8 # …e quando a fala já vem longa
|
|
51
52
|
FALA_LONGA = 6.0
|
|
52
|
-
RABO_VAD =
|
|
53
|
+
RABO_VAD = 1.1 # silêncio que encerra a frase quando o VAD confirma
|
|
53
54
|
ESPERA_COMECAR = 8.0 # tempo para a pessoa começar a falar
|
|
54
55
|
MAX_FRASE = 45.0
|
|
55
56
|
MAX_FRASE_ESTENDIDA = 100.0
|
|
@@ -97,6 +98,7 @@ class Gravador:
|
|
|
97
98
|
# VAD opcional (Silero, via pysilero-vad): bloco → True se for voz.
|
|
98
99
|
# Com ele, a frase fecha em 0,9 s de silêncio em vez de 2 s.
|
|
99
100
|
self.vad = None
|
|
101
|
+
self.espera = ESPERA_COMECAR # quanto espera a fala começar (encurta quando há frase pendente)
|
|
100
102
|
|
|
101
103
|
# O ambiente é medido ENQUANTO ele espera a pessoa começar. Medir antes de
|
|
102
104
|
# abrir o microfone daria o silêncio do processo, não o do cômodo.
|
|
@@ -142,7 +144,7 @@ class Gravador:
|
|
|
142
144
|
self._falando = True
|
|
143
145
|
self._blocos = list(self._pre)
|
|
144
146
|
self._ultimo_som = agora
|
|
145
|
-
elif agora >
|
|
147
|
+
elif agora > self.espera:
|
|
146
148
|
self.encerrou_por = "ninguem-falou"
|
|
147
149
|
return True
|
|
148
150
|
return False
|
|
@@ -228,7 +230,31 @@ class Microfone:
|
|
|
228
230
|
self._ao_receber = ao_receber
|
|
229
231
|
self._fluxo = None
|
|
230
232
|
|
|
233
|
+
def _abrir_de_arquivo(self, caminho: str) -> None:
|
|
234
|
+
"""Modo de teste (PRIMOCODE_TESTE_WAV): em vez do microfone, toca um WAV 16 kHz mono na mesma entrada,
|
|
235
|
+
depois de 12 s de silêncio (a saudação termina e o motor assenta) e com silêncio no fim."""
|
|
236
|
+
import threading
|
|
237
|
+
import time as _t
|
|
238
|
+
import numpy as _np
|
|
239
|
+
|
|
240
|
+
def alimentar():
|
|
241
|
+
# "a.wav@12;b.wav@90": cada arquivo entra N s depois do anterior (ou da abertura)
|
|
242
|
+
silencio = _np.zeros(BLOCO, dtype=_np.int16)
|
|
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)
|
|
252
|
+
threading.Thread(target=alimentar, daemon=True).start()
|
|
253
|
+
|
|
231
254
|
def abrir(self) -> None:
|
|
255
|
+
if os.environ.get("PRIMOCODE_TESTE_WAV"):
|
|
256
|
+
return self._abrir_de_arquivo(os.environ["PRIMOCODE_TESTE_WAV"])
|
|
257
|
+
|
|
232
258
|
def retorno(dados, quadros, tempo, estado): # noqa: ARG001
|
|
233
259
|
self._ao_receber(dados[:, 0].copy())
|
|
234
260
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""fala.py — a voz do Primo: Edge TTS (neural pt-BR) dentro do processo, sem subprocesso por frase.
|
|
2
|
+
|
|
3
|
+
Ordem: Edge TTS (precisa de internet) → ElevenLabs só se a pessoa pediu (`"tts": "elevenlabs"` em
|
|
4
|
+
~/.primocode/voz.json) → voz do sistema (`say`) offline. As vozes: francisca (padrão), antonio, thalita.
|
|
5
|
+
O event loop e o import do edge_tts ficam vivos na thread `Edge`; cada frase é uma tarefa nele.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import threading
|
|
11
|
+
import unicodedata
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
VOZES = {"francisca": "pt-BR-FranciscaNeural", "antonio": "pt-BR-AntonioNeural", "thalita": "pt-BR-ThalitaMultilingualNeural"}
|
|
16
|
+
ORDEM = ["francisca", "antonio", "thalita"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _s(t: str) -> str:
|
|
20
|
+
return "".join(c for c in unicodedata.normalize("NFD", t.lower()) if unicodedata.category(c) != "Mn")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def voz_pedida(texto: str, atual: str) -> Optional[str]:
|
|
24
|
+
""""muda a voz" / "troca a voz para antonio" → o id da nova voz (a próxima da lista, se não disse qual); None se não era isso."""
|
|
25
|
+
import re
|
|
26
|
+
m = re.search(r"\b(?:muda|mudar|troca|trocar|mude|troque)\s+(?:a\s+)?voz(?:\s+(?:para|pra|pro|do|da|de)\s+(?:a\s+|o\s+)?(\w+))?", _s(texto))
|
|
27
|
+
if not m:
|
|
28
|
+
return None
|
|
29
|
+
pedida = m.group(1)
|
|
30
|
+
if pedida and pedida in VOZES:
|
|
31
|
+
return pedida
|
|
32
|
+
i = ORDEM.index(atual) if atual in ORDEM else -1
|
|
33
|
+
return ORDEM[(i + 1) % len(ORDEM)]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Edge:
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.ok = False
|
|
39
|
+
self._loop: "asyncio.AbstractEventLoop | None" = None
|
|
40
|
+
self._pronto = threading.Event()
|
|
41
|
+
threading.Thread(target=self._rodar, daemon=True).start()
|
|
42
|
+
|
|
43
|
+
def _rodar(self) -> None:
|
|
44
|
+
try:
|
|
45
|
+
import edge_tts # noqa: F401
|
|
46
|
+
self.ok = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
self._pronto.set()
|
|
49
|
+
return
|
|
50
|
+
self._loop = asyncio.new_event_loop()
|
|
51
|
+
self._pronto.set()
|
|
52
|
+
self._loop.run_forever()
|
|
53
|
+
|
|
54
|
+
async def _tarefa(self, texto: str, voz: str, saida: Path) -> bool:
|
|
55
|
+
import edge_tts
|
|
56
|
+
n = 0
|
|
57
|
+
with open(saida, "wb") as f:
|
|
58
|
+
async for c in edge_tts.Communicate(texto, voz, rate="+8%").stream():
|
|
59
|
+
if c["type"] == "audio":
|
|
60
|
+
f.write(c["data"])
|
|
61
|
+
n += len(c["data"])
|
|
62
|
+
return n > 0
|
|
63
|
+
|
|
64
|
+
def sintetizar(self, texto: str, voz_id: str, saida: Path, limite: float = 15.0) -> bool:
|
|
65
|
+
self._pronto.wait(5)
|
|
66
|
+
if not self.ok or not self._loop:
|
|
67
|
+
return False
|
|
68
|
+
try:
|
|
69
|
+
fut = asyncio.run_coroutine_threadsafe(self._tarefa(texto, VOZES.get(voz_id, VOZES["francisca"]), saida), self._loop)
|
|
70
|
+
return bool(fut.result(limite))
|
|
71
|
+
except Exception:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def aquecer(self) -> None:
|
|
75
|
+
"""Uma frase curta descartada: abre DNS, TLS e o módulo antes da primeira fala de verdade."""
|
|
76
|
+
import tempfile
|
|
77
|
+
p = Path(tempfile.gettempdir()) / "primo-voz-aquece.mp3"
|
|
78
|
+
self.sintetizar("oi", "francisca", p)
|
|
79
|
+
try:
|
|
80
|
+
p.unlink()
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def hora_falada(t) -> str:
|
|
86
|
+
""""São 19 e 05." — o mesmo texto no app.py (resposta) e no motor (pré-voz do próximo minuto)."""
|
|
87
|
+
return f"São {t.tm_hour} e {t.tm_min:02d}." if t.tm_min else f"São {t.tm_hour} horas em ponto."
|
|
@@ -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
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>Primo</title>
|
|
6
|
+
<link rel="preload" as="script" href="vendor/spline-runtime.min.js">
|
|
7
|
+
<link rel="preload" as="fetch" crossorigin href="https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode">
|
|
8
|
+
<script src="vendor/spline-runtime.min.js"></script>
|
|
6
9
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
7
10
|
<style>
|
|
8
11
|
:root {
|
|
@@ -15,6 +18,10 @@
|
|
|
15
18
|
html,body { height:100%; background:var(--fundo); color:var(--texto); font-family:var(--sans); -webkit-font-smoothing:antialiased; user-select:none; -webkit-user-select:none; }
|
|
16
19
|
body { background:radial-gradient(ellipse 60% 55% at 50% 42%, #1a2a2c 0%, var(--fundo) 62%); overflow:hidden; }
|
|
17
20
|
.tela { height:100vh; padding:38px 28px 24px; display:grid; gap:18px; grid-template-columns:minmax(250px,22%) 1fr minmax(280px,25%); grid-template-rows:minmax(0,1fr) auto auto; }
|
|
21
|
+
@property --v { syntax:'<number>'; inherits:true; initial-value:0; }
|
|
22
|
+
.cartao { transition:transform .28s cubic-bezier(.2,.8,.2,1), box-shadow .28s, border-color .28s; animation:sobe .75s cubic-bezier(.2,.8,.2,1) backwards; animation-delay:calc(var(--i,0)*70ms); }
|
|
23
|
+
.cartao:hover { transform:translateY(-3px); border-color:#3a3a36; box-shadow:0 1px 0 rgba(255,255,255,.05) inset, 0 28px 44px -22px rgba(0,0,0,.9); }
|
|
24
|
+
@keyframes sobe { from { opacity:0; transform:translateY(18px); } }
|
|
18
25
|
.cartao { background:linear-gradient(180deg,var(--cartao2),var(--cartao)); border:1px solid var(--borda); border-radius:var(--raio); padding:18px 22px; box-shadow:0 1px 0 rgba(255,255,255,.04) inset, 0 20px 40px -24px rgba(0,0,0,.8); }
|
|
19
26
|
.rotulo { font-size:11px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--fraco); }
|
|
20
27
|
|
|
@@ -26,7 +33,12 @@
|
|
|
26
33
|
.cal .sem { color:var(--fraco); font-size:11px; font-weight:600; padding-bottom:3px; height:auto; }
|
|
27
34
|
.cal span { height:30px; display:grid; place-items:center; color:var(--apagado); border-radius:50%; width:30px; margin:auto; position:relative; }
|
|
28
35
|
.cal .fds { color:var(--fraco); }
|
|
29
|
-
.cal .hoje { background:var(--marca); color:#06191c; font-weight:700; }
|
|
36
|
+
.cal .hoje { background:var(--marca); color:#06191c; font-weight:700; animation:pulso-dia 2.6s ease-in-out infinite; }
|
|
37
|
+
@keyframes pulso-dia { 0%,100% { box-shadow:0 0 0 0 rgba(34,211,238,.45); } 50% { box-shadow:0 0 0 7px rgba(34,211,238,0); } }
|
|
38
|
+
.capa.troca { animation:capa-troca .8s cubic-bezier(.2,.8,.2,1); } @keyframes capa-troca { from { opacity:0; transform:rotate(-140deg) scale(.6); } }
|
|
39
|
+
@keyframes fade { from { opacity:0; } }
|
|
40
|
+
.faixa.troca { animation:fade .6s ease; }
|
|
41
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation:none !important; transition:none !important; } }
|
|
30
42
|
.cal .tem::after { content:""; width:4px; height:4px; border-radius:50%; background:var(--marca); position:absolute; bottom:1px; }
|
|
31
43
|
.cal .hoje.tem::after { background:#06191c; }
|
|
32
44
|
.hoje-card { flex:1; min-height:0; overflow:hidden; }
|
|
@@ -49,13 +61,22 @@
|
|
|
49
61
|
.nome p::before { content:""; display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--ok); margin-right:8px; box-shadow:0 0 10px var(--ok); }
|
|
50
62
|
.nome[data-e="pensando"] p::before, .nome[data-e="executando"] p::before { background:#f5b94a; box-shadow:0 0 10px #f5b94a; }
|
|
51
63
|
.nome[data-e="dormindo"] p::before { background:var(--fraco); box-shadow:none; }
|
|
64
|
+
.nome .pasta { font:12px var(--mono); color:var(--fraco); margin-top:2px; }
|
|
65
|
+
.nome .pasta::before { display:none; }
|
|
66
|
+
.b-entrar { margin-top:8px; background:var(--marca); color:#06191c; border:0; border-radius:999px; padding:8px 18px; font:600 13px var(--sans); cursor:pointer; }
|
|
52
67
|
.fala { position:absolute; top:0; left:50%; transform:translateX(-50%); max-width:70%; text-align:center; color:var(--apagado); font-size:13px; line-height:1.4; max-height:3em; overflow:hidden; }
|
|
53
68
|
/* robô 3D: canvas transparente sobre o palco; o SVG só aparece se o WebGL falhar */
|
|
54
69
|
#robo3d { width:100%; height:100%; display:none; -webkit-mask-image:linear-gradient(#000 82%,transparent); mask-image:linear-gradient(#000 82%,transparent); }
|
|
55
|
-
|
|
70
|
+
#svg-robo { display:none; }
|
|
71
|
+
.tres-d #robo3d { display:block; animation:entra .6s ease both; } .svg #svg-robo { display:block; animation:entra .6s ease both; }
|
|
72
|
+
@keyframes entra { from { opacity:0; } }
|
|
73
|
+
/* carregando: nenhum robô, só o lugar dele com um brilho ciano suave pulsando */
|
|
74
|
+
#carrega { display:none; width:min(30vh,260px); height:min(48vh,420px); border-radius:50% 50% 42% 42% / 38% 38% 62% 62%; background:radial-gradient(closest-side,rgba(34,211,238,.22),rgba(34,211,238,.07) 62%,transparent); filter:blur(14px); animation:carrega 2.2s ease-in-out infinite; pointer-events:none; }
|
|
75
|
+
.carregando #carrega { display:block; }
|
|
76
|
+
@keyframes carrega { 0%,100% { opacity:.35; transform:scale(.94); } 50% { opacity:.95; transform:scale(1.04); } }
|
|
56
77
|
/* robô do Spline (cena da comunidade): canvas transparente, brilho ciano por CSS, busto pelo zoom da câmera */
|
|
57
78
|
#robo-spline { width:100% !important; height:100% !important; visibility:hidden; background:transparent; --nv:0; -webkit-mask-image:linear-gradient(#000 44%,rgba(0,0,0,.55) 58%,transparent 76%); mask-image:linear-gradient(#000 44%,rgba(0,0,0,.55) 58%,transparent 76%); filter:drop-shadow(0 0 18px rgba(34,211,238,.22)); transition:filter .4s; }
|
|
58
|
-
.spline #robo-spline { visibility:visible; } .spline #robo3d, .spline #svg-robo { display:none; }
|
|
79
|
+
.spline #robo-spline { visibility:visible; animation:entra .6s ease both; } .spline #robo3d, .spline #svg-robo { display:none; }
|
|
59
80
|
.palco > #robo-spline { grid-area:1/1; }
|
|
60
81
|
.spline.e-ouvindo #robo-spline { filter:drop-shadow(0 0 26px rgba(34,211,238,.55)); }
|
|
61
82
|
.spline.e-pensando #robo-spline, .spline.e-executando #robo-spline { filter:drop-shadow(0 0 22px rgba(34,211,238,.38)); animation:sp-pensa 1.8s ease-in-out infinite; }
|
|
@@ -97,16 +118,16 @@
|
|
|
97
118
|
.capa::after { content:""; position:absolute; inset:44%; border-radius:50%; background:var(--fundo); box-shadow:0 0 0 2px #0b0b0a; }
|
|
98
119
|
.faixa { text-align:center; max-width:100%; } .faixa b { display:block; font-size:16px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .faixa small { color:var(--apagado); font-size:13px; }
|
|
99
120
|
.prog { width:100%; display:flex; align-items:center; gap:10px; font:12px var(--mono); color:var(--fraco); }
|
|
100
|
-
.prog .t { flex:1; height:4px; border-radius:2px; background:var(--borda); overflow:hidden; } .prog .t i { display:block; height:100%; width:0; background:var(--texto); border-radius:2px; }
|
|
121
|
+
.prog .t { flex:1; height:4px; border-radius:2px; background:var(--borda); overflow:hidden; } .prog .t i { display:block; height:100%; width:0; transition:width .3s linear; background:var(--texto); border-radius:2px; }
|
|
101
122
|
.ctrl { display:flex; gap:30px; align-items:center; }
|
|
102
123
|
.ctrl button { background:none; border:0; color:var(--texto); cursor:pointer; display:grid; place-items:center; }
|
|
103
124
|
.ctrl button:active { transform:scale(.9); }
|
|
104
125
|
.ctrl .play { width:50px; height:50px; border-radius:50%; background:var(--texto); color:var(--fundo); }
|
|
105
126
|
|
|
106
127
|
/* medidores */
|
|
107
|
-
.medidores { grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,1fr)
|
|
128
|
+
.medidores { grid-column:1 / -1; display:grid; grid-template-columns:repeat(4,1fr); gap:18px; }
|
|
108
129
|
.med { display:flex; align-items:center; gap:16px; padding:14px 20px; }
|
|
109
|
-
.anel { --v:0; width:62px; height:62px; border-radius:50%; flex:none; display:grid; place-items:center; background:conic-gradient(var(--c,var(--marca)) calc(var(--v)*1%), var(--borda) 0); position:relative; }
|
|
130
|
+
.anel { transition:--v .9s cubic-bezier(.2,.8,.2,1); --v:0; width:62px; height:62px; border-radius:50%; flex:none; display:grid; place-items:center; background:conic-gradient(var(--c,var(--marca)) calc(var(--v)*1%), var(--borda) 0); position:relative; }
|
|
110
131
|
.anel::before { content:""; position:absolute; inset:6px; border-radius:50%; background:var(--cartao); }
|
|
111
132
|
.anel span { position:relative; font:600 15px var(--sans); font-variant-numeric:tabular-nums; }
|
|
112
133
|
.med b { display:block; font-size:14px; } .med small { color:var(--apagado); font-size:12px; }
|
|
@@ -117,7 +138,7 @@
|
|
|
117
138
|
/* voz */
|
|
118
139
|
.voz { grid-column:1 / -1; display:flex; align-items:center; gap:18px; padding:12px 22px; }
|
|
119
140
|
#barras { flex:1; height:46px; display:flex; align-items:center; gap:3px; }
|
|
120
|
-
#barras i { flex:1; border-radius:3px; height:8%; background:var(--marca); transition:height .
|
|
141
|
+
#barras i { flex:1; border-radius:3px; height:8%; background:var(--marca); transition:height .12s ease-out; }
|
|
121
142
|
#barras i.p { background:var(--ok); } #barras i.z { background:var(--borda); }
|
|
122
143
|
.leg { display:flex; gap:16px; font-size:12px; color:var(--apagado); white-space:nowrap; }
|
|
123
144
|
.leg span::before { content:""; display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; background:var(--marca); }
|
|
@@ -143,6 +164,7 @@
|
|
|
143
164
|
<div class="fala" id="fala"></div>
|
|
144
165
|
<div class="palco">
|
|
145
166
|
<div class="chao"></div>
|
|
167
|
+
<div id="carrega"></div>
|
|
146
168
|
<canvas id="robo3d" aria-label="Primo"></canvas>
|
|
147
169
|
<canvas id="robo-spline" aria-label="Primo"></canvas>
|
|
148
170
|
<svg id="svg-robo" viewBox="0 0 400 520" aria-label="Primo" class="desmontado">
|
|
@@ -216,7 +238,7 @@
|
|
|
216
238
|
</g>
|
|
217
239
|
</svg>
|
|
218
240
|
</div>
|
|
219
|
-
<div class="nome" id="nome" data-e="dormindo"><h1 id="nome-h">Primo</h1><p id="estado-t">em espera</p></div>
|
|
241
|
+
<div class="nome" id="nome" data-e="dormindo"><h1 id="nome-h">Primo</h1><p id="estado-t">em espera</p><p id="pasta" class="pasta"></p><button id="b-entrar" class="b-entrar" hidden>Entrar</button></div>
|
|
220
242
|
</section>
|
|
221
243
|
|
|
222
244
|
<section class="dir">
|
|
@@ -242,10 +264,6 @@
|
|
|
242
264
|
<div class="cartao med"><div class="anel" id="a-mem" style="--c:var(--marca-clara)"><span>–</span></div><div><b>Memória</b><small id="s-mem"> </small></div></div>
|
|
243
265
|
<div class="cartao med"><div class="anel" id="a-disco" style="--c:var(--apagado)"><span>–</span></div><div><b>Disco</b><small id="s-disco"> </small></div></div>
|
|
244
266
|
<div class="cartao med"><div class="anel" id="a-bat" style="--c:var(--ok)"><span>–</span></div><div><b>Bateria</b><small id="s-bat"> </small></div></div>
|
|
245
|
-
<div class="cartao rede">
|
|
246
|
-
<div class="lin"><span>REDE</span><span id="r-txt">↓ 0 KB/s · ↑ 0 KB/s</span></div>
|
|
247
|
-
<svg viewBox="0 0 300 44" preserveAspectRatio="none"><path id="r-down" d="" fill="none" stroke="#22d3ee" stroke-width="2"/><path id="r-up" d="" fill="none" stroke="#7ad7a3" stroke-width="1.5" opacity=".85"/></svg>
|
|
248
|
-
</div>
|
|
249
267
|
</section>
|
|
250
268
|
|
|
251
269
|
<section class="cartao voz">
|
|
@@ -256,7 +274,6 @@
|
|
|
256
274
|
</main>
|
|
257
275
|
|
|
258
276
|
<script src="vendor/three-robo.min.js"></script>
|
|
259
|
-
<script src="vendor/spline-runtime.min.js"></script>
|
|
260
277
|
<script>
|
|
261
278
|
const $ = (id) => document.getElementById(id);
|
|
262
279
|
const post = (o) => { try { window.webkit.messageHandlers.primo.postMessage(JSON.stringify(o)); } catch (e) {} };
|
|
@@ -353,12 +370,13 @@ const R3 = (function () {
|
|
|
353
370
|
/* ── robô do Spline (runtime local, cena na rede). Falhou ou passou de 6 s: fica o RobotExpressive ── */
|
|
354
371
|
const SP = (function () {
|
|
355
372
|
const CENA = 'https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode';
|
|
356
|
-
const X = { ok: false, tentou: false, app: null, bot: null };
|
|
373
|
+
const X = { ok: false, tentou: false, app: null, bot: null, falhou: false };
|
|
357
374
|
X.iniciar = () => {
|
|
358
375
|
if (X.tentou || !window.SplineApp) return; X.tentou = true;
|
|
359
376
|
let acabou = false;
|
|
360
|
-
const cai = (why) => { if (acabou) return; acabou = true; try { X.app && X.app.stop(); } catch (e) {} X.ok = false; post({ acao: 'erro', arg: 'spline: ' + why + ' (fallback RobotExpressive)' }); classeTela(); };
|
|
377
|
+
const cai = (why) => { if (acabou) return; acabou = true; try { X.app && X.app.stop(); } catch (e) {} X.ok = false; X.falhou = true; post({ acao: 'erro', arg: 'spline: ' + why + ' (fallback RobotExpressive)' }); classeTela(); };
|
|
361
378
|
const tempo = setTimeout(() => cai('mais de 6 s'), 6000);
|
|
379
|
+
X.cancela = () => { acabou = true; clearTimeout(tempo); X.falhou = true; try { X.app && X.app.stop(); } catch (e) {} };
|
|
362
380
|
try {
|
|
363
381
|
const app = new window.SplineApp($('robo-spline')); X.app = app;
|
|
364
382
|
app.load(CENA).then(() => {
|
|
@@ -372,6 +390,7 @@ const SP = (function () {
|
|
|
372
390
|
} catch (e) { clearTimeout(tempo); cai(String(e)); }
|
|
373
391
|
};
|
|
374
392
|
// leve: janela oculta/minimizada = para o render
|
|
393
|
+
setTimeout(() => X.iniciar(), 0); // já na carga da página, antes de a ponte do Python falar
|
|
375
394
|
document.addEventListener('visibilitychange', () => { if (!X.app || !X.ok) return; if (document.hidden) X.app.stop(); else X.app.play(); });
|
|
376
395
|
// falando: o robô respira no ritmo da voz (escala do objeto "Bot" da cena) + brilho ciano pelo CSS (--nv)
|
|
377
396
|
X.quadro = (agora) => {
|
|
@@ -427,7 +446,7 @@ function desenharHoje() {
|
|
|
427
446
|
function desenharClima(c) {
|
|
428
447
|
if (!c || c.temp === undefined) return;
|
|
429
448
|
$('c-cidade').textContent = c.cidade || '';
|
|
430
|
-
$('c-temp')
|
|
449
|
+
rolar($('c-temp'), c.temp, '°');
|
|
431
450
|
$('c-cond').textContent = c.icone + ' ' + c.cond;
|
|
432
451
|
$('c-mm').textContent = 'Máx. ' + c.max + '° · Mín. ' + c.min + '°';
|
|
433
452
|
$('c-dias').innerHTML = (c.dias || []).slice(0, 4).map((d) => {
|
|
@@ -444,6 +463,7 @@ $('m-play').innerHTML = ICO_PLAY;
|
|
|
444
463
|
function desenharMusica(m) {
|
|
445
464
|
S.musica = m && m.faixa ? m : null; S.musicaT = Date.now();
|
|
446
465
|
if (!S.musica) { $('m-faixa').textContent = 'Nada tocando'; $('m-artista').textContent = 'abra o Spotify ou a Música'; $('disco').classList.remove('toca'); $('capa').style.backgroundImage = ''; $('m-dur').textContent = '0:00'; $('m-play').innerHTML = ICO_PLAY; return; }
|
|
466
|
+
if (m.faixa !== S.faixaAnt) { S.faixaAnt = m.faixa; for (const e of [$('capa'), document.querySelector('.faixa')]) { e.classList.remove('troca'); void e.offsetWidth; e.classList.add('troca'); } }
|
|
447
467
|
$('m-faixa').textContent = m.faixa; $('m-artista').textContent = m.artista; $('m-dur').textContent = mmss(m.dur);
|
|
448
468
|
const tocando = m.estado === 'playing';
|
|
449
469
|
$('disco').classList.toggle('toca', tocando);
|
|
@@ -469,17 +489,23 @@ document.querySelectorAll('.ctrl button').forEach((b) => b.onclick = (e) => {
|
|
|
469
489
|
const fmtBytes = (b) => b >= 1e6 ? (b / 1e6).toFixed(1) + ' MB/s' : Math.round(b / 1e3) + ' KB/s';
|
|
470
490
|
const hist = { down: Array(60).fill(0), up: Array(60).fill(0) };
|
|
471
491
|
function caminho(a, max) { return a.map((v, i) => (i ? 'L' : 'M') + (i * 300 / (a.length - 1)).toFixed(1) + ' ' + (40 - 36 * Math.min(1, v / max)).toFixed(1)).join(' '); }
|
|
472
|
-
|
|
492
|
+
const RM = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
493
|
+
/* número que rola até o valor (60 fps, ease-out); sem animação se a pessoa pediu menos movimento */
|
|
494
|
+
function rolar(el, alvo, suf) {
|
|
495
|
+
suf = suf || ''; alvo = Number(alvo); const de = el._n === undefined ? 0 : el._n; el._n = alvo;
|
|
496
|
+
cancelAnimationFrame(el._r);
|
|
497
|
+
if (RM || de === alvo || isNaN(alvo)) { el.textContent = alvo + suf; return; }
|
|
498
|
+
const t0 = performance.now();
|
|
499
|
+
const f = (t) => { const k = Math.min(1, (t - t0) / 800), e = 1 - Math.pow(1 - k, 3); el.textContent = Math.round(de + (alvo - de) * e) + suf; if (k < 1) el._r = requestAnimationFrame(f); };
|
|
500
|
+
el._r = requestAnimationFrame(f);
|
|
501
|
+
}
|
|
502
|
+
function anel(id, pct, texto, cor) { const e = $(id); e.style.setProperty('--v', Math.max(0, Math.min(100, pct))); const m = String(texto).match(/^(-?\d+)(.*)$/); if (m) rolar(e.firstChild, m[1], m[2]); else e.firstChild.textContent = texto; if (cor) e.style.setProperty('--c', cor); }
|
|
473
503
|
function desenharMaquina(m) {
|
|
474
504
|
anel('a-cpu', m.cpu, m.cpu + '%'); $('s-cpu').textContent = m.nucleos + ' núcleos';
|
|
475
505
|
if (m.mem && m.mem.total_gb) { anel('a-mem', m.mem.pct, m.mem.pct + '%'); $('s-mem').textContent = String(m.mem.usada_gb).replace('.', ',') + ' de ' + m.mem.total_gb + ' GB'; }
|
|
476
506
|
if (m.disco && m.disco.livre_gb !== undefined) { anel('a-disco', m.disco.pct, m.disco.pct + '%'); $('s-disco').textContent = m.disco.livre_gb + ' GB livres'; }
|
|
477
507
|
if (m.bat && m.bat.pct !== undefined) { anel('a-bat', m.bat.pct, String(m.bat.pct), m.bat.pct <= 20 && !m.bat.tomada ? 'var(--erro)' : 'var(--ok)'); $('s-bat').textContent = m.bat.tomada ? 'na tomada' : 'na bateria'; }
|
|
478
508
|
else { $('s-bat').textContent = 'sem bateria'; }
|
|
479
|
-
hist.down.push(m.down || 0); hist.down.shift(); hist.up.push(m.up || 0); hist.up.shift();
|
|
480
|
-
const mx = Math.max(2e4, ...hist.down, ...hist.up);
|
|
481
|
-
$('r-down').setAttribute('d', caminho(hist.down, mx)); $('r-up').setAttribute('d', caminho(hist.up, mx));
|
|
482
|
-
$('r-txt').textContent = '↓ ' + fmtBytes(m.down || 0) + ' · ↑ ' + fmtBytes(m.up || 0);
|
|
483
509
|
}
|
|
484
510
|
|
|
485
511
|
/* ── barras da voz: o histórico rola; ciano = a pessoa falou, verde = o Primo falou ── */
|
|
@@ -495,7 +521,11 @@ function empurraBarra() {
|
|
|
495
521
|
}
|
|
496
522
|
|
|
497
523
|
/* ── o robô ── */
|
|
498
|
-
function classeTela() {
|
|
524
|
+
function classeTela() {
|
|
525
|
+
// enquanto o Spline carrega: só o brilho (nada de robô). O fallback só entra se o Spline falhou de verdade.
|
|
526
|
+
const f = SP.ok ? ' spline' : !SP.falhou ? ' carregando' : R3.ok ? ' tres-d' : ' svg';
|
|
527
|
+
$('tela').className = 'tela e-' + S.estado + f;
|
|
528
|
+
}
|
|
499
529
|
function setEstado(e) {
|
|
500
530
|
if (!e || e === S.estado) return;
|
|
501
531
|
S.estado = e; classeTela(); $('nome').dataset.e = e;
|
|
@@ -506,6 +536,7 @@ function setEstado(e) {
|
|
|
506
536
|
$('foco').style.opacity = e === 'falando' ? 0 : .9;
|
|
507
537
|
}
|
|
508
538
|
$('centro').onclick = () => post({ acao: 'ptt' });
|
|
539
|
+
$('b-entrar').onclick = (e) => { e.stopPropagation(); post({ acao: 'entrar' }); };
|
|
509
540
|
|
|
510
541
|
/* simples: o visor vira equalizador de 13 barras quando fala; olha de lado quando pensa */
|
|
511
542
|
const EQ = []; (function () { const g = $('eq'); for (let i = 0; i < 13; i++) { const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); r.setAttribute('x', 136 + i * 9.8); r.setAttribute('width', 6); r.setAttribute('rx', 3); r.setAttribute('fill', '#a5f3fc'); g.appendChild(r); EQ.push(r); } })();
|
|
@@ -525,21 +556,24 @@ function laco(t) { SP.quadro(t); if (!R3.ok && !SP.ok) animaEq(t); requestAnimat
|
|
|
525
556
|
|
|
526
557
|
/* ── a ponte com o Python ── */
|
|
527
558
|
window.primo = (m) => {
|
|
559
|
+
if (m.pasta !== undefined) $('pasta').textContent = m.pasta;
|
|
560
|
+
if (m.login !== undefined) $('b-entrar').hidden = !!m.login;
|
|
528
561
|
if (m.nome) { S.nome = m.nome; $('nome-h').textContent = m.nome; $('leg-nome').textContent = m.nome; if (S.estado === 'dormindo') $('estado-t').textContent = 'diga "' + m.nome + '"'; }
|
|
529
|
-
if (m.robo === '
|
|
562
|
+
if (m.robo === 'expressive' && !SP.ok) { if (SP.cancela) SP.cancela(); SP.falhou = true; classeTela(); }
|
|
530
563
|
if (m.evento) R3.evento(m.evento);
|
|
531
564
|
if (m.boot) montar();
|
|
532
565
|
if (m.estado) setEstado(m.estado);
|
|
533
566
|
if (m.barras) { S.quem = m.barras.quem; S.nivel = m.barras.nivel; R3.nivelFala(m.barras.quem === 'primo' ? m.barras.nivel : 0); }
|
|
534
567
|
if (m.transcricao !== undefined || m.resposta !== undefined) { const t = m.resposta || m.transcricao || ''; $('fala').textContent = t.length > 160 ? t.slice(0, 157) + '…' : t; }
|
|
535
|
-
if (m.calendario) { S.perm = m.calendario.permissao; diasComEvento = m.calendario.dias || []; desenharCalendario(); desenharHoje(); }
|
|
536
|
-
if (m.hoje) { S.hoje = m.hoje; desenharHoje(); }
|
|
568
|
+
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(); } }
|
|
569
|
+
if (m.hoje) { const k = JSON.stringify(m.hoje); S.perm = 'ok'; if (k !== S.hojeKey) { S.hojeKey = k; S.hoje = m.hoje; desenharHoje(); } }
|
|
537
570
|
if (m.clima) desenharClima(m.clima);
|
|
538
571
|
if (m.musica !== undefined) desenharMusica(m.musica);
|
|
539
572
|
if (m.maquina) desenharMaquina(m.maquina);
|
|
540
573
|
};
|
|
541
574
|
|
|
542
|
-
|
|
575
|
+
document.querySelectorAll('.cartao').forEach((c, i) => c.style.setProperty('--i', i));
|
|
576
|
+
viraDia(); desenharHoje(); setEstado('dormindo'); classeTela();
|
|
543
577
|
setInterval(() => { viraDia(); andarMusica(); }, 250);
|
|
544
578
|
setInterval(empurraBarra, 50);
|
|
545
579
|
setTimeout(pisca, 3000);
|