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-app/src/main.rs | 275 +++++++++++++++++++++---------------------- 1 file changed, 133 insertions(+), 142 deletions(-) (limited to 'crates/asist-app/src/main.rs') diff --git a/crates/asist-app/src/main.rs b/crates/asist-app/src/main.rs index cc08e62..d70cd2a 100644 --- a/crates/asist-app/src/main.rs +++ b/crates/asist-app/src/main.rs @@ -1,8 +1,8 @@ -//! Asistente de voz local: escucha, piensa y responde hablando. +//! Local voice assistant: it listens, thinks and answers out loud. //! -//! Une tres motores que ya existen —Canary para oír, llama.cpp para pensar y -//! qwentts para hablar— en un pipeline de hilos y canales donde ninguna etapa -//! espera a la siguiente. +//! It joins three existing engines (Canary to hear, llama.cpp to think and +//! qwentts to speak) in a pipeline of threads and channels where no stage +//! waits for the next. mod pipeline; mod registry; @@ -38,7 +38,7 @@ fn main() -> Result<()> { init_logging(&args); let mut config = Config::load(&args.config) - .with_context(|| format!("no se pudo cargar {}", args.config.display()))?; + .with_context(|| format!("could not load {}", args.config.display()))?; args.apply(&mut config); match args.command { @@ -51,10 +51,10 @@ fn main() -> Result<()> { fn run(config: Config, args: &Args) -> Result<()> { let session = Session::new(); - // Los clientes se crean antes de arrancar nada: así se detecta lo que ya - // esté escuchando y no se levanta un servidor por duplicado. - // Compartido: el orquestador conversa con él y la herramienta de cámara - // lo usa para describir lo que capta. + // Clients are created before starting anything: that way whatever is + // already listening is detected and no server is started twice. + // Shared: the orchestrator converses with it and the camera tool uses it + // to describe what it captures. let llm = Arc::new(LlmClient::new(config.llm_authority(), &config.llm)); let tts = TtsClient::new(config.tts_authority(), &config.tts); let (llm_up, tts_up) = (llm.healthy(), tts.healthy()); @@ -63,32 +63,25 @@ fn run(config: Config, args: &Args) -> Result<()> { supervisor.start(&config, llm_up, tts_up)?; let timeout = Duration::from_secs(config.supervisor.startup_timeout_secs); - eprintln!("Esperando a los motores (hasta {} s)…", timeout.as_secs()); + eprintln!("Waiting for the engines (up to {} s)…", timeout.as_secs()); llm.wait_ready(timeout).map_err(|e| { - anyhow::anyhow!( - "{e}\nRevisa {}", - supervisor.log_path("llama-server").display() - ) - })?; - tts.wait_ready(timeout).map_err(|e| { - anyhow::anyhow!( - "{e}\nRevisa {}", - supervisor.log_path("tts-server").display() - ) + anyhow::anyhow!("{e}\nSee {}", supervisor.log_path("llama-server").display()) })?; + tts.wait_ready(timeout) + .map_err(|e| anyhow::anyhow!("{e}\nSee {}", supervisor.log_path("tts-server").display()))?; llm.warn_if_thinking_template(); if let Some(reference) = &config.tts.reference { tts.register_voice(reference) - .context("no se pudo registrar la voz clonada")?; + .context("could not register the cloned voice")?; } - // Se paga aquí la construcción de los grafos del sintetizador, unos 3,5 s - // que si no se los comería el primer turno de verdad. + // Building the synthesizer graphs is paid here, about 3.5 s that the + // first real turn would otherwise eat. if let Err(err) = tts.warmup() { - tracing::warn!(target: "tts", %err, "falló el precalentado; el primer turno irá lento"); + tracing::warn!(target: "tts", %err, "warmup failed; the first turn will be slow"); } - eprintln!("Cargando el modelo de voz a texto…"); + eprintln!("Loading the speech-to-text model…"); let recognizer = Recognizer::load(&config.asr)?; let playback = Playback::open(&config.audio)?; @@ -97,48 +90,48 @@ fn run(config: Config, args: &Args) -> Result<()> { let (tools, skipped) = registry::build(&config, &llm); if tools.is_empty() { - eprintln!("Herramientas: ninguna"); + eprintln!("Tools: none"); } else { - eprintln!("Herramientas: {}", tools.names().join(", ")); + eprintln!("Tools: {}", tools.names().join(", ")); } - // Lo que no se pudo activar se dice en voz alta, en vez de dejar al - // usuario preguntándose por qué el asistente no busca ni ve. + // Whatever could not be enabled is reported, instead of leaving the user + // wondering why the assistant does not search or see. for skip in &skipped { - eprintln!(" · {} no disponible: {}", skip.tool, skip.reason); + eprintln!(" · {} unavailable: {}", skip.tool, skip.reason); } - // Las dos capacidades que tocan algo fuera del proceso se anuncian: una - // enciende la cámara y la otra ejecuta órdenes. Quien lo arranca debería - // saberlo sin tener que leerse la configuración. + // The two capabilities that touch something outside the process are + // announced: one turns the camera on and the other runs commands. Whoever + // starts it should know without having to read the configuration. if tools.get("mirar_la_pantalla").is_some() { eprintln!( - "Captura de pantalla ACTIVA — a {} px{}", + "Screen capture ON — at {} px{}", config.screen.width, if config.screen.save_dir.is_empty() { String::new() } else { - format!(", guardando en {}", config.screen.save_dir) + format!(", saving to {}", config.screen.save_dir) } ); } if tools.get("mirar_por_la_camara").is_some() { eprintln!( - "Cámara ACTIVA — {} a {}x{}{}", + "Camera ON — {} at {}x{}{}", config.camera.device.display(), config.camera.width, config.camera.height, if config.camera.save_dir.is_empty() { String::new() } else { - format!(", guardando fotogramas en {}", config.camera.save_dir) + format!(", saving frames to {}", config.camera.save_dir) } ); } if config.tools.shell { eprintln!( - "Ejecución de órdenes ACTIVA — permitidas: {}{}", + "Command execution ON — allowed: {}{}", config.tools.shell_allowlist.join(", "), if config.tools.shell_dry_run { - " (simulación)" + " (dry run)" } else { "" } @@ -159,14 +152,14 @@ fn run(config: Config, args: &Args) -> Result<()> { )?; eprintln!( - "\nListo. Micrófono: {} · Altavoz: {} ({} Hz)\nHabla cuando quieras. Enter para salir.\n", + "\nReady. Microphone: {} · Speaker: {} ({} Hz)\nTalk whenever you like. Enter to quit.\n", capture.device_name, playback.device_name, playback.sample_rate ); - // Enter cierra, pero sólo si hay alguien delante para pulsarlo. Con la - // entrada redirigida —bajo systemd, en un contenedor, con `< /dev/null`— - // `read_line` devuelve EOF al instante y el asistente se cerraría nada - // más arrancar. En ese caso se espera a una señal. + // Enter quits, but only if someone is there to press it. With redirected + // input (under systemd, in a container, with `< /dev/null`) `read_line` + // returns EOF immediately and the assistant would quit right after + // starting. In that case it waits for a signal. if stdin_is_tty() { let session = session.clone(); std::thread::spawn(move || { @@ -175,7 +168,7 @@ fn run(config: Config, args: &Args) -> Result<()> { session.request_stop(); }); } else { - eprintln!("(entrada no interactiva: para cerrar, manda SIGINT o SIGTERM)"); + eprintln!("(non-interactive input: send SIGINT or SIGTERM to quit)"); install_signal_handler(session.clone()); } @@ -197,17 +190,17 @@ fn run(config: Config, args: &Args) -> Result<()> { if session.is_stopping() { break; } - // Si un servidor se muere a mitad de sesión, todos los turnos - // siguientes fallarían con un error de transporte sin explicar por - // qué. Mejor decirlo una vez, con el registro a mano, y cerrar. + // If a server dies mid-session, every following turn would fail with a + // transport error without saying why. Better to say it once, with the + // log at hand, and quit. if std::time::Instant::now() >= next_health_check { next_health_check = std::time::Instant::now() + Duration::from_secs(5); if let Some((name, code)) = supervisor.crashed() { renderer.finish(); eprintln!( - "\n{name} ha terminado inesperadamente (código {}). Revisa {}", + "\n{name} exited unexpectedly (code {}). See {}", code.map(|c| c.to_string()) - .unwrap_or_else(|| "desconocido".into()), + .unwrap_or_else(|| "unknown".into()), supervisor.log_path(name).display() ); break; @@ -227,18 +220,18 @@ fn run(config: Config, args: &Args) -> Result<()> { Ok(()) } -/// Lista los dispositivos de audio, para poder nombrarlos en la configuración. +/// Lists the audio devices, so they can be named in the configuration. fn devices() -> Result<()> { use asist_audio::describe; use cpal_reexport::traits::HostTrait; let host = cpal_reexport::default_host(); let default_in = host.default_input_device().map(|d| describe(&d)); - println!("Entradas:"); + println!("Inputs:"); for device in host.input_devices()? { let name = describe(&device); let mark = if Some(&name) == default_in.as_ref() { - " (por defecto)" + " (default)" } else { "" }; @@ -246,11 +239,11 @@ fn devices() -> Result<()> { } let default_out = host.default_output_device().map(|d| describe(&d)); - println!("\nSalidas:"); + println!("\nOutputs:"); for device in host.output_devices()? { let name = describe(&device); let mark = if Some(&name) == default_out.as_ref() { - " (por defecto)" + " (default)" } else { "" }; @@ -259,117 +252,117 @@ fn devices() -> Result<()> { Ok(()) } -/// Comprueba que está todo en su sitio, sin abrir el micrófono. +/// Checks that everything is in place, without opening the microphone. fn check(config: &Config) -> Result<()> { let mut problems = Vec::new(); let ok = |label: &str, detail: String| println!(" ok {label:<22} {detail}"); let paths: [(&str, &std::path::Path); 6] = [ - ("modelo asr", &config.asr.model_dir), - ("binario llama", &config.supervisor.llama.binary), - ("modelo llm", &config.supervisor.llama.model), - ("plantilla chat", &config.supervisor.llama.chat_template), - ("binario tts", &config.supervisor.tts.binary), - ("modelo tts", &config.supervisor.tts.model), + ("asr model", &config.asr.model_dir), + ("llama binary", &config.supervisor.llama.binary), + ("llm model", &config.supervisor.llama.model), + ("chat template", &config.supervisor.llama.chat_template), + ("tts binary", &config.supervisor.tts.binary), + ("tts model", &config.supervisor.tts.model), ]; - println!("Ficheros:"); + println!("Files:"); for (label, path) in paths { if path.exists() { ok(label, path.display().to_string()); } else { - println!(" FALTA {label:<22} {}", path.display()); - problems.push(format!("falta {label}: {}", path.display())); + println!(" MISSING {label:<20} {}", path.display()); + problems.push(format!("missing {label}: {}", path.display())); } } if let Some(reference) = &config.tts.reference { - println!("Voz de referencia «{}»:", reference.name); + println!("Reference voice «{}»:", reference.name); for (label, path) in [ - ("hablante (.spk)", &reference.speaker), - ("códigos (.rvq)", &reference.codes), - ("transcripción", &reference.transcript), + ("speaker (.spk)", &reference.speaker), + ("codes (.rvq)", &reference.codes), + ("transcript", &reference.transcript), ] { if path.exists() { ok(label, path.display().to_string()); } else { - println!(" FALTA {label:<22} {}", path.display()); - problems.push(format!("falta {label}")); + println!(" MISSING {label:<20} {}", path.display()); + problems.push(format!("missing {label}")); } } } - println!("Capacidades:"); - // Ninguna de las dos es fatal: el asistente conversa igual sin ellas, así - // que se informa y no se cuentan como problema. + println!("Capabilities:"); + // Neither is fatal: the assistant converses just the same without them, + // so they are reported and not counted as problems. if !config.search.enabled { - println!(" - búsqueda desactivada en la configuración"); + println!(" - search disabled in the configuration"); } else { match config.search.backend.trim().to_lowercase().as_str() { "tavily" => match std::env::var(&config.search.api_key_env) { Ok(key) if !key.trim().is_empty() => ok( - "búsqueda (tavily)", - format!("clave en ${}", config.search.api_key_env), + "search (tavily)", + format!("key in ${}", config.search.api_key_env), ), _ => println!( - " - búsqueda (tavily) falta ${}", + " - search (tavily) missing ${}", config.search.api_key_env ), }, - "ddgs" | "comando" => match config.search.command.first() { - // Un nombre suelto se resuelve por el PATH; una ruta tiene que - // existir, y más vale decirlo aquí que a mitad de una pregunta. + "ddgs" | "command" | "comando" => match config.search.command.first() { + // A bare name is resolved through PATH; a path must exist, and + // better to say so here than in the middle of a question. Some(program) if !program.contains('/') || PathBuf::from(program).exists() => { - ok("búsqueda (ddgs)", config.search.command.join(" ")) + ok("search (ddgs)", config.search.command.join(" ")) } Some(program) => { - println!(" - búsqueda (ddgs) no existe {program}") + println!(" - search (ddgs) {program} does not exist") } - None => println!(" - búsqueda (ddgs) search.command está vacío"), + None => println!(" - search (ddgs) search.command is empty"), }, - "searxng" => ok("búsqueda (searxng)", config.search.base_url.clone()), - other => println!(" - búsqueda backend desconocido: «{other}»"), + "searxng" => ok("search (searxng)", config.search.base_url.clone()), + other => println!(" - search unknown backend: «{other}»"), } } if !config.screen.enabled { - println!(" - pantalla desactivada en la configuración"); + println!(" - screen disabled in the configuration"); } else { match config.screen.command.first() { Some(program) if !program.contains('/') || PathBuf::from(program).exists() => ok( - "pantalla", + "screen", format!("{} px · {}", config.screen.width, program), ), - Some(program) => println!(" - pantalla no existe {program}"), - None => println!(" - pantalla screen.command está vacío"), + Some(program) => println!(" - screen {program} does not exist"), + None => println!(" - screen screen.command is empty"), } if std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none() { - println!(" - sesión gráfica no hay; la pantalla no se podrá capturar"); + println!(" - graphical session none; the screen cannot be captured"); } } if !config.camera.enabled { - println!(" - cámara desactivada en la configuración"); + println!(" - camera disabled in the configuration"); } else if config.camera.device.exists() { ok( - "cámara", + "camera", format!( - "{} a {}x{}", + "{} at {}x{}", config.camera.device.display(), config.camera.width, config.camera.height ), ); if which("ffmpeg").is_none() { - println!(" - ffmpeg no está; hace falta para capturar"); + println!(" - ffmpeg missing; needed to capture"); } } else { println!( - " - cámara no existe {}", + " - camera {} does not exist", config.camera.device.display() ); } - println!("Servidores:"); + println!("Servers:"); let llm = LlmClient::new(config.llm_authority(), &config.llm); let tts = TtsClient::new(config.tts_authority(), &config.tts); for (label, up, authority) in [ @@ -387,56 +380,56 @@ fn check(config: &Config) -> Result<()> { if tts.healthy() { match tts.voices() { Ok(voices) if voices.contains(&config.tts.voice) => { - println!(" ok voz «{}» registrada", config.tts.voice); + println!(" ok voice «{}» registered", config.tts.voice); } Ok(voices) => println!( - " - voz «{}» aún no registrada (hay: {})", + " - voice «{}» not registered yet (available: {})", config.tts.voice, voices.join(", ") ), - Err(err) => println!(" - no se pudieron listar las voces: {err}"), + Err(err) => println!(" - could not list the voices: {err}"), } } if problems.is_empty() { - println!("\nTodo listo."); + println!("\nAll set."); Ok(()) } else { bail!( - "{} problema(s):\n - {}", + "{} problem(s):\n - {}", problems.len(), problems.join("\n - ") ) } } -// cpal se usa aquí sólo para listar dispositivos; llega a través de asist-audio. +// cpal is only used here to list devices; it comes through asist-audio. use asist_audio::cpal as cpal_reexport; const USAGE: &str = "\ -asistente — asistente de voz local - -USO: - asistente [ORDEN] [OPCIONES] - -ÓRDENES: - run Escucha y responde (por defecto) - check Comprueba ficheros, modelos y servidores, y sale - devices Lista los dispositivos de audio - -OPCIONES: - -c, --config Fichero de configuración [config/asistente.toml] - --log-dir Carpeta de los registros de los servidores [logs/] - --voice Voz del sintetizador - --no-manage No lanzar los servidores; suponerlos ya arriba - --no-partials Sin transcripción provisional (ahorra CPU) - --barge-in Permitir cortar al asistente hablando encima - --shell Activar la ejecución de órdenes del sistema - -v, --verbose Registro de depuración (equivale a RUST_LOG=debug) - -h, --help Esta ayuda +asistente — local voice assistant + +USAGE: + asistente [COMMAND] [OPTIONS] + +COMMANDS: + run Listen and answer (default) + check Check files, models and servers, then exit + devices List the audio devices + +OPTIONS: + -c, --config Configuration file [config/asistente.toml] + --log-dir Directory for the server logs [logs/] + --voice Synthesizer voice + --no-manage Do not start the servers; assume they are up + --no-partials No partial transcription (saves CPU) + --barge-in Allow interrupting the assistant by talking over it + --shell Enable system command execution + -v, --verbose Debug logging (same as RUST_LOG=debug) + -h, --help This help VARIABLES: - RUST_LOG Filtro de registro, p. ej. «info,tts=debug,latencia=info» + RUST_LOG Log filter, e.g. «info,tts=debug,latency=info» "; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -480,25 +473,23 @@ impl Args { "check" => args.command = Command::Check, "devices" => args.command = Command::Devices, "-c" | "--config" => { - args.config = raw.next().context("--config necesita una ruta")?.into() - } - "--log-dir" => { - args.log_dir = raw.next().context("--log-dir necesita una ruta")?.into() + args.config = raw.next().context("--config needs a path")?.into() } - "--voice" => args.voice = Some(raw.next().context("--voice necesita un nombre")?), + "--log-dir" => args.log_dir = raw.next().context("--log-dir needs a path")?.into(), + "--voice" => args.voice = Some(raw.next().context("--voice needs a name")?), "--no-manage" => args.no_manage = true, "--no-partials" => args.no_partials = true, "--barge-in" => args.barge_in = true, "--shell" => args.shell = true, "-v" | "--verbose" => args.verbose = true, "-h" | "--help" => args.help = true, - other => bail!("opción desconocida: {other}\n\n{USAGE}"), + other => bail!("unknown option: {other}\n\n{USAGE}"), } } Ok(args) } - /// La línea de órdenes manda sobre el fichero. + /// The command line wins over the file. fn apply(&self, config: &mut Config) { if let Some(voice) = &self.voice { config.tts.voice = voice.clone(); @@ -518,8 +509,8 @@ impl Args { } } -/// Busca un ejecutable en el PATH, para avisar de lo que falta antes de que -/// falle a mitad de una conversación. +/// Looks an executable up in PATH, to warn about what is missing before it +/// fails in the middle of a conversation. fn which(program: &str) -> Option { std::env::var_os("PATH").and_then(|path| { std::env::split_paths(&path) @@ -528,7 +519,7 @@ fn which(program: &str) -> Option { }) } -/// ¿Hay una persona al otro lado de la entrada estándar? +/// Is there a person on the other side of standard input? fn stdin_is_tty() -> bool { #[cfg(unix)] { @@ -541,10 +532,10 @@ fn stdin_is_tty() -> bool { true } -/// Cierre ordenado ante SIGINT/SIGTERM. +/// Orderly shutdown on SIGINT/SIGTERM. /// -/// Importa más de lo que parece: matar el proceso en seco deja a los -/// servidores hijos con la memoria de la GPU tomada. +/// It matters more than it seems: killing the process outright leaves the +/// child servers holding GPU memory. #[cfg(unix)] fn install_signal_handler(session: Session) { use std::sync::OnceLock; @@ -570,9 +561,9 @@ fn install_signal_handler(_session: Session) {} fn init_logging(args: &Args) { use tracing_subscriber::{fmt, EnvFilter}; - // ONNX Runtime informa a nivel INFO de cada transformación del grafo: - // varios cientos de líneas que tapan la transcripción. Se silencia salvo - // que se pida expresamente por RUST_LOG. + // ONNX Runtime logs every graph transformation at INFO level: several + // hundred lines that bury the transcription. It is silenced unless + // explicitly requested through RUST_LOG. let default = if args.verbose { "debug,ort=warn" } else { @@ -581,8 +572,8 @@ fn init_logging(args: &Args) { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default)); fmt() .with_env_filter(filter) - // Al terminal de texto van la transcripción y la respuesta; el registro - // va a stderr para que se puedan separar con una redirección. + // The transcription and the answer go to the text terminal; the log + // goes to stderr so they can be separated with a redirection. .with_writer(std::io::stderr) .with_target(true) .without_time() -- cgit v1.2.3