first
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
//! Kokoro TTS plugin (stub).
|
||||
//!
|
||||
//! Kokoro is a lightweight, open-source TTS model. This stub shows the pattern for
|
||||
//! adding a new engine plugin: implement [`TtsEngine`] and wire the HTTP or
|
||||
//! local inference calls.
|
||||
//!
|
||||
//! # Configuration
|
||||
//! Set `TTS_ENGINE=kokoro` and provide:
|
||||
//! - `KOKORO_URL` — base URL of the Kokoro HTTP server (default: `http://localhost:8880`)
|
||||
//! - `KOKORO_VOICE` — voice name (default: `af_heart`)
|
||||
//!
|
||||
//! # Resampling note
|
||||
//! Kokoro typically outputs at 24 kHz. Since the mixer runs at
|
||||
//! [`crate::config::OUTPUT_SAMPLE_RATE`] (44.1 kHz), this plugin must
|
||||
//! resample internally before pushing samples. A basic linear resampler is
|
||||
//! provided below; replace with a polyphase/FIR resampler for higher quality.
|
||||
|
||||
use crate::audio::SampleRingBuffer;
|
||||
use crate::config::{Config, OUTPUT_SAMPLE_RATE};
|
||||
use crate::tts::TtsEngine;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::Arc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::info;
|
||||
|
||||
/// Kokoro native output sample rate (Hz).
|
||||
const KOKORO_SAMPLE_RATE: u32 = 24_000;
|
||||
|
||||
/// Kokoro TTS plugin.
|
||||
#[derive(Clone)]
|
||||
pub struct KokoroTts {
|
||||
endpoint: String,
|
||||
voice: String,
|
||||
http: reqwest::Client,
|
||||
_config: Arc<Config>,
|
||||
}
|
||||
|
||||
impl KokoroTts {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
let endpoint = std::env::var("KOKORO_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8880".to_string());
|
||||
let voice = std::env::var("KOKORO_VOICE")
|
||||
.unwrap_or_else(|_| "af_heart".to_string());
|
||||
info!(endpoint = %endpoint, voice = %voice, "Kokoro TTS plugin initialized");
|
||||
Self {
|
||||
endpoint,
|
||||
voice,
|
||||
http: reqwest::Client::builder()
|
||||
.build()
|
||||
.expect("failed to build reqwest client"),
|
||||
_config: Arc::new(config.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TtsEngine for KokoroTts {
|
||||
fn name(&self) -> &str {
|
||||
"kokoro"
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> u32 {
|
||||
// We resample to the mixer rate before pushing.
|
||||
OUTPUT_SAMPLE_RATE
|
||||
}
|
||||
|
||||
async fn synthesize(
|
||||
&self,
|
||||
text: &str,
|
||||
tts_queue: &SampleRingBuffer,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<()> {
|
||||
// TODO: implement the actual Kokoro HTTP streaming call.
|
||||
//
|
||||
// Example request (adjust to be completed by the user):
|
||||
//
|
||||
// ```ignore
|
||||
// let resp = self.http
|
||||
// .post(format!("{}/v1/audio/speech", self.endpoint))
|
||||
// .json(&serde_json::json!({
|
||||
// "model": "kokoro",
|
||||
// "input": text,
|
||||
// "voice": self.voice,
|
||||
// "response_format": "pcm",
|
||||
// "stream": true,
|
||||
// }))
|
||||
// .send()
|
||||
// .await?;
|
||||
//
|
||||
// let mut stream = resp.bytes_stream();
|
||||
// while let Some(chunk) = stream.next().await {
|
||||
// if cancel.is_cancelled() { break; }
|
||||
// let bytes = chunk?;
|
||||
// // Kokoro outputs 16-bit mono PCM at 24 kHz.
|
||||
// let samples = pcm_to_i16(&bytes);
|
||||
// let resampled = resample_24k_to_output(&samples);
|
||||
// tts_queue.push(&resampled);
|
||||
// }
|
||||
// ```
|
||||
let _ = (text, tts_queue, cancel);
|
||||
anyhow::bail!(
|
||||
"Kokoro TTS plugin is not yet implemented — \
|
||||
complete the HTTP streaming call in src/tts/kokoro.rs"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resample a 24 kHz mono `i16` buffer to [`OUTPUT_SAMPLE_RATE` using linear
|
||||
/// interpolation. Replace with a windowed-sinc FIR resampler for higher fidelity.
|
||||
#[allow(dead_code)]
|
||||
fn resample_24k_to_output(samples: &[i16]) -> Vec<i16> {
|
||||
if samples.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let ratio = OUTPUT_SAMPLE_RATE as f64 / KOKORO_SAMPLE_RATE as f64;
|
||||
let out_len = (samples.len() as f64 * ratio).round() as usize;
|
||||
let mut out = Vec::with_capacity(out_len);
|
||||
for i in 0..out_len {
|
||||
let src_pos = i as f64 / ratio;
|
||||
let idx = src_pos.floor() as usize;
|
||||
let frac = src_pos - idx as f64;
|
||||
let a = samples[idx.min(samples.len() - 1)] as f32;
|
||||
let b = samples[(idx + 1).min(samples.len() - 1)] as f32;
|
||||
out.push((a + (b - a) * frac as f32).round() as i16);
|
||||
}
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user