aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-08 10:14:15 -0300
committerelvis <elvis@claros.ar>2026-09-08 10:14:15 -0300
commitfb5f3bd47a87d3a93a88175af174a3a89f7a4bfe (patch)
treeaa9d17eeacd66343cc94bb8cf440715c1f5f9b18 /docs
parent71764752f28d31028b9c2ca486c0fc1bcea1cf95 (diff)
downloadasist-p-fb5f3bd47a87d3a93a88175af174a3a89f7a4bfe.tar.gz
asist-p-fb5f3bd47a87d3a93a88175af174a3a89f7a4bfe.zip
Screen vision
Cuarta herramienta: captura la pantalla y responde sobre lo que hay en ella. Aprovecha el mismo proyector multimodal que ya usaba la cámara. Como las dos hacen lo mismo —conseguir un JPEG, preguntarle al modelo, devolver texto— y sólo cambian en de dónde salen los píxeles, se unifican tras VisionTool y un trait FrameSource. La captura vive en scripts/capturar- pantalla.sh, que detecta grim, spectacle, maim, ImageMagick o scrot y reduce con ffmpeg: añadir un compositor es editar el guion, no recompilar. El ajuste que decide todo es la resolución, y no coincide con la de la cámara porque el problema no es el mismo: una escena se entiende, un texto hay que leerlo. Medido con tipografía de interfaz de 13 px y preguntando por datos concretos, a 1280 px acierta 3 de 3 en 7,6 s; a 960, 2 de 3; a 640, 1 de 3. Y a 640 px no falla diciendo que no lee: dijo que el error era «no se pudo abrir el archivo involution» y que la reunión era «a las 10:00», cuando ponía /dev/video0 y 15:30. Para un asistente de voz, decir una hora equivocada con aplomo es peor que tardar cuatro segundos más, así que 1280 px por defecto aunque cueste el triple que la cámara. Al prompt se le añade además que no complete lo que no se distinga, y funciona: en una prueba real contestó que el texto de la barra de direcciones era ilegible en vez de inventárselo. Con cuatro herramientas declaradas el riesgo era confundir pantalla con cámara. No pasa: 18 de 20, con cero confusiones entre ambas. Corregido de paso un fallo del lanzador de procesos que la captura destapó. Esperaba con try_wait en bucle sin vaciar las tuberías, así que un hijo que escribiera más de los 64 KB del búfer se quedaba bloqueado y moría por plazo vencido aunque estuviera trabajando. Los tres usos anteriores cabían de sobra —una fecha, un JSON, un fotograma de 9 KB—; la primera captura, de 174 KB, no. Ahora se vacían en hilos aparte, con una prueba por tubería. Ese lanzador es además nuevo: había tres copias del mismo patrón —plazo máximo, sin shell de por medio, distinguir fallo de cuelgue— en la shell, la búsqueda y la cámara. Ahora está una sola vez en asist_core::proc. Claude-Session: https://claude.ai/code/session_01KSfMfDsRwNAaXcCA6V5RTe
Diffstat (limited to 'docs')
-rw-r--r--docs/EXTENDER.md91
-rw-r--r--docs/RENDIMIENTO.md73
2 files changed, 145 insertions, 19 deletions
diff --git a/docs/EXTENDER.md b/docs/EXTENDER.md
index ea6d937..f0148e2 100644
--- a/docs/EXTENDER.md
+++ b/docs/EXTENDER.md
@@ -65,10 +65,14 @@ Tres cosas que conviene saber antes de escribir la primera:
se lo devuelve al modelo, que lo explica o reintenta.
- **La salida se lee en voz alta.** Devuelve una frase, no una tabla. El
registro recorta a 2000 caracteres, pero mucho antes de eso ya aburre.
-- **Con este modelo, más herramientas es peor.** Con tres declaradas elige
- bien entre ellas (15 de 16 medido), pero le cuesta abstenerse: busca en
- internet cosas que ya sabe. Apartados 3 y 5 de
- [RENDIMIENTO.md](RENDIMIENTO.md).
+- **Con este modelo, más herramientas es peor.** Con cuatro declaradas elige
+ bien entre ellas (18 de 20 medido, sin confundir cámara con pantalla), pero
+ le cuesta abstenerse: busca en internet cosas que ya sabe. Apartados 3 y 5
+ de [RENDIMIENTO.md](RENDIMIENTO.md).
+- **Si lanzas un proceso, usa `asist_core::proc::run`.** Controla el plazo, no
+ pasa por una shell y vacía las tuberías mientras espera. Esto último no es
+ un detalle: un hijo que escriba más de 64 KB se cuelga si nadie las lee, y
+ así es como la captura de pantalla parecía tardar 20 segundos.
## Ejecutar órdenes del sistema
@@ -169,34 +173,82 @@ Los argumentos van al `execve` tal cual, sin shell que los interprete: la
consulta sale de lo que se ha oído por el micrófono y no puede acabar
ejecutándose.
-## Mirar por la cámara
+## Ver: la cámara y la pantalla
-También montado. El servidor carga el proyector multimodal (`--mmproj`), así
-que el mismo modelo que conversa describe la imagen.
+Las dos hacen lo mismo en tres pasos —conseguir un JPEG, preguntarle al modelo
+por él, devolver texto— y comparten `VisionTool`. Lo único que las separa es
+`FrameSource`, que es de dónde salen los píxeles:
+
+```rust
+pub trait FrameSource: Send + Sync {
+ fn label(&self) -> &str;
+ fn capture(&self) -> Result<Vec<u8>>; // JPEG
+ fn available(&self) -> Result<()>; // se consulta antes de registrar
+}
+```
+
+Añadir una fuente nueva —una cámara IP, una ventana concreta, un PDF abierto—
+es implementar eso y llamar a `VisionTool::screen` o escribir una variante.
```toml
[camera]
enabled = true
device = "/dev/video0"
-width = 640 # la resolución manda en la latencia: 2,9 s aquí, 7,8 s a 720p
+width = 640 # escena: 2,9 s aquí, 7,8 s a 720p, misma descripción útil
height = 480
warmup_frames = 5 # deja que se asiente la exposición automática
save_dir = "" # vacío = no se guarda ningún fotograma
+
+[screen]
+enabled = true
+command = ["../scripts/capturar-pantalla.sh", "{ancho}", "{salida}"]
+width = 1280 # texto: NO lo bajes, ver abajo
+output = "" # monitor concreto; vacío = todo
+save_dir = "" # vacío = no se guarda ninguna captura
```
-Tres decisiones que conviene no deshacer sin pensarlo:
+### Por qué la pantalla usa 1280 y la cámara 640
+
+Porque una escena se entiende y un texto se lee, y son cosas distintas. Medido
+con tipografía de interfaz de 13 px:
+
+| Ancho | Tiempo | Aciertos sobre 3 preguntas |
+|---|---|---|
+| 640 px | 2,4 s | 1 — **se inventa el contenido** |
+| 960 px | 4,5 s | 2 |
+| 1280 px | 7,6 s | 3 |
-- **La pregunta viaja hasta la cámara.** El modelo rellena `pregunta` con lo
+A 640 px el modelo no dijo «no lo leo»: dijo que el error era «no se pudo abrir
+el archivo *involution*» y que la reunión era «a las 10:00». Ninguna de las dos
+cosas estaba en la imagen. Si bajas `screen.width`, esto es lo que compras.
+
+El prompt de la pantalla lleva además «lee sólo lo que de verdad pone y no
+completes lo que no se distinga», y funciona: en una prueba real contestó que
+«el texto de la barra de direcciones es ilegible por estar borroso».
+
+### Otro entorno gráfico
+
+`scripts/capturar-pantalla.sh` detecta grim (Wayland wlroots), spectacle (KDE),
+maim, ImageMagick y scrot (X11), reduce con ffmpeg y escribe el JPEG por la
+salida estándar. Está fuera del binario para que añadir un compositor sea
+editar el guion, no recompilar. Cualquier otro programa que respete ese
+contrato vale:
+
+```toml
+command = ["/ruta/a/mi-captura.sh", "{ancho}", "{salida}"]
+```
+
+### Tres decisiones que conviene no deshacer
+
+- **La pregunta viaja hasta la fuente.** El modelo rellena `pregunta` con lo
que quiere averiguar, y esa pregunta acompaña a la imagen. Pedir una
descripción genérica y luego interrogarla pierde el detalle que se buscaba.
-- **La imagen no entra en el historial.** Una foto ocupa cientos de tokens de
- contexto y arrastrarla turno tras turno saldría carísimo para lo poco que
- aporta una vez descrita. Lo que vuelve a la conversación es el texto.
-- **No se guarda nada en disco.** `save_dir` existe sólo para depurar qué está
- viendo el modelo; por defecto está vacío.
-
-Para ver la pantalla en vez de la cámara, `grim` ya está instalado: sería la
-misma herramienta cambiando cómo se obtienen los bytes del JPEG.
+- **La imagen no entra en el historial.** Cientos de tokens de contexto por
+ turno para algo que ya está descrito en una frase. Lo que vuelve a la
+ conversación es el texto.
+- **No se guarda nada en disco.** `save_dir` existe sólo para depurar. En la
+ pantalla pesa más que en la cámara: ahí caben contraseñas, mensajes privados
+ y correo abierto.
## Cambiar de motor
@@ -209,7 +261,8 @@ no toca el resto:
| Modelo | `asist_llm::LlmClient` | `crates/asist-llm/src/client.rs` |
| Texto a voz | `asist_tts::TtsClient` | `crates/asist-tts/src/lib.rs` |
| Visión | `asist_llm::LlmClient::look` | `crates/asist-llm/src/client.rs` |
-| Buscar y mirar | `asist_tools` | `crates/asist-tools/src/` |
+| Buscar y ver | `asist_tools` | `crates/asist-tools/src/` |
+| Lanzar procesos | `asist_core::proc::run` | `crates/asist-core/src/proc.rs` |
El cliente HTTP (`asist_core::http`) es propio y mínimo a propósito: todo es
HTTP plano contra localhost, y lo que ninguna librería genérica da cómodo es
diff --git a/docs/RENDIMIENTO.md b/docs/RENDIMIENTO.md
index b51f392..bed4f33 100644
--- a/docs/RENDIMIENTO.md
+++ b/docs/RENDIMIENTO.md
@@ -19,6 +19,8 @@ Linux 7.2 (CachyOS) · PipeWire.
| El prompt de estilo anulaba las llamadas a herramientas | 0 de 8 aciertos | 8 de 8 | — |
| La cámara capturaba a 1280x720 | 7,8 s por imagen | 2,9 s a 640x480 | **2,7×** |
| Una herramienta se ejecutaba en silencio | ~8,4 s hasta oír algo | 4,1 s con acuse hablado | **2×** |
+| La pantalla se leía a 640 px | 1 acierto de 3, con datos inventados | 3 de 3 a 1280 px | — |
+| Las tuberías no se vaciaban mientras se esperaba | captura de 180 KB colgada 20 s | 0,3 s | — |
Los tres son de configuración, no de código. Ninguno da error: el sistema
funciona, sólo que despacio o mal, que es lo que los hace difíciles de ver.
@@ -171,6 +173,50 @@ sale bien casi siempre —en el pipeline completo devolvió «Hoy en Buenos Aire
hace soleado esta mañana y por la tarde tendremos nubes con temperaturas
alrededor de 9°C», correcto— pero es una oportunidad más de equivocarse.
+## 4 ter. Mirar la pantalla: por debajo de 1280 px el modelo se inventa el texto
+
+Una pantalla no es una escena, es **texto**, y ahí este modelo tiene un modo de
+fallo que no perdona. Medido sobre una captura de 1920x1080 con tipografía de
+interfaz de 13 px, reducida a distintos anchos y preguntando por datos
+concretos (tres preguntas: qué dice el error, a qué hora es la reunión, cuánto
+es el total):
+
+| Ancho | Texto resultante | Tiempo | Aciertos |
+|---|---|---|---|
+| 640 px | 4 px | 2,4 s | **1 de 3** |
+| 960 px | 6 px | 4,5 s | 2 de 3 |
+| 1280 px | 8 px | 7,6 s | **3 de 3** |
+
+Lo grave no es que falle, es **cómo** falla. A 640 px no dijo «no lo leo»:
+
+> El error indica que el programa no pudo abrir el archivo **"involution"** y
+> que el código de error es 13.
+>
+> La reunión es **a las 10:00**.
+
+Ninguna de las dos cosas estaba en la imagen. El error era «no se pudo abrir
+/dev/video0» y la reunión, a las 15:30. Para un asistente de voz, decir una
+hora equivocada con aplomo es peor que tardar cuatro segundos más, así que el
+valor por defecto son **1280 px** aunque cueste el triple que la cámara.
+
+Con letra grande (26 px) la cosa cambia: a 640 px transcribió las seis cifras
+de prueba sin un fallo. Es el tamaño del texto *después de reducir* lo que
+manda, no la resolución en sí.
+
+Dos cosas más que salieron de aquí:
+
+- **La instrucción importa tanto como los píxeles.** Al prompt de la pantalla
+ se le añade «lee sólo lo que de verdad pone y no completes lo que no se
+ distinga». Funciona: en una prueba real el modelo contestó que «el texto de
+ la barra de direcciones es ilegible por estar borroso» en vez de
+ inventárselo.
+- **Una pantalla real cuesta más que una sintética.** La medición de 7,6 s es
+ con una captura de texto sobre fondo liso; con un navegador y un vídeo
+ abiertos subió a 10,8–12,4 s, porque hay mucho más que describir.
+
+Capturar es lo barato: `grim` tarda **0,03 s** en JPEG y el guion entero,
+reducción incluida, **0,30 s**.
+
## 5. Con tres herramientas el modelo elige bien, pero busca de más
Con `hora_actual`, `buscar_en_internet` y `mirar_por_la_camara` declaradas a la
@@ -200,6 +246,13 @@ apartado 3:
Añadir la prohibición no reduce los falsos positivos y sí empeora los
aciertos. Se queda la guía sola.
+Al añadir la cuarta herramienta —mirar la pantalla— el riesgo era que se
+confundiera con la cámara, que se le parece mucho. No pasó: **18 de 20**, con
+cero confusiones entre las dos. «¿Qué ves ahora mismo?» va a la cámara y «Lee
+lo que pone en la ventana que tengo delante» a la pantalla. El único fallo
+sigue siendo el mismo de siempre, buscar en internet «¿por qué el cielo es
+azul?».
+
## 6. Sin acuse hablado, una herramienta son seis segundos de silencio
Una búsqueda tarda unos 2,4 s y la cámara unos 4,5, y a eso hay que sumarle las
@@ -270,6 +323,7 @@ Turnos con las herramientas nuevas, también medidos de punta a punta:
|---|---|---|---|---|---|
| «¿Qué tiempo hace en Buenos Aires?» | 194 ms | 2243 ms | 2132 ms (búsqueda) | 4079 ms | **4122 ms** |
| «¿Qué ves por la cámara?» | 185 ms | 1649 ms | 5229 ms (captura + visión) | 1602 ms | **3153 ms** |
+| «¿Qué hay en mi pantalla?» | 207 ms | 1219 ms | 12434 ms (captura + visión) | 4113 ms | **3032 ms** |
El primer audio llega mucho antes que el final del turno porque suena el acuse
mientras la herramienta trabaja.
@@ -345,3 +399,22 @@ RUST_LOG=info,herramientas=info,camara=info cargo run --release -- run
scripts/servidores.sh arrancar
cargo test --release -p asist-app --test integracion -- --nocapture
```
+
+
+## 8. Un proceso que escribe mucho se cuelga si no le vacías la tubería
+
+La captura de pantalla funcionaba desde la shell en 0,3 s y desde el asistente
+vencía el plazo de 20 s. El guion era el mismo y el entorno también.
+
+La causa estaba en el lanzador de procesos compartido: esperaba con
+`try_wait()` en bucle **sin leer las tuberías**. Un hijo que escribe más de lo
+que cabe en el búfer (64 KB en Linux) se queda bloqueado escribiendo, nunca
+termina, y el bucle acaba matándolo por plazo vencido aunque estuviera
+haciendo su trabajo.
+
+Costó verlo porque los tres primeros usos cabían de sobra: una fecha, un JSON
+de búsqueda y un fotograma de cámara de 9 KB. La primera captura de pantalla,
+de 174 KB, no.
+
+Ahora las dos tuberías se vacían en hilos aparte mientras se espera. Hay dos
+pruebas que lo fijan, una por cada tubería, generando 512 KB y 200 KB.