From fb5f3bd47a87d3a93a88175af174a3a89f7a4bfe Mon Sep 17 00:00:00 2001 From: elvis Date: Tue, 8 Sep 2026 10:14:15 -0300 Subject: Screen vision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/EXTENDER.md | 91 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 19 deletions(-) (limited to 'docs/EXTENDER.md') 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>; // 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 -- cgit v1.2.3