hiapp 0.0.1__py3-none-any.whl
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.
- hiapp/__init__.py +17 -0
- hiapp/addons/__init__.py +0 -0
- hiapp/addons/base.py +60 -0
- hiapp/addons/ppsspp/__init__.py +0 -0
- hiapp/addons/ppsspp/addon.py +20 -0
- hiapp/addons/ppsspp/paths.py +21 -0
- hiapp/addons/ppsspp/routes.py +69 -0
- hiapp/config_cli.py +210 -0
- hiapp/core/__init__.py +0 -0
- hiapp/core/commands.py +138 -0
- hiapp/core/config.py +71 -0
- hiapp/core/downloader.py +158 -0
- hiapp/core/helpers.py +59 -0
- hiapp/core/http.py +22 -0
- hiapp/core/i18n.py +163 -0
- hiapp/main.py +92 -0
- hiapp/server/__init__.py +0 -0
- hiapp/server/app.py +108 -0
- hiapp/server/config.py +152 -0
- hiapp/server/routes/__init__.py +28 -0
- hiapp/server/routes/api.py +57 -0
- hiapp/server/routes/main.py +40 -0
- hiapp/voice/__init__.py +0 -0
- hiapp/voice/assistant.py +228 -0
- hiapp/voice/controller.py +282 -0
- hiapp-0.0.1.dist-info/METADATA +201 -0
- hiapp-0.0.1.dist-info/RECORD +32 -0
- hiapp-0.0.1.dist-info/WHEEL +5 -0
- hiapp-0.0.1.dist-info/entry_points.txt +5 -0
- hiapp-0.0.1.dist-info/licenses/LICENSE +176 -0
- hiapp-0.0.1.dist-info/licenses/LICENSE_ES +77 -0
- hiapp-0.0.1.dist-info/top_level.txt +1 -0
hiapp/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hiapp: H.ogar con I.nteligencia A.rtificial P.rivada P.ersonal.
|
|
3
|
+
|
|
4
|
+
Asistente de voz autogestionado para el hogar, con servidor de juegos
|
|
5
|
+
para PPSSPP y otras aplicaciones.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = version("hiapp")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
# Para estado en desarrollo, sin pip install
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
|
15
|
+
|
|
16
|
+
__author__ = "Dixan Pupo Morales (Triplex3x-cyver)"
|
|
17
|
+
__license__ = "Apache-2.0"
|
hiapp/addons/__init__.py
ADDED
|
File without changes
|
hiapp/addons/base.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clase base para todos los add-ons de Hiapp.
|
|
3
|
+
|
|
4
|
+
Un add-on es una funcionalidad opcional que se registra en el núcleo.
|
|
5
|
+
Puede ser gratuito (viene con Hiapp) o PRO (instalado desde el servidor de licencias).
|
|
6
|
+
|
|
7
|
+
Cada add-on debe:
|
|
8
|
+
- Definir `name` (identificador único).
|
|
9
|
+
- Definir `prefix` (prefijo de URL si expone rutas Flask).
|
|
10
|
+
- Definir `free` (True si es gratuito, False si es PRO).
|
|
11
|
+
- Implementar `register(app)` si expone rutas HTTP.
|
|
12
|
+
- Opcionalmente sobrescribir `start()` y `stop()` si gestiona recursos.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Addon:
|
|
17
|
+
"""Clase base para todos los add-ons de Hiapp."""
|
|
18
|
+
|
|
19
|
+
# --- Atributos que cada subclase debe sobrescribir ---
|
|
20
|
+
name: str = "unnamed"
|
|
21
|
+
prefix: str = "" # Prefijo de URL (ej. "/game")
|
|
22
|
+
free: bool = True
|
|
23
|
+
|
|
24
|
+
# --- Ciclo de vida ---
|
|
25
|
+
|
|
26
|
+
def register(self, app):
|
|
27
|
+
"""
|
|
28
|
+
Registra los blueprints y rutas del add-on en la app Flask.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
app: Instancia de Flask donde registrar el add-on.
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def start(self):
|
|
36
|
+
"""Inicia recursos propios (hilos, procesos). Opcional."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def stop(self):
|
|
40
|
+
"""Detiene recursos propios. Opcional."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# --- Estado ---
|
|
44
|
+
|
|
45
|
+
def status(self) -> dict:
|
|
46
|
+
"""
|
|
47
|
+
Devuelve un diccionario con el estado actual del add-on.
|
|
48
|
+
Las subclases pueden sobrescribirlo para dar más información.
|
|
49
|
+
"""
|
|
50
|
+
return {
|
|
51
|
+
"name": self.name,
|
|
52
|
+
"prefix": self.prefix,
|
|
53
|
+
"free": self.free,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# --- Utilidades ---
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
kind = "free" if self.free else "PRO"
|
|
60
|
+
return f"<Addon {self.name} ({kind}) prefix={self.prefix!r}>"
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Add-on gratuito: servidor de juegos para PPSSPP.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# ..
|
|
6
|
+
from ..base import Addon
|
|
7
|
+
# .
|
|
8
|
+
from .routes import bp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PpssppAddon(Addon):
|
|
12
|
+
"""Servidor HTTP de streaming para PPSSPP (soporta rangos)."""
|
|
13
|
+
|
|
14
|
+
name = "ppsspp"
|
|
15
|
+
prefix = "/game"
|
|
16
|
+
free = True
|
|
17
|
+
|
|
18
|
+
def register(self, app):
|
|
19
|
+
"""Registra el blueprint /game en la app Flask."""
|
|
20
|
+
app.register_blueprint(bp)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
# (...core)
|
|
4
|
+
from ...core.config import load_config, save_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_games_directory() -> str:
|
|
8
|
+
"""Devuelve el directorio de juegos configurado, creándolo si no existe."""
|
|
9
|
+
config = load_config()
|
|
10
|
+
games_dir = config.get("games_dir", str(Path.home() / "PSP"))
|
|
11
|
+
|
|
12
|
+
# Crear si no existe
|
|
13
|
+
Path(games_dir).mkdir(parents=True, exist_ok=True)
|
|
14
|
+
return games_dir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save_games_directory(path: str) -> None:
|
|
18
|
+
"""Guarda el directorio de juegos en la configuración."""
|
|
19
|
+
config = load_config()
|
|
20
|
+
config["games_dir"] = str(path)
|
|
21
|
+
save_config(config)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rutas HTTP del add-on PPSSPP.
|
|
3
|
+
|
|
4
|
+
Se registran bajo el prefijo /game:
|
|
5
|
+
/game/ → index.txt con la lista de juegos
|
|
6
|
+
/game/<filename> → streaming del archivo (soporta rangos)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from flask import Blueprint, Response, abort, send_file
|
|
12
|
+
|
|
13
|
+
# (.)
|
|
14
|
+
from .paths import get_games_directory
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
bp = Blueprint(
|
|
18
|
+
"ppsspp",
|
|
19
|
+
__name__,
|
|
20
|
+
url_prefix="/game",
|
|
21
|
+
template_folder="templates",
|
|
22
|
+
static_folder="static",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@bp.route("/", methods=["GET"])
|
|
27
|
+
def index():
|
|
28
|
+
"""
|
|
29
|
+
Devuelve la lista de juegos disponibles en formato index.txt.
|
|
30
|
+
|
|
31
|
+
Cada línea debe empezar con '/' (PPSSPP lo requiere).
|
|
32
|
+
Formato de ejemplo:
|
|
33
|
+
/dissidia.iso
|
|
34
|
+
/god-of-war.cso
|
|
35
|
+
"""
|
|
36
|
+
games_dir = Path(get_games_directory())
|
|
37
|
+
lines = []
|
|
38
|
+
|
|
39
|
+
if games_dir.is_dir():
|
|
40
|
+
for entry in sorted(games_dir.iterdir()):
|
|
41
|
+
if entry.is_file() and entry.suffix.lower() in {".iso", ".cso"}:
|
|
42
|
+
lines.append(f"/{entry.name}")
|
|
43
|
+
|
|
44
|
+
content = "\n".join(lines) + "\n" if lines else ""
|
|
45
|
+
return Response(content, mimetype="text/plain")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@bp.route("/<path:filename>", methods=["GET"])
|
|
49
|
+
def stream_game(filename):
|
|
50
|
+
"""
|
|
51
|
+
Sirve un archivo de juego con soporte de peticiones de rango.
|
|
52
|
+
PPSSPP requiere respuestas 206 Partial Content.
|
|
53
|
+
"""
|
|
54
|
+
games_dir = Path(get_games_directory()).resolve()
|
|
55
|
+
file_path = (games_dir / filename).resolve()
|
|
56
|
+
|
|
57
|
+
# Seguridad: evitar path traversal (../../etc/passwd)
|
|
58
|
+
if not str(file_path).startswith(str(games_dir)):
|
|
59
|
+
abort(403)
|
|
60
|
+
|
|
61
|
+
if not file_path.is_file():
|
|
62
|
+
abort(404)
|
|
63
|
+
|
|
64
|
+
# conditional=True habilita el manejo de cabeceras Range
|
|
65
|
+
return send_file(
|
|
66
|
+
file_path,
|
|
67
|
+
conditional=True,
|
|
68
|
+
mimetype="application/octet-stream",
|
|
69
|
+
)
|
hiapp/config_cli.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Comando `hiapp-config`: gestiona la configuración de Hiapp.
|
|
4
|
+
|
|
5
|
+
Reglas:
|
|
6
|
+
- USER_DEFAULTS se pueden leer, escribir y resetear.
|
|
7
|
+
- APP_DEFAULTS solo se leen. Para cambiar lang/lang_model_name,
|
|
8
|
+
se usa el flujo de reconfiguración (reset lang_model_name + reinicio).
|
|
9
|
+
- `reset --all` solo afecta a USER_DEFAULTS.
|
|
10
|
+
|
|
11
|
+
Modo interactivo:
|
|
12
|
+
hiapp-config list
|
|
13
|
+
hiapp-config get port
|
|
14
|
+
hiapp-config set port 8080
|
|
15
|
+
hiapp-config get port
|
|
16
|
+
hiapp-config reset port
|
|
17
|
+
hiapp-config get port
|
|
18
|
+
|
|
19
|
+
hiapp-config get models_dir
|
|
20
|
+
hiapp-config set models_dir /storage/XXXX-XXXX/models
|
|
21
|
+
hiapp-config get models_dir
|
|
22
|
+
hiapp-config reset models_dir
|
|
23
|
+
hiapp-config get models_dir
|
|
24
|
+
hiapp-config list
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# 🔧 Configuración de Hiapp
|
|
28
|
+
# ------------------------------
|
|
29
|
+
import sys
|
|
30
|
+
import argparse
|
|
31
|
+
|
|
32
|
+
from .core.config import (
|
|
33
|
+
ALL_DEFAULTS,
|
|
34
|
+
APP_DEFAULTS,
|
|
35
|
+
USER_DEFAULTS,
|
|
36
|
+
load_config,
|
|
37
|
+
save_config,
|
|
38
|
+
)
|
|
39
|
+
from .core.i18n import translate
|
|
40
|
+
|
|
41
|
+
# Traducir guiones (CLI) a guiones bajos (interno)
|
|
42
|
+
CLI_TO_INTERNAL = {k.replace("_", "-"): k for k in ALL_DEFAULTS}
|
|
43
|
+
config = load_config()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_key(key: str) -> str:
|
|
47
|
+
"""Convierte 'models-dir' → 'models_dir'. Deja pasar claves ya válidas.
|
|
48
|
+
Si entra models_dir, pasa sin cambiar.
|
|
49
|
+
"""
|
|
50
|
+
return CLI_TO_INTERNAL.get(key, key)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_app_key(key: str) -> bool:
|
|
54
|
+
"""¿Es una clave gestionada por la app (no libremente editable)?"""
|
|
55
|
+
return key in APP_DEFAULTS
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- Comandos -----------
|
|
59
|
+
def cmd_list(args):
|
|
60
|
+
print(translate("config_title", config.get("lang")))
|
|
61
|
+
print("=" * 40)
|
|
62
|
+
for key, value in config.items():
|
|
63
|
+
marca = " [app]" if _is_app_key(key) else ""
|
|
64
|
+
print(f" {key:18} = {value}{marca}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_get(args):
|
|
68
|
+
key = _resolve_key(args.key)
|
|
69
|
+
if key not in ALL_DEFAULTS:
|
|
70
|
+
print(translate("config_unknown_key", config.get("lang"), text=key))
|
|
71
|
+
print(translate(
|
|
72
|
+
"config_valid_keys", config.get("lang"), valid=", ".join(ALL_DEFAULTS.keys())))
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
print(config.get(key))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_set(args):
|
|
79
|
+
# global config
|
|
80
|
+
key = _resolve_key(args.key)
|
|
81
|
+
|
|
82
|
+
if key not in USER_DEFAULTS:
|
|
83
|
+
if _is_app_key(key):
|
|
84
|
+
print(translate("config_applang_warn", config.get("lang"), text=key))
|
|
85
|
+
else:
|
|
86
|
+
print(translate("config_unknown_key", config.get("lang"), text=key))
|
|
87
|
+
print(translate(
|
|
88
|
+
"config_valid_keys", config.get("lang"), valid=", ".join(ALL_DEFAULTS.keys())))
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
# Unir todos los argumentos en un solo string
|
|
92
|
+
raw_value = " ".join(args.value)
|
|
93
|
+
|
|
94
|
+
# Convertir tipo si el valor por defecto es int
|
|
95
|
+
if isinstance(USER_DEFAULTS[key], int):
|
|
96
|
+
try:
|
|
97
|
+
value = int(raw_value)
|
|
98
|
+
except ValueError:
|
|
99
|
+
print(translate(
|
|
100
|
+
"config_nan_err", config.get("lang"), value=raw_value, text=key))
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
elif isinstance(USER_DEFAULTS[key], list):
|
|
104
|
+
# Para listas (aliases), separar por comas
|
|
105
|
+
value = [v.strip() for v in raw_value.split(",") if v.strip()]
|
|
106
|
+
else:
|
|
107
|
+
value = raw_value
|
|
108
|
+
|
|
109
|
+
config[key] = value
|
|
110
|
+
save_config(config)
|
|
111
|
+
print(f"✅ {key} = {value}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cmd_reset(args):
|
|
115
|
+
# global config
|
|
116
|
+
|
|
117
|
+
# Caso especial: reset --all
|
|
118
|
+
if args.all:
|
|
119
|
+
#
|
|
120
|
+
# Solo resetear las configurables
|
|
121
|
+
for key in USER_DEFAULTS:
|
|
122
|
+
config[key] = USER_DEFAULTS[key]
|
|
123
|
+
save_config(config)
|
|
124
|
+
print(translate("config_reset_ok", config.get("lang")))
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
if not args.key:
|
|
128
|
+
print(translate("config_nokey_err", config.get("lang")))
|
|
129
|
+
print(translate(
|
|
130
|
+
"config_valid_keys", config.get("lang"),
|
|
131
|
+
valid=", ".join(ALL_DEFAULTS.keys()) + ", --all"))
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
key = _resolve_key(args.key)
|
|
135
|
+
|
|
136
|
+
# RESET LANG
|
|
137
|
+
if key == "lang":
|
|
138
|
+
print(translate("config_applang_warn", config.get("lang"), text=key))
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
#
|
|
141
|
+
# Claves de APP (excepto lang, ya bloqueada arriba)
|
|
142
|
+
if _is_app_key(key):
|
|
143
|
+
print(translate("config_appstat_warn", config.get("lang"), text=key))
|
|
144
|
+
confirm = input("¿Continuar? [s/N]: ").strip().lower()
|
|
145
|
+
if confirm != "s":
|
|
146
|
+
print(translate("canceled", config.get("lang")))
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
config[key] = APP_DEFAULTS[key]
|
|
150
|
+
save_config(config)
|
|
151
|
+
print(f"✅ {key} = {config[key]}")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if key not in ALL_DEFAULTS:
|
|
155
|
+
print(translate("config_unknown_key", config.get("lang"), text=key))
|
|
156
|
+
print(translate(
|
|
157
|
+
"config_valid_keys", config.get("lang"),
|
|
158
|
+
valid=", ".join(ALL_DEFAULTS.keys()) + ", --all"))
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
# Ejecutar reset en clave de usuario
|
|
162
|
+
config[key] = USER_DEFAULTS[key]
|
|
163
|
+
save_config(config)
|
|
164
|
+
print(f"✅ {key} = {USER_DEFAULTS[key]}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --- Punto de entrada -----------
|
|
168
|
+
def main():
|
|
169
|
+
parser = argparse.ArgumentParser(
|
|
170
|
+
prog="hiapp-config",
|
|
171
|
+
description=translate("cmd_parser_desc", config.get("lang"))
|
|
172
|
+
)
|
|
173
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
174
|
+
|
|
175
|
+
subparsers.add_parser("list", help=translate("cmd_list_help", config.get("lang")))
|
|
176
|
+
|
|
177
|
+
# GET
|
|
178
|
+
p_get = subparsers.add_parser("get", help=translate("cmd_get_help", config.get("lang")))
|
|
179
|
+
p_get.add_argument("key")
|
|
180
|
+
|
|
181
|
+
# SET
|
|
182
|
+
p_set = subparsers.add_parser("set", help=translate("cmd_set_help", config.get("lang")))
|
|
183
|
+
p_set.add_argument("key")
|
|
184
|
+
# p_set.add_argument("value", help="Valor (para listas, separar por comas a,b,c)")
|
|
185
|
+
p_set.add_argument(
|
|
186
|
+
"value", nargs="+", help=translate("cmd_setvalue_help", config.get("lang")))
|
|
187
|
+
|
|
188
|
+
# RESET
|
|
189
|
+
p_reset = subparsers.add_parser("reset", help=translate("cmd_reset_help", config.get("lang")))
|
|
190
|
+
p_reset.add_argument("key", nargs="?", help=translate("cmd_resetkey_help", config.get("lang")))
|
|
191
|
+
p_reset.add_argument(
|
|
192
|
+
"--all", action="store_true", help=translate("cmd_resetall_help", config.get("lang")))
|
|
193
|
+
|
|
194
|
+
args = parser.parse_args()
|
|
195
|
+
|
|
196
|
+
# Dispatch
|
|
197
|
+
if args.command == "list":
|
|
198
|
+
cmd_list(args)
|
|
199
|
+
elif args.command == "get":
|
|
200
|
+
cmd_get(args)
|
|
201
|
+
elif args.command == "set":
|
|
202
|
+
cmd_set(args)
|
|
203
|
+
elif args.command == "reset":
|
|
204
|
+
cmd_reset(args)
|
|
205
|
+
else:
|
|
206
|
+
parser.print_help()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
hiapp/core/__init__.py
ADDED
|
File without changes
|
hiapp/core/commands.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registro de comandos de voz de Hiapp.
|
|
3
|
+
|
|
4
|
+
Los comandos se recogen de dos fuentes:
|
|
5
|
+
- El núcleo: estado, ayuda.
|
|
6
|
+
- Los add-ons: cada uno declara los suyos en su addon.json.
|
|
7
|
+
|
|
8
|
+
Con todos los comandos + la wake word, se construye una gramática JSON
|
|
9
|
+
que se pasa a Vosk para restringir el reconocimiento.
|
|
10
|
+
|
|
11
|
+
Beneficios de la gramática:
|
|
12
|
+
- Menos falsos positivos (de ~50% a ~100% de precisión).
|
|
13
|
+
- Menos uso de CPU (espacio de búsqueda reducido).
|
|
14
|
+
- Misma velocidad que un modelo "small".
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Comandos del núcleo por idioma
|
|
21
|
+
CORE_COMMANDS = {
|
|
22
|
+
"es": {
|
|
23
|
+
"status": ("estado", "estatus"),
|
|
24
|
+
"help": ("ayuda", "aiuda", "ayúa"),
|
|
25
|
+
},
|
|
26
|
+
"en": {
|
|
27
|
+
"status": ("status", "state"),
|
|
28
|
+
"help": ("help",),
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CommandRegistry:
|
|
34
|
+
"""
|
|
35
|
+
Recoge comandos del núcleo y de los add-ons, y construye la gramática
|
|
36
|
+
de Vosk para el idioma activo.
|
|
37
|
+
|
|
38
|
+
Mapea frases exactas a intenciones para que el controlador sepa
|
|
39
|
+
qué hacer cuando Vosk devuelve una transcripción.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
# {lang: {intent: [frases]}}
|
|
44
|
+
self._by_intent = {}
|
|
45
|
+
# {lang: {frase: intent}}
|
|
46
|
+
self._phrase_to_intent = {}
|
|
47
|
+
|
|
48
|
+
# --- Registro -----------
|
|
49
|
+
|
|
50
|
+
def register_core(self):
|
|
51
|
+
"""Registra los comandos del núcleo (estado, ayuda)."""
|
|
52
|
+
for lang, intents in CORE_COMMANDS.items():
|
|
53
|
+
for intent, phrases in intents.items():
|
|
54
|
+
self._register(lang, intent, phrases)
|
|
55
|
+
|
|
56
|
+
def register_addon(self, manifest: dict):
|
|
57
|
+
"""
|
|
58
|
+
Registra los comandos de un add-on a partir de su manifest
|
|
59
|
+
(el diccionario cargado desde addon.json).
|
|
60
|
+
|
|
61
|
+
El manifest debe exponer los comandos con esta estructura:
|
|
62
|
+
"commands": {
|
|
63
|
+
"es": {
|
|
64
|
+
"start": ["activar servidor de juegos"],
|
|
65
|
+
"stop": ["desactivar servidor de juegos"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Las intenciones se registran con el prefijo del add-on:
|
|
70
|
+
"ppsspp:start", "ppsspp:stop"
|
|
71
|
+
"""
|
|
72
|
+
name = manifest.get("name", "addon")
|
|
73
|
+
commands = manifest.get("commands", {})
|
|
74
|
+
|
|
75
|
+
for lang, intents in commands.items():
|
|
76
|
+
for intent, phrases in intents.items():
|
|
77
|
+
full_intent = f"{name}:{intent}"
|
|
78
|
+
self._register(lang, full_intent, phrases)
|
|
79
|
+
|
|
80
|
+
def _register(self, lang: str, intent: str, phrases):
|
|
81
|
+
"""Añade frases a una intención en un idioma, evitando duplicados."""
|
|
82
|
+
self._by_intent.setdefault(lang, {})
|
|
83
|
+
self._by_intent[lang].setdefault(intent, [])
|
|
84
|
+
|
|
85
|
+
self._phrase_to_intent.setdefault(lang, {})
|
|
86
|
+
|
|
87
|
+
for phrase in phrases:
|
|
88
|
+
# Evitar duplicados
|
|
89
|
+
if phrase not in self._by_intent[lang][intent]:
|
|
90
|
+
self._by_intent[lang][intent].append(phrase)
|
|
91
|
+
# Mapear frase → intención (el último gana si hay colisión)
|
|
92
|
+
self._phrase_to_intent[lang][phrase] = intent
|
|
93
|
+
|
|
94
|
+
# --- Gramática -----------
|
|
95
|
+
|
|
96
|
+
def build_grammar(self, lang: str, wake_words: list) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Construye la gramática JSON para Vosk.
|
|
99
|
+
Incluye la wake word, los comandos del idioma, y el token [unk].
|
|
100
|
+
"""
|
|
101
|
+
phrases = set(wake_words)
|
|
102
|
+
|
|
103
|
+
for phrases_list in self._by_intent.get(lang, {}).values():
|
|
104
|
+
phrases.update(phrases_list)
|
|
105
|
+
|
|
106
|
+
phrases.add("[unk]")
|
|
107
|
+
return json.dumps(sorted(phrases))
|
|
108
|
+
|
|
109
|
+
# --- Búsqueda de intención -----------
|
|
110
|
+
|
|
111
|
+
def find_intent(self, text: str, lang: str) -> str | None:
|
|
112
|
+
"""
|
|
113
|
+
Devuelve la intención cuyo comando aparece en el texto, o None.
|
|
114
|
+
|
|
115
|
+
Con gramática activa, Vosk devuelve frases exactas. El fallback
|
|
116
|
+
por substring está por si el modelo devuelve algo inesperado.
|
|
117
|
+
"""
|
|
118
|
+
mapping = self._phrase_to_intent.get(lang, {})
|
|
119
|
+
|
|
120
|
+
# 1. Coincidencia exacta
|
|
121
|
+
if text in mapping:
|
|
122
|
+
return mapping[text]
|
|
123
|
+
|
|
124
|
+
# 2. Fallback: substring
|
|
125
|
+
for phrase, intent in mapping.items():
|
|
126
|
+
if phrase in text:
|
|
127
|
+
return intent
|
|
128
|
+
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# --- Utilidades -----------
|
|
132
|
+
|
|
133
|
+
def list_commands(self, lang: str) -> list:
|
|
134
|
+
"""Devuelve las frases registradas para un idioma."""
|
|
135
|
+
phrases = []
|
|
136
|
+
for phrases_list in self._by_intent.get(lang, {}).values():
|
|
137
|
+
phrases.extend(phrases_list)
|
|
138
|
+
return sorted(phrases)
|
hiapp/core/config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import socket
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Rutas base (fijas)
|
|
6
|
+
HIAPP_DIR = Path.home() / ".hiapp"
|
|
7
|
+
CONFIG_FILE = HIAPP_DIR / "config.json"
|
|
8
|
+
|
|
9
|
+
# Configuración de usuario (resetable con --all)
|
|
10
|
+
USER_DEFAULTS = {
|
|
11
|
+
"models_dir": str(HIAPP_DIR / "models"),
|
|
12
|
+
"games_dir": str(Path.home() / "PSP"),
|
|
13
|
+
"port": 5000,
|
|
14
|
+
"host": "0.0.0.0",
|
|
15
|
+
"aliases": [],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Estado de la aplicación (NO resetable con --all)
|
|
19
|
+
APP_DEFAULTS = {
|
|
20
|
+
"lang": None,
|
|
21
|
+
"lang_model_name": None,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Todas las claves válidas (para validación en CLI)
|
|
25
|
+
ALL_DEFAULTS = {**USER_DEFAULTS, **APP_DEFAULTS}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_config() -> dict:
|
|
29
|
+
"""Carga la configuración aplicando esta prioridad:
|
|
30
|
+
1. Valores por defecto (USER_DEFAULTS + APP_DEFAULTS).
|
|
31
|
+
2. Valores del archivo ~/.hiapp/config.json (si existen).
|
|
32
|
+
|
|
33
|
+
Solo se cargan las claves que existen en ALL_DEFAULTS.
|
|
34
|
+
"""
|
|
35
|
+
result = {**USER_DEFAULTS, **APP_DEFAULTS}
|
|
36
|
+
|
|
37
|
+
# 1. Cargar del archivo json
|
|
38
|
+
if CONFIG_FILE.exists():
|
|
39
|
+
try:
|
|
40
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
41
|
+
# config.update(json.load(f))
|
|
42
|
+
saved = json.load(f)
|
|
43
|
+
|
|
44
|
+
for key in ALL_DEFAULTS:
|
|
45
|
+
if key in saved: # Solo carga las que estén en ALL_KEYS
|
|
46
|
+
result[key] = saved[key]
|
|
47
|
+
except (json.JSONDecodeError, OSError):
|
|
48
|
+
pass # Usar valores por defecto
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_config(config: dict):
|
|
54
|
+
"""Guarda la configuración en ~/.hiapp/config.json."""
|
|
55
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
58
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_lan_ip() -> str:
|
|
62
|
+
"""Devuelve la IP local de la interfaz que conecta a la LAN."""
|
|
63
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
64
|
+
try:
|
|
65
|
+
# No envía nada, solo consulta la ruta de salida hacia internet
|
|
66
|
+
s.connect(("8.8.8.8", 80))
|
|
67
|
+
return s.getsockname()[0]
|
|
68
|
+
except Exception:
|
|
69
|
+
return "127.0.0.1"
|
|
70
|
+
finally:
|
|
71
|
+
s.close()
|