aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: deeef3c013a13a8e68f770c6e808af87ff039864 (plain)
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
# asist-p — asistente de voz local

Escucha por el micrófono, piensa y contesta hablando. Todo en la máquina: no
sale un byte a internet.

Une tres motores que ya existían —**Canary** para oír, **llama.cpp** para
pensar y **qwentts.cpp** para hablar— en un pipeline de hilos y canales en
Rust donde ninguna etapa espera a la siguiente.

```
  micrófono ──muestras──> segmentador ──intervención──> ASR ──texto──┐
  (cpal, tiempo real)      (VAD, turnos)                 (Canary)    │
                                                                     v
  altavoz <──muestras── síntesis <──frases── conversación <──────────┘
  (cpal, anillo)         (qwentts)          (llama.cpp + herramientas)
```

Cada caja es un hilo; cada flecha, un canal. Dos decisiones sostienen el
resto:

- **Nada bloquea al que va delante.** El micrófono nunca espera al
  reconocedor, y el modelo nunca espera al sintetizador. Quien va sobrado
  descarta trabajo en lugar de acumular retraso.
- **Se responde por frases, no por respuestas.** Cada frase sale hacia el
  sintetizador en cuanto se cierra, así que el asistente empieza a hablar
  mientras el modelo sigue escribiendo. Es lo que separa medio segundo de
  cinco.

## Puesta en marcha

```bash
git clone --recursive <este-repo> && cd asist-p
scripts/bootstrap.sh          # submódulos, parches, binarios y modelos
cargo run --release -- check  # comprueba que está todo en su sitio
cargo run --release -- run    # a hablar
```

`bootstrap.sh` es idempotente y no copia modelos: los enlaza desde donde ya
estén (`~/GIT-MIRRO`, `~/HF`). Si no encuentra los motores construidos, los
construye, y eso sí tarda.

```bash
cargo run --release -- devices          # listar dispositivos de audio
cargo run --release -- run --barge-in   # permitir cortar al asistente
cargo run --release -- run --shell      # habilitar órdenes del sistema
cargo run --release -- run --no-manage  # no lanzar los servidores
```

Durante el desarrollo conviene dejar los servidores levantados aparte —cargar
los modelos cuesta más de un minuto— y reiniciar sólo el binario de Rust:

```bash
scripts/servidores.sh arrancar
cargo run --release -- run --no-manage
```

## Cómo está organizado

| Crate | De qué se ocupa |
|---|---|
| `asist-core` | Configuración, eventos, errores, cliente HTTP, telemetría y el registro de herramientas |
| `asist-audio` | Captura y reproducción con cpal, detección de voz, troceado en turnos |
| `asist-asr` | Canary en ONNX: ventana deslizante y transcripción final |
| `asist-llm` | llama-server: streaming SSE, historial, llamadas a herramientas |
| `asist-tts` | qwentts: síntesis en streaming y voces clonadas |
| `asist-app` | Supervisor de procesos, orquestador y binario `asistente` |

Los tres motores viven en `vendor/` como submódulos fijados a un commit
concreto. Los cambios locales sobre ellos están en `vendor/patches/`, y
`bootstrap.sh` los aplica: sin ellos el reconocedor no se comporta como el
que se midió.

## Configuración

Todo está en [`config/asistente.toml`](config/asistente.toml), comentado. Los
valores que llevan una cifra en el comentario salen de una medición concreta,
no de una suposición; el detalle está en
[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md).

Lo que más se toca:

```toml
[vad]
silence_hold = 0.8     # cuánto silencio cierra tu intervención
barge_in = false       # cortar al asistente hablando encima

[general]
history_turns = 8      # memoria de la conversación
system_prompt = "..."  # carácter y estilo

[tools]
shell = false          # ejecutar órdenes del sistema
```

## Interrumpir y el eco del altavoz

Por defecto el asistente es **media dúplex**: mientras habla, el micrófono
está cerrado. No es pereza: con altavoces abiertos el micrófono se oye a sí
mismo, el asistente se transcribe y se responde solo.

Con auriculares, `--barge-in` deja hablar encima para cortarlo. El umbral se
eleva (`vad.barge_in_factor`) para que el eco no baste y una voz de verdad sí.

## Latencia

Un turno real, medido de punta a punta:

| Etapa | Tiempo |
|---|---|
| Reconocimiento (2,5 s de voz) | 200 ms |
| Modelo, primer token | 1660 ms |
| Modelo, resto | 1362 ms |
| Herramientas | 1 ms |
| **Del final de tu frase al primer audio** | **3213 ms** |

El binario mide esto en cada turno y señala la etapa más lenta. Tres ajustes
de configuración valieron más que cualquier cambio de código:

- El TTS decodificaba en bloques de 24 s: **4948 ms → 585 ms** al primer audio.
- La plantilla del modelo dejaba `<think>` abierto: **8630 ms → 413 ms** al
  primer token.
- El prompt de estilo anulaba las llamadas a herramientas: **0/8 → 8/8**.

Los tres, con el porqué y cómo se midieron, en
[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md).

## Herramientas

El asistente puede llamar a funciones. Viene con `hora_actual` y con
`ejecutar_comando`, esta última **apagada**: darle una shell a un modelo que
obedece a lo que oye por el micrófono es un cambio de postura de seguridad.
Cuando se enciende, sólo pasan las órdenes de una lista blanca, sin shell que
interprete metacaracteres y con un plazo máximo.

Añadir una propia son unas veinte líneas:
[docs/EXTENDER.md](docs/EXTENDER.md).

## Pruebas

68 pruebas unitarias que no necesitan ni modelos ni micrófono, y seis de
integración contra los servidores de verdad.

```bash
cargo test                                    # unitarias, sin modelos
scripts/servidores.sh arrancar
cargo test --release -p asist-app --test integracion -- --nocapture
cargo test --release -p asist-app --test integracion -- --ignored  # bucle completo
```

Las de integración se saltan solas si no hay servidores escuchando, y se
turnan la GPU con un mutex: los dos servidores comparten una tarjeta de 4 GB y
en paralelo miden contención en vez de latencia.

La prueba marcada `--ignored` cierra el bucle sin micrófono: sintetiza una
frase y comprueba que el reconocedor la recupera.

## Requisitos

- Rust 1.85 o posterior
- CUDA para los motores en C++ (funcionan en CPU, pero muy justos)
- PipeWire o ALSA
- ~6 GB de disco para los modelos