aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-tts
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
committerelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
commit8518a63f55153e7f45fd49ad6caff5555f4e374f (patch)
tree636684eea3fa6f35d78282ab95eb687ef49154af /crates/asist-tts
parent69de76dc9cbedc6092d1e5ce84094a8030de1470 (diff)
downloadasist-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.rs85
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());
}
}