This commit is contained in:
valenti
2026-10-17 15:49:20 +01:00
parent 430997bd2e
commit bfed42c3b3
124 changed files with 5646 additions and 249 deletions
+57 -2
View File
@@ -5,6 +5,12 @@
//! mixer's output rate, so samples are pushed directly into the TTS queue
//! without resampling. A [`CancellationToken`] allows immediate interruption
//! (barge-in).
//!
//! Drama 3 can sound overly theatrical on some references. Two mitigations are
//! applied here, both configurable from the environment: optional sampling
//! knobs (`temperature` / `top_p` / `top_k`) that trim the tails of the
//! acoustic token distribution, and a neutral style tag prepended to untagged
//! requests so plain LLM prose is read in a conversational register.
use crate::audio::SampleRingBuffer;
use crate::config::Config;
@@ -26,6 +32,11 @@ struct Reference {
}
/// Request body for the SGLang `/v1/audio/speech` endpoint.
///
/// Sampling knobs are optional and only serialized when set. Drama 3 accepts
/// `temperature`, `top_p` and `top_k` at this level; `repetition_penalty` is
/// deliberately never sent because this server rejects any value other than
/// `1.0` (`Drama 3 request: repetition_penalty must be 1.0`).
#[derive(Debug, Serialize)]
struct SpeechRequest<'a> {
model: &'a str,
@@ -35,6 +46,12 @@ struct SpeechRequest<'a> {
stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
references: Option<Vec<Reference>>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
top_k: Option<u32>,
}
/// Streaming TTS client backed by Fish Audio / SGLang-Omni Drama 3.
@@ -46,6 +63,12 @@ pub struct FishAudioTts {
voice: String,
/// Current voice-cloning reference (config default, overridable at runtime).
reference: Arc<parking_lot::RwLock<Option<Reference>>>,
/// Optional sampling overrides (de-emphasis of the recitative register).
temperature: Option<f32>,
top_p: Option<f32>,
top_k: Option<u32>,
/// Neutral style tag prepended to untagged synthesis requests.
style_tag: String,
}
impl FishAudioTts {
@@ -61,6 +84,13 @@ impl FishAudioTts {
"TTS voice-cloning reference configured"
);
}
info!(
temperature = ?config.tts_temperature,
top_p = ?config.tts_top_p,
top_k = ?config.tts_top_k,
style_tag = %config.tts_style_tag,
"TTS de-emphasis settings"
);
Self {
http: Client::builder()
.build()
@@ -69,6 +99,23 @@ impl FishAudioTts {
model: config.tts_model.clone(),
voice: config.tts_voice.clone(),
reference: Arc::new(parking_lot::RwLock::new(reference)),
temperature: config.tts_temperature,
top_p: config.tts_top_p,
top_k: config.tts_top_k,
style_tag: config.tts_style_tag.clone(),
}
}
/// The text actually sent to the model: the configured neutral style tag is
/// prepended unless the text already opens with a style tag or the tag is
/// disabled. LLM replies carry no tags, so they get de-emphasized; clip
/// banks that start with their own tag are left untouched.
fn styled<'a>(&self, text: &'a str) -> std::borrow::Cow<'a, str> {
let tag = self.style_tag.trim();
if tag.is_empty() || text.trim_start().starts_with('[') {
std::borrow::Cow::Borrowed(text)
} else {
std::borrow::Cow::Owned(format!("{tag} {text}"))
}
}
@@ -107,14 +154,18 @@ impl TtsEngine for FishAudioTts {
cancel: &CancellationToken,
) -> Result<()> {
let references = self.reference.read().clone().map(|r| vec![r]);
let input = self.styled(text);
let body = SpeechRequest {
model: &self.model,
input: text,
input: &input,
voice: &self.voice,
response_format: "pcm",
stream: true,
references,
temperature: self.temperature,
top_p: self.top_p,
top_k: self.top_k,
};
info!(model = %self.model, text_len = text.len(), "SGLang TTS request");
@@ -125,7 +176,11 @@ impl TtsEngine for FishAudioTts {
response_format = "pcm",
stream = true,
has_reference = body.references.is_some(),
text = %text,
temperature = ?self.temperature,
top_p = ?self.top_p,
top_k = ?self.top_k,
styled = input.len() != text.len(),
text = %input,
"TTS request dispatched"
);