From 8518a63f55153e7f45fd49ad6caff5555f4e374f Mon Sep 17 00:00:00 2001 From: elvis Date: Sat, 26 Sep 2026 20:20:19 -0300 Subject: Translate code, comments, logs and terminal UI to English; add English README; rename scripts --- crates/asist-tools/src/camera.rs | 56 +++++++++--------- crates/asist-tools/src/lib.rs | 14 ++--- crates/asist-tools/src/screen.rs | 88 ++++++++++++++-------------- crates/asist-tools/src/search.rs | 122 +++++++++++++++++++-------------------- crates/asist-tools/src/vision.rs | 66 ++++++++++----------- 5 files changed, 172 insertions(+), 174 deletions(-) (limited to 'crates/asist-tools/src') diff --git a/crates/asist-tools/src/camera.rs b/crates/asist-tools/src/camera.rs index bdc2aa0..9f17c88 100644 --- a/crates/asist-tools/src/camera.rs +++ b/crates/asist-tools/src/camera.rs @@ -1,4 +1,4 @@ -//! La cámara como fuente de imágenes. +//! The camera as an image source. use std::path::PathBuf; use std::time::Duration; @@ -13,19 +13,19 @@ const TOOL: &str = "mirar_por_la_camara"; pub struct CameraConfig { /// Dispositivo V4L2. pub device: PathBuf, - /// Resolución de captura. Medido con este modelo: 1,3 s a 320x240, 2,9 s a - /// 640x480 y 7,8 s a 1280x720, con la misma descripción útil a partir de - /// 640. Para una escena basta; para leer texto no (eso es la pantalla). + /// Capture resolution. Measured with this model: 1.3 s at 320x240, 2.9 s + /// at 640x480 and 7.8 s at 1280x720, with the same useful description from + /// 640 up. Enough for a scene; not for reading text (that is the screen). pub width: u32, pub height: u32, - /// Fotogramas que se descartan antes de quedarse con uno. + /// Frames discarded before keeping one. /// - /// La cámara arranca con la exposición automática sin asentar y el primer - /// fotograma suele salir quemado. Descartar unos pocos es prácticamente - /// gratis: medido, 0,45 s frente a 0,53 s. + /// The camera starts with auto-exposure not settled yet and the first frame + /// is usually blown out. Discarding a few is practically free: measured, + /// 0.45 s versus 0.53 s. pub warmup_frames: u32, pub timeout: Duration, - /// Carpeta donde dejar los fotogramas. `None` = no se guarda ninguno. + /// Directory to leave frames in. `None` = none is saved. pub save_dir: Option, } @@ -62,7 +62,7 @@ impl FrameSource for Camera { return Err(Error::Tool { tool: TOOL.into(), message: format!( - "no existe {}; comprueba con «v4l2-ctl --list-devices»", + "{} does not exist; check with «v4l2-ctl --list-devices»", self.config.device.display() ), }); @@ -73,9 +73,9 @@ impl FrameSource for Camera { fn capture(&self) -> Result> { self.available()?; - // ffmpeg y no V4L2 a pelo: una cámara USB entrega MJPEG, YUYV o lo que - // le parezca, y reimplementar esa negociación para ahorrarse un - // proceso no sale a cuenta. + // ffmpeg rather than raw V4L2: a USB camera delivers MJPEG, YUYV or + // whatever it likes, and reimplementing that negotiation to save one + // process is not worth it. let mut args = vec![ "-hide_banner".into(), "-loglevel".into(), @@ -89,8 +89,8 @@ impl FrameSource for Camera { self.config.device.to_string_lossy().into_owned(), ]; if self.config.warmup_frames > 0 { - // Se leen N fotogramas y se conserva el último: así la exposición - // se asienta sin abrir el dispositivo dos veces. + // N frames are read and the last one is kept: that way exposure + // settles without opening the device twice. args.push("-vf".into()); args.push(format!("select=eq(n\\,{})", self.config.warmup_frames)); } @@ -108,9 +108,9 @@ impl FrameSource for Camera { } } -/// Guarda una copia sólo si se ha pedido expresamente. Por defecto no se -/// escribe nada: un asistente que deja fotogramas por ahí es un problema de -/// privacidad, no una comodidad de depuración. +/// Saves a copy only if explicitly requested. By default nothing is +/// written: an assistant that leaves frames lying around is a privacy +/// problem, not a debugging convenience. pub(crate) fn save(dir: &std::path::Path, bytes: &[u8], prefix: &str) { let name = format!( "{prefix}-{}.jpg", @@ -122,7 +122,7 @@ pub(crate) fn save(dir: &std::path::Path, bytes: &[u8], prefix: &str) { if let Err(err) = std::fs::create_dir_all(dir).and_then(|()| std::fs::write(dir.join(name), bytes)) { - tracing::warn!(target: "vision", %err, "no se pudo guardar la captura"); + tracing::warn!(target: "vision", %err, "could not save the capture"); } } @@ -131,38 +131,38 @@ mod tests { use super::*; #[test] - fn un_dispositivo_inexistente_se_detecta_antes_de_registrar_la_herramienta() { + fn missing_device_is_detected_before_registering_the_tool() { let camera = Camera::new(CameraConfig { - device: PathBuf::from("/dev/video-que-no-existe"), + device: PathBuf::from("/dev/video-does-not-exist"), ..Default::default() }); let err = camera.available().unwrap_err().to_string(); - assert!(err.contains("no existe"), "{err}"); + assert!(err.contains("does not exist"), "{err}"); assert!( err.contains("v4l2-ctl"), - "el error debe decir cómo comprobarlo: {err}" + "the error must say how to check it: {err}" ); } #[test] - fn capturar_sin_dispositivo_falla_sin_llegar_a_ffmpeg() { + fn capturing_without_a_device_fails_before_ffmpeg() { let camera = Camera::new(CameraConfig { - device: PathBuf::from("/dev/video-que-no-existe"), + device: PathBuf::from("/dev/video-does-not-exist"), ..Default::default() }); assert!(camera.capture().is_err()); } #[test] - fn por_defecto_no_se_guarda_ningun_fotograma() { + fn no_frame_is_saved_by_default() { assert!( CameraConfig::default().save_dir.is_none(), - "guardar imágenes por defecto sería una fuga de privacidad" + "saving images by default would be a privacy leak" ); } #[test] - fn la_resolucion_por_defecto_es_la_medida_como_equilibrada() { + fn default_resolution_is_the_measured_balance() { let config = CameraConfig::default(); assert_eq!((config.width, config.height), (640, 480)); } diff --git a/crates/asist-tools/src/lib.rs b/crates/asist-tools/src/lib.rs index a1c2466..3815373 100644 --- a/crates/asist-tools/src/lib.rs +++ b/crates/asist-tools/src/lib.rs @@ -1,11 +1,11 @@ -//! Herramientas que asoman el asistente al mundo: buscar en internet, mirar -//! por la cámara y mirar la pantalla. +//! Tools that open the assistant to the world: searching the web, looking +//! through the camera and looking at the screen. //! -//! Viven en un crate aparte de `asist-core` porque necesitan cosas que el -//! núcleo no debe arrastrar —un cliente con TLS, el modelo multimodal, el -//! dispositivo de vídeo, el compositor—, y porque son el ejemplo de que el -//! punto de extensión funciona: se registran con `ToolRegistry::register` sin -//! tocar el orquestador. +//! They live in a crate separate from `asist-core` because they need things +//! the core must not pull in (a TLS client, the multimodal model, the video +//! device, the compositor), and because they prove the extension point +//! works: they are registered with `ToolRegistry::register` without touching +//! the orchestrator. pub mod camera; pub mod screen; diff --git a/crates/asist-tools/src/screen.rs b/crates/asist-tools/src/screen.rs index dfed923..6fab402 100644 --- a/crates/asist-tools/src/screen.rs +++ b/crates/asist-tools/src/screen.rs @@ -1,21 +1,20 @@ -//! La pantalla como fuente de imágenes. +//! The screen as an image source. //! -//! Se diferencia de la cámara en lo único que de verdad importa aquí: **una -//! pantalla es texto**. Y con texto este modelo tiene un modo de fallo feo. -//! Medido sobre una captura con tipografía de interfaz de 13 px, preguntando -//! por datos concretos: +//! It differs from the camera in the only thing that really matters here: **a +//! screen is text**. And with text this model has an ugly failure mode. +//! Measured on a capture with 13 px UI type, asking for specific details: //! -//! | Ancho | Tiempo | Aciertos | Qué pasa cuando falla | -//! |------|--------|----------|-----------------------| -//! | 640 | 2,4 s | 1 de 3 | se inventa el contenido | -//! | 960 | 4,5 s | 2 de 3 | mezcla lo leído con lo supuesto | -//! | 1280 | 7,6 s | 3 de 3 | — | +//! | Width | Time | Hits | What happens when it fails | +//! |-------|-------|--------|----------------------------| +//! | 640 | 2.4 s | 1 of 3 | it makes the content up | +//! | 960 | 4.5 s | 2 of 3 | it mixes what it read with what it assumed | +//! | 1280 | 7.6 s | 3 of 3 | — | //! -//! A 640 px 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. De ahí que el valor por defecto sean 1280 px -//! aunque cueste el triple que la cámara: para un asistente de voz, decir una -//! hora equivocada con aplomo es peor que tardar cuatro segundos más. +//! At 640 px it did not say «I cannot read it»: it said the error was «no se +//! pudo abrir el archivo involution» and the meeting was «at 10:00». Neither +//! was in the image. Hence the 1280 px default even though it costs three +//! times the camera: for a voice assistant, confidently stating a wrong time +//! is worse than taking four more seconds. use std::path::PathBuf; use std::time::Duration; @@ -28,19 +27,18 @@ const TOOL: &str = "mirar_la_pantalla"; #[derive(Debug, Clone)] pub struct ScreenConfig { - /// Programa de captura y sus argumentos. `{ancho}` y `{salida}` se - /// sustituyen antes de ejecutar. Tiene que escribir un JPEG por la salida - /// estándar. + /// Capture program and its arguments. `{width}` and `{output}` are + /// substituted before running. It must write a JPEG to standard output. pub command: Vec, - /// Ancho al que se reduce la captura antes de mandarla al modelo. + /// Width the capture is scaled down to before sending it to the model. pub width: u32, - /// Monitor concreto; vacío = todo lo que haya. + /// A specific monitor; empty = everything there is. pub output: String, pub timeout: Duration, - /// Carpeta donde dejar las capturas. `None` = no se guarda ninguna. + /// Directory to leave captures in. `None` = none is saved. /// - /// Aquí pesa más que en la cámara: en una captura de pantalla caben - /// contraseñas, mensajes privados y correo abierto. + /// It weighs more here than for the camera: a screenshot can hold + /// passwords, private messages and open email. pub save_dir: Option, } @@ -48,9 +46,9 @@ impl Default for ScreenConfig { fn default() -> Self { Self { command: vec![ - "scripts/capturar-pantalla.sh".into(), - "{ancho}".into(), - "{salida}".into(), + "scripts/capture-screen.sh".into(), + "{width}".into(), + "{output}".into(), ], width: 1280, output: String::new(), @@ -72,7 +70,7 @@ impl Screen { fn program(&self) -> Result<&String> { self.config.command.first().ok_or_else(|| Error::Tool { tool: TOOL.into(), - message: "screen.command está vacío".into(), + message: "screen.command is empty".into(), }) } } @@ -84,19 +82,19 @@ impl FrameSource for Screen { fn available(&self) -> Result<()> { let program = self.program()?; - // Un nombre suelto se resuelve por el PATH; una ruta tiene que existir. + // A bare name is resolved through PATH; a path must exist. if program.contains('/') && !std::path::Path::new(program).exists() { return Err(Error::Tool { tool: TOOL.into(), message: format!("no existe {program}"), }); } - // Sin entorno gráfico no hay nada que capturar, y más vale decirlo al - // arrancar que a mitad de una pregunta. + // Without a graphical environment there is nothing to capture, and + // better to say so at startup than in the middle of a question. if std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none() { return Err(Error::Tool { tool: TOOL.into(), - message: "no hay sesión gráfica (ni WAYLAND_DISPLAY ni DISPLAY)".into(), + message: "no graphical session (neither WAYLAND_DISPLAY nor DISPLAY)".into(), }); } Ok(()) @@ -108,8 +106,8 @@ impl FrameSource for Screen { let args: Vec = self.config.command[1..] .iter() .map(|arg| { - arg.replace("{ancho}", &self.config.width.to_string()) - .replace("{salida}", &self.config.output) + arg.replace("{width}", &self.config.width.to_string()) + .replace("{output}", &self.config.output) }) .collect(); @@ -127,30 +125,30 @@ mod tests { fn config() -> ScreenConfig { ScreenConfig { - command: vec!["/bin/echo".into(), "{ancho}".into(), "{salida}".into()], + command: vec!["/bin/echo".into(), "{width}".into(), "{output}".into()], ..Default::default() } } #[test] - fn el_ancho_por_defecto_es_el_minimo_medido_para_leer_texto() { + fn default_width_is_the_measured_minimum_to_read_text() { assert_eq!( ScreenConfig::default().width, 1280, - "por debajo de 1280 el modelo se inventa lo que pone en pantalla" + "below 1280 the model makes up what the screen says" ); } #[test] - fn por_defecto_no_se_guarda_ninguna_captura() { + fn no_capture_is_saved_by_default() { assert!( ScreenConfig::default().save_dir.is_none(), - "en una captura de pantalla caben contraseñas y mensajes privados" + "a screenshot can hold passwords and private messages" ); } #[test] - fn un_guion_inexistente_se_detecta_antes_de_registrar_la_herramienta() { + fn missing_script_is_detected_before_registering_the_tool() { let screen = Screen::new(ScreenConfig { command: vec!["/no/existe/captura.sh".into()], ..Default::default() @@ -163,7 +161,7 @@ mod tests { } #[test] - fn una_orden_vacia_se_rechaza() { + fn empty_command_is_rejected() { let screen = Screen::new(ScreenConfig { command: vec![], ..Default::default() @@ -172,16 +170,16 @@ mod tests { } #[test] - fn los_marcadores_se_sustituyen_antes_de_ejecutar() { - // /bin/echo devuelve los argumentos, así que la captura falla por no - // ser un JPEG; lo que se comprueba es que el mensaje trae el ancho ya - // sustituido, no el marcador. + fn placeholders_are_substituted_before_running() { + // /bin/echo returns the arguments, so the capture fails for not being + // a JPEG; what is checked is that the message carries the width already + // substituted, not the placeholder. let mut config = config(); config.width = 1280; config.output = "eDP-1".into(); let screen = Screen::new(config); if std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none() { - return; // sin sesión gráfica no hay nada que probar aquí + return; // without a graphical session there is nothing to test here } let err = screen.capture().unwrap_err().to_string(); assert!(err.contains("no devolvió un JPEG"), "{err}"); diff --git a/crates/asist-tools/src/search.rs b/crates/asist-tools/src/search.rs index 2d26020..c5b7e83 100644 --- a/crates/asist-tools/src/search.rs +++ b/crates/asist-tools/src/search.rs @@ -1,9 +1,9 @@ -//! Búsqueda en internet. +//! Web search. //! -//! El resultado se va a leer en voz alta, así que lo que interesa no es una -//! lista de enlaces sino una respuesta. Por eso se prefiere un buscador que -//! sintetice —Tavily devuelve un párrafo ya redactado— y los titulares sólo -//! acompañan como respaldo cuando no hay síntesis. +//! The result will be read aloud, so what matters is not a list of links but +//! an answer. That is why a search engine that summarizes is preferred +//! (Tavily returns an already written paragraph), and the headlines only come +//! along as a fallback when there is no summary. use std::path::PathBuf; use std::time::{Duration, Instant}; @@ -13,33 +13,32 @@ use asist_core::proc; use asist_core::tools::Tool; use serde_json::{json, Value}; -/// De dónde salen los resultados. +/// Where the results come from. #[derive(Debug, Clone)] pub enum SearchBackend { - /// API de Tavily. Devuelve una respuesta ya redactada además de los - /// resultados, que es justo lo que hace falta para hablarla. + /// Tavily API. It returns an already written answer besides the + /// results, which is exactly what is needed to speak it. Tavily { api_key: String }, - /// Instancia de SearXNG, propia o de confianza. Sin clave, pero devuelve - /// sólo resultados: la síntesis la tiene que hacer el modelo. + /// SearXNG instance, your own or a trusted one. No key, but it only + /// returns results: the model has to do the summary. SearxNG { base_url: String }, - /// Un programa externo que imprime los resultados en JSON. + /// An external program that prints the results as JSON. /// - /// Es la vía sin clave: `scripts/buscar-ddgs.sh` consulta DuckDuckGo y - /// compañía a través de la librería `ddgs`. Vale para cualquier otra cosa - /// que escriba JSON por la salida estándar —un puente a un servidor MCP, - /// un buscador interno—, así que también es el punto de extensión del - /// apartado de búsqueda. + /// It is the keyless path: `scripts/search-ddgs.sh` queries DuckDuckGo and + /// friends through the `ddgs` library. It works for anything else that writes + /// JSON to standard output (a bridge to an MCP server, an internal search + /// engine), so it is also the extension point of the search feature. /// - /// En `args`, `{consulta}` y `{max}` se sustituyen antes de ejecutar. + /// In `args`, `{query}` and `{max}` are substituted before running. Command { program: PathBuf, args: Vec }, } impl SearchBackend { - /// Backend sin clave por omisión: el guion que envuelve a ddgs. + /// Default keyless backend: the script that wraps ddgs. pub fn ddgs(script: impl Into) -> Self { SearchBackend::Command { program: script.into(), - args: vec!["{consulta}".into(), "{max}".into()], + args: vec!["{query}".into(), "{max}".into()], } } } @@ -49,7 +48,7 @@ impl SearchBackend { match self { SearchBackend::Tavily { .. } => "Tavily", SearchBackend::SearxNG { .. } => "SearXNG", - SearchBackend::Command { .. } => "comando", + SearchBackend::Command { .. } => "command", } } } @@ -92,8 +91,8 @@ impl WebSearch { "query": query, "max_results": self.max_results, "search_depth": "basic", - // La respuesta redactada es la razón de usar este buscador: sin - // ella habría que gastar otra vuelta del modelo en resumir. + // The written answer is the reason to use this engine: without it + // another model round would be spent summarizing. "include_answer": true, }); let mut response = self @@ -112,18 +111,18 @@ impl WebSearch { compose(&answer, &results) } - /// Ejecuta el programa configurado y traduce lo que imprima. + /// Runs the configured program and translates whatever it prints. fn command(&self, program: &std::path::Path, args: &[String], query: &str) -> Result { let rendered: Vec = args .iter() .map(|arg| { - arg.replace("{consulta}", query) + arg.replace("{query}", query) .replace("{max}", &self.max_results.to_string()) }) .collect(); - // Los argumentos van al `execve` tal cual: la consulta sale de lo que - // se ha oído por el micrófono, y no puede acabar interpretada por una + // The arguments go to `execve` as they are: the query comes from what + // the microphone heard, and it must never end up interpreted by a // shell. let output = proc::run(program, &rendered, self.timeout, None) .map_err(|e| Self::fail(e.to_string()))?; @@ -205,22 +204,23 @@ impl Tool for WebSearch { SearchBackend::Command { program, args } => self.command(program, args, query), }; tracing::info!( - target: "herramientas", - buscador = self.backend.label(), - consulta = query, + target: "tools", + backend = self.backend.label(), + query = query, ms = started.elapsed().as_millis(), ok = result.is_ok(), - "búsqueda" + "search" ); result } } -/// Saca respuesta y resultados de un JSON sin exigir una forma concreta. +/// Pulls the answer and results out of a JSON without requiring a specific +/// shape. /// -/// Cada buscador nombra los campos a su manera —`href` o `url`, `body` o -/// `content`, la lista suelta o dentro de `results`— y aquí lo que interesa es -/// que un guion nuevo funcione sin tener que tocar Rust. +/// Each engine names the fields its own way (`href` or `url`, `body` or +/// `content`, a bare list or inside `results`), and what matters here is that +/// a new script works without touching Rust. fn extract(parsed: &Value, max: usize) -> (String, Vec) { let answer = parsed .get("answer") @@ -264,11 +264,11 @@ fn extract(parsed: &Value, max: usize) -> (String, Vec) { (answer, results) } -/// Junta la respuesta sintetizada con los titulares. +/// Joins the summarized answer with the headlines. /// -/// La síntesis va primero porque es lo que probablemente se pronuncie; los -/// titulares quedan detrás para que el modelo tenga de dónde tirar si la -/// pregunta pedía un detalle que el resumen no cubre. +/// The summary goes first because it is what will probably be spoken; the +/// headlines stay behind so the model has something to draw on if the +/// question asked for a detail the summary does not cover. fn compose(answer: &str, results: &[String]) -> Result { if answer.is_empty() && results.is_empty() { return Err(Error::Tool { @@ -299,9 +299,9 @@ fn clip(text: &str, max: usize) -> String { flat.chars().take(max).collect::() + "…" } -/// Un error de ureq trae la cadena completa con la URL dentro, y en Tavily esa -/// URL no lleva la clave —va en la cabecera—, pero más vale no acostumbrarse: -/// se resume el error en vez de volcarlo entero. +/// A ureq error carries the whole chain with the URL inside, and for Tavily +/// that URL does not hold the key (it goes in a header), but better not to get +/// used to it: the error is summarized instead of dumped whole. fn describe_ureq(err: &ureq::Error) -> String { match err { ureq::Error::StatusCode(code) => match code { @@ -333,46 +333,46 @@ mod tests { use super::*; #[test] - fn la_respuesta_sintetizada_va_delante_de_las_fuentes() { + fn synthesized_answer_goes_before_the_sources() { let out = compose("Hace 17 grados.", &["Meteored: parcialmente nuboso".into()]).unwrap(); assert!(out.starts_with("Hace 17 grados.")); assert!(out.contains("Fuentes:")); } #[test] - fn sin_sintesis_valen_los_titulares() { + fn without_an_answer_the_headlines_are_used() { let out = compose("", &["Uno: algo".into(), "Dos: otra cosa".into()]).unwrap(); assert!(out.starts_with("1. Uno")); assert!(out.contains("2. Dos")); } #[test] - fn una_busqueda_sin_resultados_es_un_error_y_no_una_cadena_vacia() { - // Si devolviera "" el modelo se inventaría la respuesta creyendo que - // la herramienta funcionó. + fn search_without_results_is_an_error_not_an_empty_string() { + // If it returned "" the model would make the answer up believing the + // tool had worked. assert!(compose("", &[]).is_err()); } #[test] - fn los_fragmentos_largos_se_recortan() { - let largo = "palabra ".repeat(80); - assert!(clip(&largo, 100).chars().count() <= 101); + fn long_snippets_are_truncated() { + let long_text = "palabra ".repeat(80); + assert!(clip(&long_text, 100).chars().count() <= 101); assert_eq!(clip("corto", 100), "corto"); } #[test] - fn los_saltos_de_linea_de_los_fragmentos_se_aplanan() { + fn snippet_newlines_are_flattened() { assert_eq!(clip("uno\n\n dos", 100), "uno dos"); } #[test] - fn la_consulta_se_codifica_para_la_url() { + fn query_is_url_encoded() { assert_eq!(urlencode("qué tiempo hace"), "qu%C3%A9+tiempo+hace"); assert_eq!(urlencode("a&b=c"), "a%26b%3Dc"); } #[test] - fn una_consulta_vacia_se_rechaza_sin_salir_a_la_red() { + fn empty_query_is_rejected_without_network() { let tool = WebSearch::new( SearchBackend::Tavily { api_key: "x".into(), @@ -385,7 +385,7 @@ mod tests { } #[test] - fn se_entiende_la_lista_suelta_que_devuelve_ddgs() { + fn bare_ddgs_list_is_understood() { let raw = serde_json::json!([ { "title": "Canberra", "href": "https://x", "body": "es la capital" }, { "title": "Sídney", "href": "https://y", "body": "no lo es" } @@ -397,7 +397,7 @@ mod tests { } #[test] - fn se_entiende_tambien_la_forma_con_results_y_answer() { + fn results_and_answer_shape_is_understood_too() { let raw = serde_json::json!({ "answer": "Canberra.", "results": [{ "title": "T", "url": "https://x", "content": "C" }] @@ -408,7 +408,7 @@ mod tests { } #[test] - fn se_respeta_el_maximo_de_resultados() { + fn maximum_results_is_honoured() { let raw = serde_json::json!([ { "title": "1", "body": "a" }, { "title": "2", "body": "b" }, { "title": "3", "body": "c" } @@ -417,26 +417,26 @@ mod tests { } #[test] - fn una_fila_sin_titulo_ni_texto_se_ignora() { + fn row_without_title_or_text_is_ignored() { let raw = serde_json::json!([{ "href": "https://x" }, { "title": "T", "body": "C" }]); assert_eq!(extract(&raw, 5).1, vec!["T: C"]); } #[test] - fn los_marcadores_del_comando_se_sustituyen() { + fn command_placeholders_are_substituted() { let tool = WebSearch::new(SearchBackend::ddgs("/bin/echo"), 4, Duration::from_secs(5)); let SearchBackend::Command { args, .. } = tool.backend() else { - panic!("esperaba un backend de comando"); + panic!("expected a command backend"); }; let rendered: Vec = args .iter() - .map(|a| a.replace("{consulta}", "hola").replace("{max}", "4")) + .map(|a| a.replace("{query}", "hola").replace("{max}", "4")) .collect(); assert_eq!(rendered, vec!["hola", "4"]); } #[test] - fn un_comando_que_no_existe_da_un_error_util() { + fn missing_command_gives_a_useful_error() { let tool = WebSearch::new( SearchBackend::ddgs("/no/existe/buscador.sh"), 3, @@ -450,7 +450,7 @@ mod tests { } #[test] - fn el_numero_de_resultados_se_mantiene_en_un_rango_sensato() { + fn result_count_stays_in_a_sensible_range() { let tool = WebSearch::new( SearchBackend::Tavily { api_key: "x".into(), diff --git a/crates/asist-tools/src/vision.rs b/crates/asist-tools/src/vision.rs index 2b91ed9..5db4aee 100644 --- a/crates/asist-tools/src/vision.rs +++ b/crates/asist-tools/src/vision.rs @@ -1,13 +1,13 @@ -//! Lo que comparten mirar por la cámara y mirar la pantalla. +//! What looking through the camera and looking at the screen have in common. //! -//! Las dos herramientas hacen lo mismo en tres pasos —conseguir un JPEG, -//! preguntarle al modelo por él, devolver texto— y sólo se diferencian en de -//! dónde salen los píxeles. Eso es lo que abstrae `FrameSource`. +//! Both tools do the same three steps (get a JPEG, ask the model about it, +//! return text) and only differ in where the pixels come from. That is what +//! `FrameSource` abstracts. //! -//! Lo que **no** comparten es la resolución, y no por descuido: una escena de -//! cámara se entiende a 640 px, pero el texto de una interfaz a esa escala el -//! modelo no lo lee mal, se lo **inventa** (ver docs/RENDIMIENTO.md). Por eso -//! cada fuente trae la suya. +//! What they do **not** share is the resolution, and not by oversight: a +//! camera scene is understood at 640 px, but at that scale the model does not +//! misread UI text, it **makes it up** (see docs/RENDIMIENTO.md). That is why +//! each source brings its own. use std::sync::Arc; use std::time::Instant; @@ -17,27 +17,27 @@ use asist_core::http::Cancel; use asist_core::proc; use asist_llm::LlmClient; -/// De dónde salen los píxeles. +/// Where the pixels come from. pub trait FrameSource: Send + Sync { - /// Para los mensajes de error y el registro: «cámara», «pantalla». + /// For error messages and logs: «cámara», «pantalla». fn label(&self) -> &str; - /// Captura un fotograma en JPEG. + /// Captures one frame as JPEG. fn capture(&self) -> Result>; - /// `false` si falta el dispositivo o la herramienta de captura. + /// `false` if the device or the capture tool is missing. /// - /// Se consulta antes de registrar: declarar una herramienta que va a - /// fallar siempre es peor que no tenerla, porque el modelo la llama, se - /// come el error y gasta el turno. + /// Checked before registering: declaring a tool that will always fail is + /// worse than not having it, because the model calls it, swallows the error + /// and wastes the turn. fn available(&self) -> Result<()>; } -/// Ejecuta un programa que escribe un JPEG por la salida estándar. +/// Runs a program that writes a JPEG to standard output. /// -/// Es el mecanismo de captura de las dos fuentes: la cámara llama a ffmpeg y -/// la pantalla a un guion que sabe de compositores. Tenerlo aquí evita que -/// cada una repita el control del plazo y la comprobación de la cabecera. +/// It is the capture mechanism of both sources: the camera calls ffmpeg and +/// the screen a script that knows about compositors. Having it here keeps +/// each one from repeating the deadline control and the header check. pub fn capture_jpeg( tool: &str, program: &std::path::Path, @@ -53,7 +53,7 @@ pub fn capture_jpeg( if !output.success() || output.stdout.is_empty() { let stderr = output.stderr.to_lowercase(); - // Los dos fallos que más se dan, traducidos a algo accionable. + // The two most common failures, translated into something actionable. let hint = if stderr.contains("permission denied") { ". Comprueba los permisos del dispositivo" } else if stderr.contains("busy") { @@ -67,8 +67,8 @@ pub fn capture_jpeg( ))); } - // Cabecera JPEG. Sin esto, una captura corrupta llega hasta el modelo y - // vuelve como un error genérico que no dice dónde mirar. + // JPEG header. Without this, a corrupt capture reaches the model and + // comes back as a generic error that does not say where to look. if output.stdout.len() < 4 || output.stdout[..2] != [0xFF, 0xD8] { return Err(fail(format!( "{} no devolvió un JPEG ({} bytes)", @@ -79,7 +79,7 @@ pub fn capture_jpeg( tracing::debug!( target: "vision", - programa = %program.display(), + program = %program.display(), kb = output.stdout.len() / 1024, ms = output.took.as_millis(), "captura" @@ -87,7 +87,7 @@ pub fn capture_jpeg( Ok(output.stdout) } -/// La herramienta que ve: captura y le pregunta al modelo. +/// The tool that sees: it captures and asks the model. pub struct VisionTool { source: Box, llm: Arc, @@ -96,10 +96,10 @@ pub struct VisionTool { parameter_hint: &'static str, default_question: &'static str, acknowledgement: &'static str, - /// Se añade a la pregunta del usuario antes de mandarla con la imagen. + /// Appended to the user's question before sending it with the image. /// - /// Hace falta porque esta petición va fuera del historial y no le llega la - /// instrucción de voz del asistente. + /// It is needed because this request goes outside the history and does not + /// get the assistant's voice prompt. style: &'static str, } @@ -135,9 +135,9 @@ impl VisionTool { en pantalla, pon: ¿Qué se ve en la pantalla?", default_question: "¿Qué se ve en esta captura de pantalla?", acknowledgement: "Miro la pantalla.", - // El aviso de no inventar es lo más importante de toda la - // instrucción: cuando el texto queda pequeño, este modelo no dice - // que no lo lee, se saca un contenido plausible de la manga. + // The warning about not making things up is the most important part of + // the whole instruction: when the text is small, this model does not say + // it cannot read it, it pulls plausible content out of thin air. style: "Responde en una o dos frases cortas en español, en texto plano. Lee sólo \ lo que de verdad pone en la imagen y no completes lo que no se distinga: \ si el texto está borroso o no se lee, dilo en vez de suponerlo.", @@ -168,8 +168,8 @@ impl asist_core::tools::Tool for VisionTool { }) } - /// Enciende la cámara o fotografía lo que haya en pantalla; en ambos casos - /// conviene que se anuncie. + /// It turns the camera on or photographs whatever is on screen; in both + /// cases it should be announced. fn is_side_effecting(&self) -> bool { true } @@ -196,7 +196,7 @@ impl asist_core::tools::Tool for VisionTool { tracing::info!( target: "vision", - fuente = self.source.label(), + source = self.source.label(), kb = frame.len() / 1024, ms = started.elapsed().as_millis(), "descrito" -- cgit v1.2.3