diff options
| author | elvis <elvis@claros.ar> | 2026-09-26 20:20:19 -0300 |
|---|---|---|
| committer | elvis <elvis@claros.ar> | 2026-09-26 20:20:19 -0300 |
| commit | 8518a63f55153e7f45fd49ad6caff5555f4e374f (patch) | |
| tree | 636684eea3fa6f35d78282ab95eb687ef49154af /crates/asist-tts | |
| parent | 69de76dc9cbedc6092d1e5ce84094a8030de1470 (diff) | |
| download | asist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.tar.gz asist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.zip | |
Translate code, comments, logs and terminal UI to English; add English README; rename scriptsHEADmain
Diffstat (limited to 'crates/asist-tts')
| -rw-r--r-- | crates/asist-tts/src/lib.rs | 85 |
1 files changed, 42 insertions, 43 deletions
diff --git a/crates/asist-tts/src/lib.rs b/crates/asist-tts/src/lib.rs index b250116..6962a7f 100644 --- a/crates/asist-tts/src/lib.rs +++ b/crates/asist-tts/src/lib.rs @@ -1,11 +1,11 @@ -//! Síntesis de voz sobre qwentts.cpp. +//! Speech synthesis on top of qwentts.cpp. //! -//! Se habla con `tts-server` por HTTP en lugar de invocar el binario -//! `qwen-tts` una vez por frase. La diferencia no es de estilo: el proceso -//! carga 2,1 GB de modelo hablante más 291 MB de codec, y pagar esa carga en -//! cada frase pondría varios segundos delante de cada respuesta. El servidor -//! los mantiene residentes en la GPU y responde en formato `pcm`, que llega -//! troceado según se genera. +//! It talks to `tts-server` over HTTP instead of invoking the `qwen-tts` +//! binary once per sentence. The difference is not a matter of style: the +//! process loads 2.1 GB of talker model plus 291 MB of codec, and paying that +//! load on every sentence would put several seconds in front of every answer. +//! The server keeps them resident on the GPU and answers in `pcm` format, +//! which arrives in chunks as it is generated. use std::time::{Duration, Instant}; @@ -16,13 +16,13 @@ use asist_core::config::{ReferenceVoice, TtsConfig}; use asist_core::error::{Error, Result}; use asist_core::http::{Cancel, HttpClient}; -/// Frecuencia a la que sintetiza el modelo. +/// Rate the model synthesizes at. pub const SAMPLE_RATE: u32 = 24_000; -/// Cómo terminó una síntesis. +/// How a synthesis ended. #[derive(Debug, Clone, Default)] pub struct SpeechOutcome { - /// Tiempo hasta el primer bloque de audio. + /// Time until the first audio block. pub ttfb: Option<Duration>, pub total: Duration, pub samples: usize, @@ -34,8 +34,8 @@ impl SpeechOutcome { self.samples as f32 / SAMPLE_RATE as f32 } - /// Múltiplo del tiempo real. Por encima de 1,0 el sintetizador va más - /// lento que el habla y la cola de reproducción acabará vaciándose. + /// Multiple of real time. Above 1.0 the synthesizer is slower than + /// speech and the playback queue will eventually run dry. pub fn rtf(&self) -> f32 { let audio = self.audio_secs(); if audio <= 0.0 { @@ -72,33 +72,32 @@ impl TtsClient { std::thread::sleep(Duration::from_millis(500)); } Err(Error::Tts(format!( - "{} no respondió a /health en {} s", + "{} did not answer /health within {} s", self.http.authority, timeout.as_secs() ))) } - /// Registra una voz clonada a partir de los latentes de `qwen-codec`. + /// Registers a cloned voice from the `qwen-codec` latents. /// - /// Se mandan los `.spk` y `.rvq` ya extraídos en lugar del WAV original: - /// el servidor los toma tal cual y se ahorra volver a analizar la - /// referencia en cada arranque. + /// The already extracted `.spk` and `.rvq` are sent instead of the original + /// WAV: the server takes them as they are and does not have to analyze the + /// reference again on every start. pub fn register_voice(&self, voice: &ReferenceVoice) -> Result<()> { let read = |path: &std::path::Path, what: &str| -> Result<Vec<u8>> { - std::fs::read(path).map_err(|e| { - Error::Tts(format!("no se pudo leer {what} ({}): {e}", path.display())) - }) + std::fs::read(path) + .map_err(|e| Error::Tts(format!("could not read {what} ({}): {e}", path.display()))) }; let b64 = base64::engine::general_purpose::STANDARD; - let speaker = b64.encode(read(&voice.speaker, "el embedding del hablante")?); - let codes = b64.encode(read(&voice.codes, "los códigos de referencia")?); - let transcript = String::from_utf8_lossy(&read(&voice.transcript, "la transcripción")?) + let speaker = b64.encode(read(&voice.speaker, "the speaker embedding")?); + let codes = b64.encode(read(&voice.codes, "the reference codes")?); + let transcript = String::from_utf8_lossy(&read(&voice.transcript, "the transcript")?) .trim() .to_string(); if transcript.is_empty() { return Err(Error::Tts(format!( - "la transcripción de referencia {} está vacía; sin ella no se activa el clonado", + "the reference transcript {} is empty; without it cloning is not enabled", voice.transcript.display() ))); } @@ -110,11 +109,11 @@ impl TtsClient { "rvq_b64": codes, }); self.http.post_json("/v1/audio/voices", &body)?; - tracing::info!(target: "tts", voz = %voice.name, "voz clonada registrada"); + tracing::info!(target: "tts", speech = %voice.name, "voz clonada registrada"); Ok(()) } - /// Voces que el servidor conoce ahora mismo. + /// Voices the server knows right now. pub fn voices(&self) -> Result<Vec<String>> { let body = self.http.get("/v1/audio/voices")?; let parsed: serde_json::Value = serde_json::from_slice(&body)?; @@ -135,12 +134,12 @@ impl TtsClient { .unwrap_or_default()) } - /// Sintetiza `text` y entrega el audio a trozos según se genera. + /// Synthesizes `text` and delivers the audio in pieces as it is generated. /// - /// `on_audio` recibe muestras mono f32 a 24 kHz y devuelve `false` para - /// cortar; `cancel` hace lo propio desde otro hilo. Ese corte es lo que - /// permite callar de golpe cuando el usuario interrumpe, sin esperar a que - /// termine de generarse una frase que ya no se va a oír. + /// `on_audio` gets mono f32 samples at 24 kHz and returns `false` to stop; + /// `cancel` does the same from another thread. That cut is what allows going + /// silent at once when the user interrupts, without waiting for a sentence + /// that will no longer be heard to finish generating. pub fn speak( &self, text: &str, @@ -160,9 +159,9 @@ impl TtsClient { let started = Instant::now(); let mut outcome = SpeechOutcome::default(); - // El servidor no garantiza que cada bloque traiga un número par de - // bytes, así que el byte suelto de un bloque espera al siguiente en - // vez de desplazar todas las muestras posteriores. + // The server does not guarantee each block carries an even number of + // bytes, so a stray byte from one block waits for the next instead of + // shifting every later sample. let mut odd: Option<u8> = None; let mut samples = Vec::with_capacity(8192); let mut stopped = false; @@ -208,26 +207,26 @@ impl TtsClient { tracing::debug!( target: "tts", - caracteres = text.chars().count(), + chars = text.chars().count(), ttfb_ms = outcome.ttfb.map(|d| d.as_millis()).unwrap_or(0), audio_s = outcome.audio_secs(), rtf = outcome.rtf(), - cancelada = outcome.cancelled, - "síntesis" + cancelled = outcome.cancelled, + "synthesis" ); Ok(outcome) } - /// Sintetiza una frase corta para pagar por adelantado la construcción de - /// los grafos: la primera petición cuesta unos 3,5 s más que las - /// siguientes, y no conviene gastarlos en el primer turno de verdad. + /// Synthesizes a short sentence to pay upfront for building the graphs: + /// the first request costs about 3.5 s more than the following ones, and + /// they are better not spent on the first real turn. pub fn warmup(&self) -> Result<()> { if !self.config.warmup { return Ok(()); } let started = Instant::now(); let cancel = Cancel::new(); - // El audio se tira: sólo interesa el efecto secundario. + // The audio is thrown away: only the side effect matters. self.speak("Listo.", &cancel, |_| true)?; tracing::info!(target: "tts", ms = started.elapsed().as_millis(), "precalentado"); Ok(()) @@ -239,7 +238,7 @@ mod tests { use super::*; #[test] - fn el_rtf_relaciona_tiempo_de_calculo_y_audio() { + fn rtf_relates_compute_time_and_audio() { let outcome = SpeechOutcome { total: Duration::from_secs(2), samples: SAMPLE_RATE as usize * 4, @@ -250,7 +249,7 @@ mod tests { } #[test] - fn sin_audio_el_rtf_es_infinito_en_vez_de_dividir_por_cero() { + fn without_audio_rtf_is_infinite_instead_of_dividing_by_zero() { assert!(SpeechOutcome::default().rtf().is_infinite()); } } |