File size: 6,753 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# openTARS — nucleo/voz.py
# Voz (3.0): falar as respostas, ouvir "TARS", instalar o que falta.
# A parte pesada (microfone, Whisper, Piper) fica em tars_voz.py.
#
# Parte do núcleo: o tars.py executa este arquivo DENTRO do próprio
# namespace (ver _carregar_nucleo), então tudo aqui é tars.<nome> e as
# partes enxergam umas às outras sem import. Os testes que trocam
# tars.<função> por uma falsa continuam valendo pra todas as partes.
# flake8: noqa

# ============================================================
# VOZ
# ============================================================

_falador_voz = {"f": None}


def config_voz():
    """{'falar': bool, 'ativacao': bool} salvos no config.json."""
    dados = i18n.ler_config()
    return {"falar": bool(dados.get("voz_falar")), "ativacao": bool(dados.get("voz_ativacao"))}


def salvar_config_voz(**mudancas):
    i18n.salvar_config(**{f"voz_{k}": bool(v) for k, v in mudancas.items()})


def voz_no_idioma():
    """5.0: a voz existe em inglês e em português."""
    return voz_lib.tem_voz(i18n.idioma_atual())


def o_que_falta_voz():
    if not voz_no_idioma():
        return ["idioma"]
    return voz_lib.o_que_falta(i18n.idioma_atual())


def descrever_falta_voz(falta):
    """['piper-tts', 'gravador'] -> texto no idioma da pessoa."""
    return plataforma.dica_instalar(", ".join(t(f"voz.falta_{item.replace('-', '_')}") for item in falta))


def falador():
    if _falador_voz["f"] is None:
        _falador_voz["f"] = voz_lib.Falador()
    aplicar_preferencias_voz()
    return _falador_voz["f"]


# ------------------------------------------------------------
# 4.1: voz escolhida e velocidade
# ------------------------------------------------------------

def preferencias_voz():
    """{'vozes': {'pt_BR': 'cadu'}, 'velocidade': 1.0} salvos no config.json."""
    dados = i18n.ler_config()
    vozes = {idioma: dados.get(f"voz_nome_{idioma}") for idioma in voz_lib.VOZES_DISPONIVEIS
             if dados.get(f"voz_nome_{idioma}")}
    try:
        velocidade = float(dados.get("voz_velocidade") or 1.0)
    except (TypeError, ValueError):
        velocidade = 1.0
    return {"vozes": vozes, "velocidade": velocidade}


def aplicar_preferencias_voz():
    p = preferencias_voz()
    voz_lib.configurar(vozes=p["vozes"], velocidade=p["velocidade"], idioma=i18n.idioma_atual())


def voz_atual(idioma=None):
    return voz_lib.nome_da_voz(idioma or i18n.idioma_atual())


def vocabulario_voz():
    """Nomes dos apps instalados: o Whisper erra menos neles ("abre o Steam",
    "fecha o Discord")."""
    try:
        nomes = [e.get("nome") or "" for e in desktop_entries()]
    except Exception:
        nomes = []
    curtos = {n.strip() for n in nomes if 2 <= len(n.strip()) <= 24 and len(n.split()) <= 3}
    return sorted(curtos, key=lambda n: (len(n), n.lower()))[:40]


def escolher_voz(nome, avisar=None):
    """Troca a voz do idioma atual (baixa se ainda não tiver). (ok, mensagem)"""
    idioma = i18n.idioma_atual() if i18n.idioma_atual() in voz_lib.VOZES_DISPONIVEIS else "en"
    disponiveis = voz_lib.VOZES_DISPONIVEIS.get(idioma, {})
    nome = (nome or "").strip().lower()
    if nome not in disponiveis:
        return False, t("voz.nome_invalido", nome=nome, lista=", ".join(disponiveis))
    if not voz_lib.kokoro_baixado():
        ok, erro = voz_lib.baixar_voz(idioma, avisar or (lambda msg: None), nome=nome)
        if not ok:
            return False, t("voz.erro_instalar", erro=erro)
    i18n.salvar_config(**{f"voz_nome_{idioma}": nome})
    aplicar_preferencias_voz()
    return True, t("voz.escolhida", nome=nome.capitalize())


def definir_velocidade_voz(valor):
    try:
        v = float(str(valor).replace(",", "."))
    except ValueError:
        return False, t("voz.velocidade_invalida", valor=valor)
    v = min(voz_lib.VELOCIDADE_MAX, max(voz_lib.VELOCIDADE_MIN, v))
    i18n.salvar_config(voz_velocidade=round(v, 2))
    aplicar_preferencias_voz()
    return True, t("voz.velocidade_ok", valor=f"{v:.2f}")


def frase_de_teste_voz():
    return t("voz.frase_teste", nome=voz_atual().capitalize())


def falar_resposta(texto, forcar=False, ao_terminar=None):
    """Lê a resposta em voz alta se a pessoa ligou "responder falando".
    ao_terminar: chamado quando a fala acaba (pra ouvir a continuação)."""
    if not texto or texto == RESPOSTA_INTERROMPIDA:
        return False
    if not (forcar or config_voz()["falar"]) or o_que_falta_voz():
        return False
    return falador().falar(texto, i18n.idioma_atual(), ao_terminar=ao_terminar)


def instalar_voz(avisar=None, progresso=None):
    avisar = avisar or (lambda msg: print(c(f"[openTARS] {msg}", Cor.CINZA)))
    faltava = o_que_falta_voz()
    duras = [x for x in faltava if x in ("gravador", "tocador")]
    if duras:
        return False, t("voz.precisa_sistema", itens=descrever_falta_voz(duras))
    ok, erro = voz_lib.instalar(i18n.idioma_atual(), avisar, progresso=progresso)
    return ok, ("" if ok else erro)


def modo_voz_terminal():
    """opentars --voz: fica ouvindo "TARS" e responde falando."""
    falta = o_que_falta_voz()
    if falta:
        print(c(t("voz.falta", itens=descrever_falta_voz(falta)), Cor.AMARELO))
        return 1
    pedidos = []
    trava = threading.Event()

    def ao_pedido(texto):
        pedidos.append(texto)
        trava.set()

    def ao_estado(estado):
        if estado == "chamado":
            print(c(f"[openTARS] {t('voz.chamado')}", Cor.CIANO))
        elif estado == "conversa":
            print(c(f"[openTARS] {t('gui.voz_conversa')}", Cor.CINZA))
        elif estado == "carregando":
            print(c(f"[openTARS] {t('gui.voz_carregando')}", Cor.CINZA))
        elif estado.startswith("erro:"):
            print(c(f"[openTARS] {t('voz.erro_microfone', erro=estado[5:])}", Cor.VERMELHO))
            trava.set()

    ouvinte = voz_lib.Ouvinte(ao_pedido, i18n.idioma_atual, modo="ativacao", ao_estado=ao_estado,
                              falador=falador(), vocabulario=vocabulario_voz)
    ouvinte.iniciar()
    print(c(t("voz.ouvindo"), Cor.CIANO))
    try:
        while ouvinte.ativo:
            if not trava.wait(0.5):
                continue
            trava.clear()
            while pedidos:
                texto = pedidos.pop(0)
                print(c(t("term.voce"), Cor.NEGRITO + Cor.VERDE) + f" ({t('voz.por_voz')}) {texto}")
                with TRAVA_PEDIDO:
                    resposta = processar_mensagem(texto)
                if not falar_resposta(resposta, forcar=True, ao_terminar=ouvinte.abrir_conversa):
                    ouvinte.abrir_conversa()
    except KeyboardInterrupt:
        print()
    finally:
        ouvinte.parar()
        falador().parar()
    return 0