use crate::error::FttsError;
use ftts_core::{
CancellationToken, EngineError, FrameGenerator, GenerationError, NormalizationOptions,
NormalizationTrace, PreparedText, SynthesisObserver, SynthesisRequest, TextPreparationError,
TextPreparer, TtsEngine,
};
use ftts_model_qwen::checkpoint::{
CODEC_LANGUAGE_ENGLISH_ID, CheckpointError, CodecCheckpoint, TALKER_HIDDEN, TalkerCheckpoint,
};
use ftts_model_qwen::generate::{QwenGenerator, QwenGeneratorConfig};
use ftts_model_qwen::microdecoder::MicrodecoderConfig;
use ftts_model_qwen::prompt::{CloneMode, PromptMode};
use ftts_model_qwen::sampler::SamplingMode;
use ftts_model_qwen::speaker::{
Encoder as SpeakerEncoder, SPEAKER_SAMPLE_RATE_HZ, log_mel_from_24khz_pcm,
};
use ftts_model_qwen::talker::TalkerConfig;
use ftts_model_qwen::tokenizer::{QwenTokenizer, TokenizerFiles};
use std::fs;
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use symphonia::default::{get_codecs, get_probe};
pub const SPEAKER_VECTOR_BYTES: usize = TALKER_HIDDEN * 4;
const CANONICAL_MODEL_BASENAME: &str = "qwen3-tts-12hz-0.6b-base.fttsq";
fn checkpoint_error(error: CheckpointError) -> FttsError {
FttsError::ArtifactFormat(error.to_string())
}
#[derive(Clone, Debug)]
pub struct ModelBundle {
pub root: PathBuf,
pub main: PathBuf,
pub canonical_main: Option<PathBuf>,
pub codec: PathBuf,
}
impl ModelBundle {
pub fn resolve(model: &Path) -> Result<Self, FttsError> {
let root = if model.is_dir() {
model.to_path_buf()
} else {
model
.parent()
.ok_or_else(|| {
FttsError::ModelNotFound(format!(
"model path {} has no parent directory",
model.display()
))
})?
.to_path_buf()
};
let canonical_main = if model.is_dir() {
let canonical = root.join(CANONICAL_MODEL_BASENAME);
if canonical.is_file() {
Some(canonical)
} else {
None
}
} else if model.extension().and_then(|extension| extension.to_str()) == Some("fttsq") {
Some(model.to_path_buf())
} else {
None
};
let main = root.join("model.safetensors");
let codec = root.join("speech_tokenizer/model.safetensors");
let (main_label, main_path) = match canonical_main.as_ref() {
Some(path) => ("canonical talker artifact", path),
None => ("talker checkpoint", &main),
};
for (label, path) in [
(main_label, main_path),
("codec checkpoint", &codec),
("tokenizer vocabulary", &root.join("vocab.json")),
("tokenizer merges", &root.join("merges.txt")),
("tokenizer config", &root.join("tokenizer_config.json")),
] {
if !path.is_file() {
return Err(FttsError::ModelNotFound(format!(
"{label} is missing at {}; `ftts say` needs a complete model directory \
({CANONICAL_MODEL_BASENAME} or model.safetensors, \
speech_tokenizer/model.safetensors, \
vocab.json, merges.txt, tokenizer_config.json)",
path.display()
)));
}
}
Ok(Self {
root,
main,
canonical_main,
codec,
})
}
}
pub struct LoadedModel {
talker: TalkerCheckpoint,
codec: CodecCheckpoint,
tokenizer: QwenTokenizer,
artifact: Option<std::sync::Arc<ftts_artifacts::fttsq::MappedFttsq>>,
}
impl LoadedModel {
pub fn load(bundle: &ModelBundle) -> Result<Self, FttsError> {
let read = |name: &str| -> Result<String, FttsError> {
let path = bundle.root.join(name);
fs::read_to_string(&path).map_err(|error| {
FttsError::ArtifactFormat(format!("cannot read {}: {error}", path.display()))
})
};
let vocab = read("vocab.json")?;
let merges = read("merges.txt")?;
let config = read("tokenizer_config.json")?;
let (talker, codec, tokenizer) = std::thread::scope(|scope| {
let codec = scope.spawn(|| CodecCheckpoint::load(&bundle.codec));
let tokenizer = scope.spawn(|| {
QwenTokenizer::from_files_using_environment(TokenizerFiles {
vocab_json: &vocab,
merges_txt: &merges,
tokenizer_config_json: &config,
})
});
let talker = match bundle.canonical_main.as_deref() {
Some(path) => TalkerCheckpoint::load_fttsq_elided(
path,
ftts_model_qwen::generate::hot_elision_from_environment(),
),
None => TalkerCheckpoint::load(&bundle.main),
};
(
talker,
codec.join().expect("codec loader panicked"),
tokenizer.join().expect("tokenizer builder panicked"),
)
});
let tokenizer = tokenizer
.map_err(|error| FttsError::ArtifactFormat(format!("tokenizer unusable: {error}")))?;
let talker = talker.map_err(checkpoint_error)?;
let artifact = talker.artifact().cloned();
Ok(Self {
talker,
codec: codec.map_err(checkpoint_error)?,
tokenizer,
artifact,
})
}
}
pub fn read_speaker_vector(path: &Path) -> Result<Vec<f32>, FttsError> {
let bytes = fs::read(path).map_err(|error| {
FttsError::Input(format!(
"cannot read speaker vector {}: {error}",
path.display()
))
})?;
if bytes.len() != SPEAKER_VECTOR_BYTES {
return Err(FttsError::Input(format!(
"speaker vector {} is {} bytes; `ftts say --voice` expects exactly {} \
({TALKER_HIDDEN} little-endian f32)",
path.display(),
bytes.len(),
SPEAKER_VECTOR_BYTES
)));
}
let vector: Vec<f32> = bytes
.as_chunks::<4>()
.0
.iter()
.map(|quad| f32::from_le_bytes(*quad))
.collect();
if let Some(index) = vector.iter().position(|value| !value.is_finite()) {
return Err(FttsError::Input(format!(
"speaker vector {} holds a non-finite value at index {index}; it would poison every \
prefill position it is summed into",
path.display()
)));
}
Ok(vector)
}
#[derive(Clone, Copy, Debug)]
pub struct DenoiseReport {
pub before_dbfs: f32,
pub after_dbfs: f32,
}
#[derive(Default)]
pub struct ReferenceCleanup<'a> {
pub denoise: Option<&'a mut Option<DenoiseReport>>,
pub dereverb: Option<&'a mut Option<DereverbReport>>,
}
pub fn speaker_from_voice(
bundle: &ModelBundle,
path: &Path,
cleanup: ReferenceCleanup<'_>,
) -> Result<Vec<f32>, FttsError> {
let bytes = fs::read(path).map_err(|error| {
FttsError::Input(format!(
"cannot read voice source {}: {error}",
path.display()
))
})?;
let looks_like_audio = bytes.len() >= 12
&& (bytes.starts_with(b"RIFF")
|| bytes.starts_with(b"fLaC")
|| bytes.starts_with(b"ID3")
|| bytes.starts_with(b"OggS")
|| &bytes[4..8] == b"ftyp");
let looks_like_image = bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
|| bytes.starts_with(&[0xFF, 0xD8, 0xFF]);
if looks_like_image {
let (_, vector) = crate::card::decode_card(&bytes)?;
return Ok(vector);
}
if bytes.len() == SPEAKER_VECTOR_BYTES && !looks_like_audio {
return decode_speaker_vector(path, &bytes);
}
let pcm = decode_reference_audio_any(path)?;
speaker_from_reference_pcm(bundle, pcm, cleanup)
}
pub fn speaker_from_reference_pcm(
bundle: &ModelBundle,
pcm: Vec<f32>,
cleanup: ReferenceCleanup<'_>,
) -> Result<Vec<f32>, FttsError> {
let ReferenceCleanup { denoise, dereverb } = cleanup;
let pcm = match dereverb {
Some(report) => {
let before = reverb_time_s(&pcm);
let dried = dereverb_reference(&pcm);
let after = reverb_time_s(&dried);
if let (Some(before), Some(after)) = (before, after) {
*report = Some(DereverbReport {
before_rt60_s: before,
after_rt60_s: after,
});
}
dried
}
None => pcm,
};
let pcm = match denoise {
Some(report) => {
let before = pause_floor_dbfs(&pcm);
let cleaned = match neural_denoise_reference(bundle, &pcm)? {
Some(cleaned) => cleaned,
None => denoise_reference(&pcm),
};
*report = Some(DenoiseReport {
before_dbfs: before,
after_dbfs: pause_floor_dbfs(&cleaned),
});
cleaned
}
None => pcm,
};
let mel = log_mel_from_24khz_pcm(&pcm)
.map_err(|error| FttsError::Input(format!("cannot extract speaker features: {error}")))?;
let encoder = match bundle.canonical_main.as_deref() {
Some(artifact) => SpeakerEncoder::load_fttsq(artifact),
None => SpeakerEncoder::load(&bundle.main),
}
.map_err(checkpoint_error)?;
let vector = encoder.encode(&mel.values, mel.frames);
if vector.iter().all(|value| value.is_finite()) {
Ok(vector)
} else {
Err(FttsError::Input(
"speaker encoder produced a non-finite x-vector; refusing to condition synthesis"
.to_owned(),
))
}
}
pub fn write_speaker_vector_new(path: &Path, vector: &[f32]) -> Result<(), FttsError> {
if vector.len() != TALKER_HIDDEN {
return Err(FttsError::Input(format!(
"cannot write {}-wide speaker vector; expected {TALKER_HIDDEN}",
vector.len()
)));
}
if let Some(index) = vector.iter().position(|value| !value.is_finite()) {
return Err(FttsError::Input(format!(
"cannot write speaker vector with a non-finite value at index {index}"
)));
}
let mut bytes = Vec::with_capacity(SPEAKER_VECTOR_BYTES);
for value in vector {
bytes.extend_from_slice(&value.to_le_bytes());
}
use std::io::Write;
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| {
FttsError::Input(format!(
"cannot create enrolled voice {} without overwriting an existing file: {error}",
path.display()
))
})?;
file.write_all(&bytes).map_err(|error| {
FttsError::Input(format!(
"cannot write enrolled voice {}: {error}",
path.display()
))
})
}
pub fn replace_speaker_vector(path: &Path, vector: &[f32]) -> Result<PathBuf, FttsError> {
let backup = path.with_extension("spk.bak");
fs::copy(path, &backup).map_err(|error| {
FttsError::Input(format!(
"cannot back up the existing voice {} to {}: {error}",
path.display(),
backup.display()
))
})?;
let staging = path.with_extension("spk.incoming");
if staging.exists() {
fs::remove_file(&staging).map_err(|error| {
FttsError::Input(format!(
"cannot clear the stale staging file {}: {error}",
staging.display()
))
})?;
}
write_speaker_vector_new(&staging, vector)?;
fs::rename(&staging, path).map_err(|error| {
FttsError::Input(format!(
"cannot replace {} with the new voice: {error}",
path.display()
))
})?;
Ok(backup)
}
fn decode_speaker_vector(path: &Path, bytes: &[u8]) -> Result<Vec<f32>, FttsError> {
let vector: Vec<f32> = bytes
.as_chunks::<4>()
.0
.iter()
.map(|quad| f32::from_le_bytes(*quad))
.collect();
if let Some(index) = vector.iter().position(|value| !value.is_finite()) {
return Err(FttsError::Input(format!(
"speaker vector {} holds a non-finite value at index {index}; it would poison every \
prefill position it is summed into",
path.display()
)));
}
Ok(vector)
}
const SYSTEM_DECODED_EXTENSIONS: [&str; 6] = ["m4a", "mp3", "aac", "mp4", "ogg", "opus"];
pub(crate) fn private_staging_dir() -> std::io::Result<PathBuf> {
#[allow(deprecated)] let base = std::env::home_dir()
.map(|home| home.join(".cache/franken_tts"))
.unwrap_or_else(std::env::temp_dir);
let dir = base.join("staging");
fs::create_dir_all(&dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?;
}
Ok(dir)
}
fn decode_reference_audio_any(path: &Path) -> Result<Vec<f32>, FttsError> {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.map(str::to_ascii_lowercase);
let needs_system_decoder = extension
.as_deref()
.is_some_and(|extension| SYSTEM_DECODED_EXTENSIONS.contains(&extension));
if !needs_system_decoder {
return decode_reference_audio(path);
}
let staging_dir = private_staging_dir()
.map_err(|error| FttsError::Generic(format!("cannot create staging directory: {error}")))?;
let staging = staging_dir.join(format!(
"ftts-enroll-{}-{}.wav",
std::process::id(),
path.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("reference")
));
let attempts: &[(&str, Vec<&std::ffi::OsStr>)] = &[
(
"afconvert",
vec![
"-f".as_ref(),
"WAVE".as_ref(),
"-d".as_ref(),
"LEI16@24000".as_ref(),
"-c".as_ref(),
"1".as_ref(),
path.as_os_str(),
staging.as_os_str(),
],
),
(
"ffmpeg",
vec![
"-y".as_ref(),
"-loglevel".as_ref(),
"error".as_ref(),
"-i".as_ref(),
path.as_os_str(),
"-acodec".as_ref(),
"pcm_s16le".as_ref(),
"-ar".as_ref(),
"24000".as_ref(),
"-ac".as_ref(),
"1".as_ref(),
staging.as_os_str(),
],
),
];
let mut ran = false;
for (tool, arguments) in attempts {
match std::process::Command::new(tool).args(arguments).status() {
Ok(status) if status.success() => {
ran = true;
break;
}
Ok(status) => {
let _ = fs::remove_file(&staging);
return Err(FttsError::Input(format!(
"{tool} failed decoding reference audio {} (exit {status})",
path.display()
)));
}
Err(_) => continue, }
}
if !ran {
return Err(FttsError::Input(format!(
"reference audio {} is a compressed container and no system decoder was found; \
install afconvert (macOS) or ffmpeg, or supply WAV/FLAC",
path.display()
)));
}
let decoded = decode_reference_audio(&staging);
let _ = fs::remove_file(&staging);
decoded
}
fn decode_reference_audio(path: &Path) -> Result<Vec<f32>, FttsError> {
let file = fs::File::open(path).map_err(|error| {
FttsError::Input(format!(
"cannot open reference audio {}: {error}",
path.display()
))
})?;
let mut hint = Hint::new();
if let Some(extension) = path.extension().and_then(|extension| extension.to_str()) {
hint.with_extension(extension);
}
let stream = MediaSourceStream::new(Box::new(file), Default::default());
let probed = get_probe()
.format(
&hint,
stream,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(|error| {
FttsError::Input(format!(
"cannot identify reference audio {}: {error}",
path.display()
))
})?;
let mut format = probed.format;
let track = format.default_track().ok_or_else(|| {
FttsError::Input(format!(
"reference audio {} has no default audio track",
path.display()
))
})?;
let track_id = track.id;
let mut decoder = get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(|error| {
FttsError::Input(format!(
"cannot decode reference audio {}: {error}",
path.display()
))
})?;
let mut sample_rate = None;
let mut mono = Vec::new();
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(SymphoniaError::IoError(error))
if error.kind() == std::io::ErrorKind::UnexpectedEof =>
{
break;
}
Err(error) => {
return Err(FttsError::Input(format!(
"cannot read reference audio {}: {error}",
path.display()
)));
}
};
if packet.track_id() != track_id {
continue;
}
let decoded = decoder.decode(&packet).map_err(|error| {
FttsError::Input(format!(
"cannot decode reference audio {}: {error}",
path.display()
))
})?;
let spec = *decoded.spec();
match sample_rate {
Some(rate) if rate != spec.rate => {
return Err(FttsError::Input(format!(
"reference audio {} changed sample rate mid-stream ({rate} to {} Hz)",
path.display(),
spec.rate
)));
}
None => sample_rate = Some(spec.rate),
Some(_) => {}
}
let channels = spec.channels.count();
let mut samples = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks_exact(channels) {
mono.push(frame.iter().sum::<f32>() / channels as f32);
}
}
let rate = sample_rate.ok_or_else(|| {
FttsError::Input(format!(
"reference audio {} contains no decodable samples",
path.display()
))
})?;
if mono.is_empty() {
return Err(FttsError::Input(format!(
"reference audio {} contains no PCM samples",
path.display()
)));
}
let pcm = resample_to_speaker_rate(mono, rate);
if pcm.is_empty() {
return Err(FttsError::Input(format!(
"reference audio {} is too short to resample from {rate} Hz to \
{SPEAKER_SAMPLE_RATE_HZ} Hz; supply a longer recording",
path.display()
)));
}
Ok(pcm)
}
fn resample_to_speaker_rate(mono: Vec<f32>, from_rate: u32) -> Vec<f32> {
if from_rate == SPEAKER_SAMPLE_RATE_HZ {
return mono;
}
resample_lanczos(&mono, from_rate, SPEAKER_SAMPLE_RATE_HZ)
}
fn resample_lanczos(mono: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
if from_rate == to_rate {
return mono.to_vec();
}
const LOBES: f64 = 6.0;
let ratio = f64::from(to_rate) / f64::from(from_rate);
let cutoff = ratio.min(1.0);
let half = (LOBES / cutoff).ceil() as isize;
let out_len = ((mono.len() as f64) * ratio).round() as usize;
let mut out = Vec::with_capacity(out_len);
for index in 0..out_len {
let center = index as f64 / ratio;
let first = center.floor() as isize - half + 1;
let mut acc = 0.0_f64;
let mut norm = 0.0_f64;
for tap in first..first + 2 * half {
if tap < 0 {
continue;
}
let Some(sample) = mono.get(tap as usize) else {
break;
};
let weight = lanczos_tap(center - tap as f64, cutoff, LOBES);
acc += weight * f64::from(*sample);
norm += weight;
}
out.push(if norm.abs() > 1e-12 {
(acc / norm) as f32
} else {
0.0
});
}
out
}
const DENOISE_FRAME: usize = 512;
const DENOISE_HOP: usize = DENOISE_FRAME / 4;
const DD_ALPHA: f32 = 0.92;
const OMLSA_GAIN_FLOOR: f32 = 0.017_782_79;
const NOISE_BLOCK_FRAMES: usize = 256;
const SPEECH_ABSENCE_PRIOR: f32 = 0.6;
#[allow(clippy::excessive_precision)]
const NOISE_QUANTILE_BIAS: f32 = 9.491_221;
const NOISE_INIT_QUANTILE: f32 = 0.1;
pub const DENOISE_ARTIFACT_RELPATH: &str = "denoise/fastenhancer-s-48k.safetensors";
fn neural_denoise_reference(
bundle: &ModelBundle,
pcm24k: &[f32],
) -> Result<Option<Vec<f32>>, FttsError> {
let path = bundle.root.join(DENOISE_ARTIFACT_RELPATH);
if !path.is_file() {
return Ok(None);
}
if std::env::var("FTTS_DENOISE_ENGINE").is_ok_and(|v| v.eq_ignore_ascii_case("omlsa")) {
return Ok(None);
}
let enhancer = ftts_artifacts::enhance_loader::open_enhancer(&path).map_err(|error| {
FttsError::ArtifactFormat(format!(
"denoiser artifact {} is unreadable ({error}); re-fetch it with `ftts pull --force`",
path.display()
))
})?;
Ok(Some(enhancer.enhance_24k(pcm24k)))
}
pub fn denoise_pcm_24k(bundle: &ModelBundle, pcm: &[f32]) -> Result<Option<Vec<f32>>, FttsError> {
neural_denoise_reference(bundle, pcm)
}
fn denoise_reference(pcm: &[f32]) -> Vec<f32> {
const DENOISE_MIN_FRAMES: usize = 32;
if pcm.len() < DENOISE_FRAME + (DENOISE_MIN_FRAMES - 1) * DENOISE_HOP {
return pcm.to_vec();
}
let mut planner = rustfft::FftPlanner::<f32>::new();
let forward = planner.plan_fft_forward(DENOISE_FRAME);
let inverse = planner.plan_fft_inverse(DENOISE_FRAME);
let window: Vec<f32> = (0..DENOISE_FRAME)
.map(|n| {
let phase = std::f32::consts::TAU * n as f32 / DENOISE_FRAME as f32;
0.5 - 0.5 * phase.cos()
})
.collect();
let bins = DENOISE_FRAME / 2 + 1;
let starts: Vec<usize> = (0..=pcm.len() - DENOISE_FRAME)
.step_by(DENOISE_HOP)
.collect();
let mut powers: Vec<Vec<f32>> = Vec::with_capacity(starts.len());
let mut scratch: Vec<rustfft::num_complex::Complex<f32>> =
vec![rustfft::num_complex::Complex::new(0.0, 0.0); DENOISE_FRAME];
for &start in &starts {
for (slot, n) in scratch.iter_mut().zip(0..DENOISE_FRAME) {
*slot = rustfft::num_complex::Complex::new(pcm[start + n] * window[n], 0.0);
}
forward.process(&mut scratch);
powers.push((0..bins).map(|bin| scratch[bin].norm_sqr()).collect());
}
let blocks = powers.len().div_ceil(NOISE_BLOCK_FRAMES);
let mut noise = vec![f32::INFINITY; bins];
let mut column: Vec<f32> = Vec::with_capacity(NOISE_BLOCK_FRAMES);
for block in 0..blocks {
let span = block * NOISE_BLOCK_FRAMES..((block + 1) * NOISE_BLOCK_FRAMES).min(powers.len());
for (bin, slot) in noise.iter_mut().enumerate() {
column.clear();
column.extend(powers[span.clone()].iter().map(|frame| frame[bin]));
column.sort_by(f32::total_cmp);
let rank = ((column.len() as f32 - 1.0) * NOISE_INIT_QUANTILE).round() as usize;
*slot = slot.min(column[rank].max(1e-12) * NOISE_QUANTILE_BIAS);
}
}
let mut prev_gain = vec![1.0_f32; bins];
let mut prev_gamma = vec![1.0_f32; bins];
let mut out = vec![0.0_f32; pcm.len()];
let mut weight = vec![0.0_f32; pcm.len()];
for (index, &start) in starts.iter().enumerate() {
let mut frame: Vec<rustfft::num_complex::Complex<f32>> = (0..DENOISE_FRAME)
.map(|n| rustfft::num_complex::Complex::new(pcm[start + n] * window[n], 0.0))
.collect();
forward.process(&mut frame);
let power = &powers[index];
for bin in 0..bins {
let gamma = (power[bin] / noise[bin]).min(1e6);
let xi = (DD_ALPHA * prev_gain[bin].powi(2) * prev_gamma[bin]
+ (1.0 - DD_ALPHA) * (gamma - 1.0).max(0.0))
.max(1e-6);
let nu = (xi / (1.0 + xi)) * gamma;
let lsa =
((xi / (1.0 + xi)) * (0.5 * exponential_integral_e1(nu)).exp()).clamp(0.0, 1.0);
let odds = SPEECH_ABSENCE_PRIOR / (1.0 - SPEECH_ABSENCE_PRIOR);
let presence = 1.0 / (1.0 + odds * (1.0 + xi) * (-nu).exp());
let presence = presence.clamp(0.0, 1.0);
let gain = (lsa.max(OMLSA_GAIN_FLOOR).powf(presence)
* OMLSA_GAIN_FLOOR.powf(1.0 - presence))
.clamp(OMLSA_GAIN_FLOOR, 1.0);
prev_gain[bin] = gain;
prev_gamma[bin] = gamma;
frame[bin] *= gain;
let mirror = DENOISE_FRAME - bin;
if mirror != bin && mirror < DENOISE_FRAME {
frame[mirror] *= gain;
}
}
inverse.process(&mut frame);
let scale = 1.0 / DENOISE_FRAME as f32;
for n in 0..DENOISE_FRAME {
out[start + n] += frame[n].re * scale * window[n];
weight[start + n] += window[n] * window[n];
}
}
const WOLA_MIN_WEIGHT: f32 = 0.15;
for (sample, energy) in out.iter_mut().zip(weight.iter()) {
if *energy > WOLA_MIN_WEIGHT {
*sample /= *energy;
}
}
let covered =
starts.first().copied().unwrap_or(0)..starts.last().map_or(0, |last| last + DENOISE_FRAME);
for (index, sample) in out.iter_mut().enumerate() {
if !covered.contains(&index) || weight[index] <= WOLA_MIN_WEIGHT {
*sample = pcm[index];
}
}
out
}
fn exponential_integral_e1(x: f32) -> f32 {
if x <= 0.0 {
return 0.0;
}
let x = f64::from(x);
let value = if x < 1.0 {
const A: [f64; 6] = [
-0.577_215_664_9,
0.999_991_93,
-0.249_910_55,
0.055_199_68,
-0.009_760_04,
0.001_078_57,
];
let mut acc = 0.0;
for (power, coefficient) in A.iter().enumerate() {
acc += coefficient * x.powi(power as i32);
}
acc - x.ln()
} else {
const A: [f64; 4] = [8.573_328_74, 18.059_016_97, 8.634_760_89, 0.267_773_734];
const B: [f64; 4] = [9.573_322_34, 25.632_956_15, 21.099_653_08, 3.958_496_93];
let numerator = x.powi(4) + A[0] * x.powi(3) + A[1] * x * x + A[2] * x + A[3];
let denominator = x.powi(4) + B[0] * x.powi(3) + B[1] * x * x + B[2] * x + B[3];
(numerator / denominator) / (x * x.exp())
};
value as f32
}
const DEREVERB_FRAME: usize = 1024;
const DEREVERB_HOP: usize = 256;
const DEREVERB_TAPS: usize = 40;
const DEREVERB_DELAY: usize = 2;
const DEREVERB_ITERATIONS: usize = 3;
const DEREVERB_LOADING: f64 = 1e-4;
#[derive(Clone, Copy, Debug)]
pub struct DereverbReport {
pub before_rt60_s: f32,
pub after_rt60_s: f32,
}
fn dereverb_reference(pcm: &[f32]) -> Vec<f32> {
if pcm.len() < DEREVERB_FRAME * 4 {
return pcm.to_vec();
}
let mut planner = rustfft::FftPlanner::<f32>::new();
let forward = planner.plan_fft_forward(DEREVERB_FRAME);
let inverse = planner.plan_fft_inverse(DEREVERB_FRAME);
let window: Vec<f32> = (0..DEREVERB_FRAME)
.map(|n| {
let phase = std::f32::consts::TAU * n as f32 / DEREVERB_FRAME as f32;
0.5 - 0.5 * phase.cos()
})
.collect();
let bins = DEREVERB_FRAME / 2 + 1;
let starts: Vec<usize> = (0..=pcm.len() - DEREVERB_FRAME)
.step_by(DEREVERB_HOP)
.collect();
let frames = starts.len();
if frames <= DEREVERB_DELAY + DEREVERB_TAPS + 2 {
return pcm.to_vec();
}
let mut observed: Vec<Vec<Complex64>> = Vec::with_capacity(frames);
let mut scratch: Vec<rustfft::num_complex::Complex<f32>> =
vec![rustfft::num_complex::Complex::new(0.0, 0.0); DEREVERB_FRAME];
for &start in &starts {
for (slot, n) in scratch.iter_mut().zip(0..DEREVERB_FRAME) {
*slot = rustfft::num_complex::Complex::new(pcm[start + n] * window[n], 0.0);
}
forward.process(&mut scratch);
observed.push(
scratch[..bins]
.iter()
.map(|value| Complex64::new(f64::from(value.re), f64::from(value.im)))
.collect(),
);
}
let mut desired = observed.clone();
for _ in 0..DEREVERB_ITERATIONS {
for bin in 0..bins {
let mut power: Vec<f64> = (0..frames).map(|t| desired[t][bin].norm_sqr()).collect();
let mean = power.iter().sum::<f64>() / frames as f64;
let floor = (mean * 1e-6).max(1e-12);
for value in &mut power {
*value = value.max(floor);
}
let taps = DEREVERB_TAPS;
let mut covariance = vec![Complex64::new(0.0, 0.0); taps * taps];
let mut cross = vec![Complex64::new(0.0, 0.0); taps];
for t in (DEREVERB_DELAY + taps)..frames {
let weight = 1.0 / power[t];
let regressor: Vec<Complex64> = (0..taps)
.map(|lag| observed[t - DEREVERB_DELAY - lag][bin])
.collect();
for row in 0..taps {
let scaled = regressor[row] * weight;
for column in row..taps {
covariance[row * taps + column] += scaled * regressor[column].conj();
}
cross[row] += scaled * observed[t][bin].conj();
}
}
for row in 0..taps {
for column in 0..row {
covariance[row * taps + column] = covariance[column * taps + row].conj();
}
}
let trace: f64 = (0..taps).map(|i| covariance[i * taps + i].re).sum();
if trace <= 0.0 {
continue;
}
let loading = trace / taps as f64 * DEREVERB_LOADING;
for i in 0..taps {
covariance[i * taps + i] += Complex64::new(loading, 0.0);
}
let Some(filter) = solve_complex_system(&mut covariance, &mut cross, taps) else {
continue;
};
for t in 0..frames {
if t < DEREVERB_DELAY + taps {
desired[t][bin] = observed[t][bin];
continue;
}
let mut tail = Complex64::new(0.0, 0.0);
for (lag, coefficient) in filter.iter().enumerate() {
tail += coefficient.conj() * observed[t - DEREVERB_DELAY - lag][bin];
}
desired[t][bin] = observed[t][bin] - tail;
}
}
}
let mut out = vec![0.0_f32; pcm.len()];
let mut weight = vec![0.0_f32; pcm.len()];
for (index, &start) in starts.iter().enumerate() {
let mut frame = vec![rustfft::num_complex::Complex::new(0.0_f32, 0.0); DEREVERB_FRAME];
for bin in 0..bins {
let value = desired[index][bin];
#[allow(clippy::cast_possible_truncation)]
let value = rustfft::num_complex::Complex::new(value.re as f32, value.im as f32);
frame[bin] = value;
let mirror = DEREVERB_FRAME - bin;
if mirror != bin && mirror < DEREVERB_FRAME {
frame[mirror] = value.conj();
}
}
inverse.process(&mut frame);
let scale = 1.0 / DEREVERB_FRAME as f32;
for n in 0..DEREVERB_FRAME {
out[start + n] += frame[n].re * scale * window[n];
weight[start + n] += window[n] * window[n];
}
}
for (sample, energy) in out.iter_mut().zip(weight.iter()) {
if *energy > 1e-6 {
*sample /= *energy;
}
}
let covered =
starts.first().copied().unwrap_or(0)..starts.last().map_or(0, |last| last + DEREVERB_FRAME);
for (index, sample) in out.iter_mut().enumerate() {
if !covered.contains(&index) || weight[index] <= 1e-6 {
*sample = pcm[index];
}
}
out
}
type Complex64 = rustfft::num_complex::Complex<f64>;
fn solve_complex_system(
a: &mut [Complex64],
b: &mut [Complex64],
n: usize,
) -> Option<Vec<Complex64>> {
for column in 0..n {
let (pivot, magnitude) = (column..n).fold((column, 0.0_f64), |best, row| {
let candidate = a[row * n + column].norm_sqr();
if candidate > best.1 {
(row, candidate)
} else {
best
}
});
if magnitude <= f64::MIN_POSITIVE {
return None;
}
if pivot != column {
for k in 0..n {
a.swap(pivot * n + k, column * n + k);
}
b.swap(pivot, column);
}
let diagonal = a[column * n + column];
for row in (column + 1)..n {
let factor = a[row * n + column] / diagonal;
if factor == Complex64::new(0.0, 0.0) {
continue;
}
for k in column..n {
let value = a[column * n + k] * factor;
a[row * n + k] -= value;
}
let value = b[column] * factor;
b[row] -= value;
}
}
let mut solution = vec![Complex64::new(0.0, 0.0); n];
for row in (0..n).rev() {
let mut accumulator = b[row];
for k in (row + 1)..n {
accumulator -= a[row * n + k] * solution[k];
}
solution[row] = accumulator / a[row * n + row];
}
Some(solution)
}
fn reverb_time_s(pcm: &[f32]) -> Option<f32> {
let hop = (SPEAKER_SAMPLE_RATE_HZ as usize) / 100; if pcm.len() < hop * 32 {
return None;
}
let envelope: Vec<f32> = pcm
.chunks_exact(hop)
.map(|chunk| {
let energy = chunk.iter().map(|s| s * s).sum::<f32>() / chunk.len() as f32;
10.0 * (energy + 1e-9).log10()
})
.collect();
let peak = envelope.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let span = 15_usize; let mut slopes: Vec<f32> = Vec::new();
for index in 1..envelope.len().saturating_sub(span) {
if envelope[index] < peak - 25.0 || envelope[index] <= envelope[index - 1] {
continue;
}
let drop = envelope[index] - envelope[index + span - 1];
if drop < 6.0 {
continue;
}
slopes.push(drop / (span as f32 * 0.01));
}
if slopes.is_empty() {
return None;
}
slopes.sort_by(f32::total_cmp);
let median = slopes[slopes.len() / 2];
(median > 0.0).then(|| 60.0 / median)
}
fn pause_floor_dbfs(pcm: &[f32]) -> f32 {
let span = (SPEAKER_SAMPLE_RATE_HZ as usize) / 20;
if pcm.len() < span {
return f32::NEG_INFINITY;
}
let mut windows: Vec<f32> = pcm
.chunks_exact(span)
.map(|chunk| {
(chunk
.iter()
.map(|s| f64::from(*s) * f64::from(*s))
.sum::<f64>()
/ chunk.len() as f64)
.sqrt() as f32
})
.filter(|rms| *rms > 0.0)
.collect();
if windows.is_empty() {
return f32::NEG_INFINITY;
}
windows.sort_by(f32::total_cmp);
let keep = (windows.len() / 10).max(1);
let mean = windows[..keep].iter().sum::<f32>() / keep as f32;
20.0 * mean.log10()
}
fn lanczos_tap(offset: f64, cutoff: f64, lobes: f64) -> f64 {
let scaled = cutoff * offset;
if scaled.abs() >= lobes {
return 0.0;
}
sinc(scaled) * sinc(scaled / lobes)
}
fn sinc(x: f64) -> f64 {
if x.abs() < 1e-12 {
return 1.0;
}
let scaled = std::f64::consts::PI * x;
scaled.sin() / scaled
}
struct PreparedPassThrough {
prepared: PreparedText,
}
impl TextPreparer for PreparedPassThrough {
fn prepare(
&self,
_text: &str,
_options: &NormalizationOptions,
) -> Result<PreparedText, TextPreparationError> {
Ok(PreparedText::new(
self.prepared.token_ids.clone(),
NormalizationTrace {
mode: self.prepared.normalization_trace.mode,
unicode_version: self.prepared.normalization_trace.unicode_version.clone(),
changes: self.prepared.normalization_trace.changes.clone(),
},
))
}
}
pub struct SynthesizedAudio {
pub frames: u64,
pub prepared_token_count: usize,
pub pcm: Vec<f32>,
pub ttfa: Option<std::time::Duration>,
}
#[allow(clippy::too_many_arguments)]
pub fn synthesize(
model: &LoadedModel,
engine: &TtsEngine,
request: &SynthesisRequest,
speaker: &[f32],
seed: u64,
cancellation: &CancellationToken,
observer: &dyn SynthesisObserver,
) -> Result<SynthesizedAudio, FttsError> {
let prepared_raw = model
.tokenizer
.prepare(&request.text, &request.normalization_options)
.map_err(|error| FttsError::Input(format!("text preparation failed: {error}")))?;
let wrapped = TalkerCheckpoint::wrap_target_ids(&prepared_raw.token_ids);
let prepared = PreparedText::new(wrapped.clone(), prepared_raw.normalization_trace);
let ids = TalkerCheckpoint::utterance_text_ids(&wrapped);
let table = model
.talker
.gather_text_rows(&ids)
.map_err(checkpoint_error)?;
let header = model
.talker
.xvector_header(&table, speaker, CODEC_LANGUAGE_ENGLISH_ID)
.map_err(checkpoint_error)?;
let tts_eos = model.talker.tts_eos(&table);
let talker_layers = model.talker.talker_layer_weights();
let micro_layers = model.talker.microdecoder_layer_weights();
let residual = model.talker.residual_embedding_slices();
let heads = model.talker.microdecoder_head_slices();
let micro_residual = &residual[..residual.len() - 1];
let mut generator = QwenGenerator::new_with_artifact(
QwenGeneratorConfig {
talker_config: TalkerConfig::default(),
talker_weights: model.talker.talker_weights(&talker_layers),
text: model.talker.text_weights(&table),
feedback: model.talker.feedback_tables(&residual),
microdecoder_config: MicrodecoderConfig::default(),
microdecoder_weights: model.talker.microdecoder_weights(
µ_layers,
micro_residual,
&heads,
),
prompt_mode: PromptMode {
clone_mode: CloneMode::XVector,
non_streaming_mode: false,
},
header,
tts_eos,
reference: None,
sampling_mode: SamplingMode::Production,
seed,
},
model.artifact.as_deref(),
);
let preparer = PreparedPassThrough { prepared };
let (frame_tx, frame_rx) = std::sync::mpsc::sync_channel::<ftts_core::CodeFrame>(256);
let codec = &model.codec;
let synthesis_started = std::time::Instant::now();
let (result, pcm, ttfa) = std::thread::scope(
|scope| -> Result<
(
ftts_core::SynthesisResult,
Vec<f32>,
Option<std::time::Duration>,
),
FttsError,
> {
let worker = scope.spawn(
move || -> Result<(Vec<f32>, Option<std::time::Duration>), FttsError> {
ftts_kernels::team::bypass_team_on_this_thread();
const PACKET_FRAMES: usize = 4;
let mut state = codec.stream_state();
let mut pcm = Vec::new();
let mut packet_pcm = Vec::new();
let mut packet: Vec<i32> = Vec::with_capacity(16 * PACKET_FRAMES);
let mut packet_frames = 0_usize;
let mut first_audio_at: Option<std::time::Duration> = None;
while let Ok(frame) = frame_rx.recv() {
if frame.codes.len() != 16 {
return Err(FttsError::Generic(format!(
"generated frame carries {} codes, expected 16",
frame.codes.len()
)));
}
for code in &frame.codes {
packet.push(i32::try_from(*code).map_err(|_| {
FttsError::Generic(format!(
"generated code {code} does not fit the codec's i32"
))
})?);
}
packet_frames += 1;
if packet_frames == PACKET_FRAMES {
codec
.stream_push(&mut state, &packet, packet_frames, &mut packet_pcm)
.map_err(checkpoint_error)?;
pcm.extend_from_slice(&packet_pcm);
first_audio_at.get_or_insert_with(|| synthesis_started.elapsed());
packet.clear();
packet_frames = 0;
}
}
if packet_frames > 0 {
codec
.stream_push(&mut state, &packet, packet_frames, &mut packet_pcm)
.map_err(checkpoint_error)?;
pcm.extend_from_slice(&packet_pcm);
first_audio_at.get_or_insert_with(|| synthesis_started.elapsed());
}
Ok((pcm, first_audio_at))
},
);
let mut tee = TeeGenerator {
inner: &mut generator,
frames: frame_tx,
};
let result = engine
.synthesize(
request.clone(),
&preparer,
&mut tee as &mut dyn FrameGenerator,
cancellation,
observer,
)
.map_err(engine_error);
drop(tee); let pcm = worker.join().expect("codec worker must not panic");
let result = result?;
let (pcm, ttfa) = pcm?;
Ok((result, pcm, ttfa))
},
)?;
if result.code_frames.is_empty() {
return Err(FttsError::Generic(
"the talker stopped before emitting a frame; there is no audio to write. This is a \
model or prompt problem, not an output problem — check the speaker vector and the \
text"
.to_owned(),
));
}
Ok(SynthesizedAudio {
frames: result.generated_frames,
prepared_token_count: result.prepared_token_count,
pcm,
ttfa,
})
}
struct TeeGenerator<'a> {
inner: &'a mut dyn FrameGenerator,
frames: std::sync::mpsc::SyncSender<ftts_core::CodeFrame>,
}
impl FrameGenerator for TeeGenerator<'_> {
fn begin_utterance(&mut self, prepared: &PreparedText) -> Result<(), GenerationError> {
self.inner.begin_utterance(prepared)
}
fn next_frame(&mut self) -> Result<Option<ftts_core::CodeFrame>, GenerationError> {
let frame = self.inner.next_frame()?;
if let Some(frame) = &frame
&& self.frames.send(frame.clone()).is_err()
{
return Err(GenerationError::new(
"the codec worker stopped accepting frames; its error follows at join",
));
}
Ok(frame)
}
}
fn engine_error(error: EngineError) -> FttsError {
match error {
EngineError::BudgetExceeded(_) => FttsError::BudgetTimeout(error.to_string()),
EngineError::ResourceAdmission(_) => FttsError::BudgetTimeout(error.to_string()),
EngineError::TextPreparation(_) => FttsError::Input(error.to_string()),
other => FttsError::Generic(other.to_string()),
}
}
#[must_use]
pub fn generation_error(message: &str) -> GenerationError {
GenerationError::new(message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audio_at_the_pinned_rate_is_returned_bit_for_bit() {
let pcm: Vec<f32> = (0..4_096)
.map(|n| (n as f32 * 0.017).sin() * 0.4 + (n as f32 * 0.31).sin() * 0.05)
.collect();
let out = resample_to_speaker_rate(pcm.clone(), SPEAKER_SAMPLE_RATE_HZ);
assert_eq!(out.len(), pcm.len());
for (index, (a, b)) in out.iter().zip(pcm.iter()).enumerate() {
assert!(
a.to_bits() == b.to_bits(),
"sample {index} was altered at the pinned rate"
);
}
}
#[test]
fn the_exponential_integral_matches_its_series_expansion() {
for (x, expected) in [
(0.1_f32, 1.822_923_9_f32),
(0.5, 0.559_773_6),
(0.9, 0.260_183_94),
] {
let actual = exponential_integral_e1(x);
let relative = ((actual - expected) / expected).abs();
assert!(
relative < 1e-6,
"E1({x}) = {actual} but the series gives {expected} (relative {relative:e})"
);
}
let below = exponential_integral_e1(0.999_9);
let above = exponential_integral_e1(1.000_1);
assert!(
below > above && (below - above).abs() < 1e-4,
"the series and rational branches disagree across x = 1: {below} vs {above}"
);
assert_eq!(
exponential_integral_e1(0.0),
0.0,
"a non-positive argument must not produce NaN"
);
}
#[test]
fn denoise_lowers_the_floor_between_bursts_without_eating_the_signal() {
const TONE_HZ: f64 = 700.0;
let samples = SPEAKER_SAMPLE_RATE_HZ as usize * 2;
let mut state = 0x2545_F491_4F6C_DD1D_u64;
let mut noise = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
((state >> 40) as f32 / 16_777_216.0) - 0.5
};
let clean: Vec<f32> = (0..samples)
.map(|n| {
let t = n as f64 / f64::from(SPEAKER_SAMPLE_RATE_HZ);
let speaking = (n / (SPEAKER_SAMPLE_RATE_HZ as usize / 2)).is_multiple_of(2);
if speaking {
(std::f64::consts::TAU * TONE_HZ * t).sin() as f32 * 0.35
} else {
0.0
}
})
.collect();
let noisy: Vec<f32> = clean.iter().map(|s| s + noise() * 0.1).collect();
let cleaned = denoise_reference(&noisy);
assert_eq!(cleaned.len(), noisy.len(), "denoise must preserve length");
assert!(
cleaned.iter().all(|s| s.is_finite()),
"denoise produced a non-finite sample"
);
let before = pause_floor_dbfs(&noisy);
let after = pause_floor_dbfs(&cleaned);
assert!(
after < before - 3.0,
"expected the pause floor to drop by >3 dB, got {before:.1} -> {after:.1} dBFS"
);
let span = SPEAKER_SAMPLE_RATE_HZ as usize / 4;
let peak_rms = |pcm: &[f32]| {
pcm.chunks_exact(span)
.map(|c| (c.iter().map(|s| s * s).sum::<f32>() / c.len() as f32).sqrt())
.fold(0.0_f32, f32::max)
};
let kept = peak_rms(&cleaned) / peak_rms(&noisy);
assert!(
kept > 0.7,
"denoise removed too much of the signal: peak RMS kept {kept:.3} of the original"
);
}
#[test]
fn denoise_keeps_a_reference_that_opens_on_speech() {
let rate = SPEAKER_SAMPLE_RATE_HZ as usize;
let mut state = 0x9E37_79B9_7F4A_7C15_u64;
let mut hiss = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
((state >> 40) as f32 / 16_777_216.0) - 0.5
};
let burst = rate / 4;
let noisy: Vec<f32> = (0..rate * 3)
.map(|n| {
let t = n as f64 / rate as f64;
let index = n / burst;
let voice = if index.is_multiple_of(2) {
let vibrato = 1.0 + 0.03 * (std::f64::consts::TAU * 5.5 * t).sin();
let phase = (n % burst) as f32 / burst as f32;
let envelope = (std::f32::consts::PI * phase).sin();
let f0 = 140.0 * vibrato * (1.0 + 0.15 * (index / 2) as f64);
(1..=10)
.map(|h| {
let a = 0.3 / h as f32;
(std::f64::consts::TAU * f0 * h as f64 * t).sin() as f32 * a
})
.sum::<f32>()
* envelope
} else {
0.0
};
voice + hiss() * 0.02
})
.collect();
let cleaned = denoise_reference(&noisy);
let rms = |pcm: &[f32]| (pcm.iter().map(|s| s * s).sum::<f32>() / pcm.len() as f32).sqrt();
let kept = rms(&cleaned[..burst]) / rms(&noisy[..burst]);
assert!(
kept > 0.7,
"the opening burst kept only {kept:.3} of its energy; the noise floor is being \
seeded from speech the estimator has not yet learned to exclude"
);
}
#[test]
fn denoise_does_not_preferentially_zap_high_frequencies() {
let rate = SPEAKER_SAMPLE_RATE_HZ as usize;
let mut state = 0xDEAD_BEEF_1234_5678_u64;
let mut hiss = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
((state >> 40) as f32 / 16_777_216.0) - 0.5
};
let probes: [f64; 5] = [300.0, 1_200.0, 3_000.0, 6_000.0, 9_000.0];
let burst = rate / 4;
let noisy: Vec<f32> = (0..rate * 3)
.map(|n| {
let t = n as f64 / rate as f64;
let voice = if (n / burst).is_multiple_of(2) {
let phase = (n % burst) as f32 / burst as f32;
let envelope = (std::f32::consts::PI * phase).sin();
probes
.iter()
.map(|hz| (std::f64::consts::TAU * hz * t).sin() as f32 * 0.12)
.sum::<f32>()
* envelope
} else {
0.0
};
voice + hiss() * 0.02
})
.collect();
let cleaned = denoise_reference(&noisy);
let span = burst / 2..burst;
let energy_at = |pcm: &[f32], hz: f64| -> f32 {
let (mut re, mut im) = (0.0_f64, 0.0_f64);
for (offset, sample) in pcm[span.clone()].iter().enumerate() {
let t = (span.start + offset) as f64 / rate as f64;
let angle = std::f64::consts::TAU * hz * t;
re += f64::from(*sample) * angle.cos();
im += f64::from(*sample) * angle.sin();
}
(re.hypot(im) / span.len() as f64) as f32
};
let retention: Vec<f32> = probes
.iter()
.map(|hz| energy_at(&cleaned, *hz) / energy_at(&noisy, *hz).max(1e-9))
.collect();
let low = retention[0];
for (hz, kept) in probes.iter().zip(retention.iter()) {
assert!(
*kept > 0.5,
"{hz} Hz retained only {kept:.3}; the denoiser is eating the band, not the noise \
(all bands: {retention:?})"
);
assert!(
*kept > low * 0.6,
"{hz} Hz retained {kept:.3} against {low:.3} at 300 Hz — high frequencies are \
being attenuated preferentially, which is how sibilance and speaker identity go \
(all bands: {retention:?})"
);
}
}
#[test]
fn a_clip_shorter_than_its_downsample_ratio_resamples_to_nothing() {
let out = resample_to_speaker_rate(vec![0.25], 192_000);
assert!(
out.is_empty(),
"one sample at 192 kHz is less than half an output sample at \
{SPEAKER_SAMPLE_RATE_HZ} Hz, so it cannot produce one"
);
}
#[test]
fn a_48k_tone_resamples_to_24k_with_its_shape_intact() {
const SOURCE_HZ: u32 = 48_000;
const TONE_HZ: f64 = 1_000.0;
let samples = SOURCE_HZ as usize; let pcm: Vec<f32> = (0..samples)
.map(|n| {
let t = n as f64 / f64::from(SOURCE_HZ);
(std::f64::consts::TAU * TONE_HZ * t).sin() as f32
})
.collect();
let out = resample_to_speaker_rate(pcm, SOURCE_HZ);
let expected_len = SPEAKER_SAMPLE_RATE_HZ as usize;
assert!(
out.len().abs_diff(expected_len) <= 1,
"expected ~{expected_len} samples at {SPEAKER_SAMPLE_RATE_HZ} Hz, got {}",
out.len()
);
let skip = 64;
let interior = out.len() - skip;
let mut worst = 0.0_f32;
for (index, sample) in out.iter().enumerate().take(interior).skip(skip) {
let t = index as f64 / f64::from(SPEAKER_SAMPLE_RATE_HZ);
let ideal = (std::f64::consts::TAU * TONE_HZ * t).sin() as f32;
worst = worst.max((sample - ideal).abs());
}
assert!(
worst < 0.02,
"resampled tone drifted from the analytic reference by {worst}"
);
}
#[test]
fn a_short_speaker_vector_is_refused_rather_than_padded() {
let dir = std::env::temp_dir().join("ftts-synth-tests");
fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("short.spk");
fs::write(&path, vec![0u8; 64]).expect("write");
let error = read_speaker_vector(&path).expect_err("a short vector must be refused");
let message = error.to_string();
assert!(message.contains("64 bytes"), "{message}");
assert!(message.contains("4096"), "{message}");
}
#[test]
fn a_non_finite_speaker_vector_is_refused() {
let dir = std::env::temp_dir().join("ftts-synth-tests");
fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("nan.spk");
let mut bytes = vec![0u8; SPEAKER_VECTOR_BYTES];
bytes[0..4].copy_from_slice(&f32::NAN.to_le_bytes());
fs::write(&path, &bytes).expect("write");
let error = read_speaker_vector(&path).expect_err("NaN must be refused");
assert!(error.to_string().contains("index 0"), "{error}");
}
#[test]
fn a_well_formed_speaker_vector_reads_back_exactly() {
let dir = std::env::temp_dir().join("ftts-synth-tests");
fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("good.spk");
let expected: Vec<f32> = (0..TALKER_HIDDEN).map(|i| i as f32 * 0.001).collect();
let mut bytes = Vec::with_capacity(SPEAKER_VECTOR_BYTES);
for value in &expected {
bytes.extend_from_slice(&value.to_le_bytes());
}
fs::write(&path, &bytes).expect("write");
assert_eq!(read_speaker_vector(&path).expect("read"), expected);
}
#[test]
fn enrollment_writer_refuses_overwrite_and_preserves_the_vector() {
let path = std::env::temp_dir().join(format!(
"ftts-enroll-{}-{}.spk",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
let expected: Vec<f32> = (0..TALKER_HIDDEN)
.map(|index| index as f32 * 0.125)
.collect();
write_speaker_vector_new(&path, &expected).expect("initial enrollment write");
assert_eq!(
read_speaker_vector(&path).expect("read enrolled vector"),
expected
);
let error = write_speaker_vector_new(&path, &[0.0; TALKER_HIDDEN])
.expect_err("an enrollment must never replace an existing voice");
assert!(error.to_string().contains("without overwriting"), "{error}");
}
#[test]
fn wav_reference_decodes_to_mono_24khz_pcm() {
let path = std::env::temp_dir().join(format!(
"ftts-reference-{}-{}.wav",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
let pcm: Vec<f32> = (0..1_920)
.map(|index| (index as f32 / 1_920.0 * std::f32::consts::TAU).sin() * 0.25)
.collect();
fs::write(
&path,
ftts_core::audio::encode_wav(&pcm, SPEAKER_SAMPLE_RATE_HZ),
)
.expect("write reference WAV");
let decoded = decode_reference_audio(&path).expect("decode reference WAV");
assert_eq!(decoded.len(), pcm.len());
assert!(decoded.iter().all(|sample| sample.is_finite()));
}
#[test]
fn a_bundle_names_the_file_that_is_actually_missing() {
let dir = std::env::temp_dir().join("ftts-bundle-tests-empty");
fs::create_dir_all(&dir).expect("temp dir");
let error = ModelBundle::resolve(&dir).expect_err("an empty directory is not a bundle");
assert!(error.to_string().contains("model.safetensors"), "{error}");
}
#[test]
fn a_complete_bundle_prefers_its_canonical_artifact_for_synthesis() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"ftts-bundle-canonical-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(dir.join("speech_tokenizer")).expect("create bundle sidecar directory");
for name in [
CANONICAL_MODEL_BASENAME,
"speech_tokenizer/model.safetensors",
"vocab.json",
"merges.txt",
"tokenizer_config.json",
] {
fs::write(dir.join(name), []).expect("write bundle fixture sidecar");
}
let expected_artifact = dir.join(CANONICAL_MODEL_BASENAME);
let bundle = ModelBundle::resolve(&dir).expect("complete canonical bundle resolves");
assert_eq!(
bundle.canonical_main.as_deref(),
Some(expected_artifact.as_path())
);
assert!(
!bundle.main.exists(),
"canonical synthesis must not require the raw main checkpoint"
);
let explicit = ModelBundle::resolve(&expected_artifact)
.expect("an explicit canonical artifact resolves against its sidecars");
assert_eq!(
explicit.canonical_main.as_deref(),
Some(expected_artifact.as_path())
);
}
}