#![allow(
non_camel_case_types,
non_snake_case,
dead_code,
clippy::too_many_lines
)]
use std::ffi::{c_char, c_int, c_void};
use std::fmt;
pub(crate) const OV_ABI_VERSION: c_int = 4;
#[repr(C)]
pub struct ov_context {
_private: [u8; 0],
}
pub type ov_status = c_int;
pub(crate) const OV_STATUS_OK: c_int = 0;
pub(crate) const OV_STATUS_ERROR: c_int = -1;
pub(crate) const OV_STATUS_INVALID_PARAMS: c_int = -1;
pub(crate) const OV_STATUS_INSTRUCT_INVALID: c_int = -2;
pub(crate) const OV_STATUS_GENERATE_FAILED: c_int = -3;
pub(crate) const OV_STATUS_OUT_OF_MEMORY: c_int = -4;
pub(crate) const OV_STATUS_CANCELLED: c_int = -5;
pub type ov_cancel_cb = Option<unsafe extern "C" fn(user_data: *mut c_void) -> bool>;
pub type ov_audio_chunk_cb =
Option<unsafe extern "C" fn(samples: *const f32, n: c_int, user_data: *mut c_void) -> bool>;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ov_audio {
pub samples: *const f32,
pub n_samples: c_int,
pub sample_rate: c_int,
pub channels: c_int,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ov_init_params {
pub abi_version: c_int,
pub model_path: *const c_char,
pub codec_path: *const c_char,
pub use_fa: bool,
pub clamp_fp16: bool,
pub device: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct ov_tts_params {
pub abi_version: c_int,
pub text: *const c_char,
pub lang: *const c_char,
pub instruct: *const c_char,
pub T_override: c_int,
pub chunk_duration_sec: f32,
pub chunk_threshold_sec: f32,
pub denoise: bool,
pub preprocess_prompt: bool,
pub mg_num_step: c_int,
pub mg_guidance_scale: f32,
pub mg_t_shift: f32,
pub mg_layer_penalty_factor: f32,
pub mg_position_temperature: f32,
pub mg_class_temperature: f32,
pub mg_seed: u64,
pub ref_audio_tokens: *const i32,
pub ref_T: c_int,
pub ref_audio_24k: *const f32,
pub ref_n_samples: c_int,
pub ref_text: *const c_char,
pub dump_dir: *const c_char,
pub cancel: ov_cancel_cb,
pub cancel_user_data: *mut c_void,
pub on_chunk: ov_audio_chunk_cb,
pub on_chunk_user_data: *mut c_void,
pub postproc: bool,
}
macro_rules! omnivoice_ffi {
() => {
extern "C" {
pub fn ov_init_default_params(p: *mut ov_init_params);
pub fn ov_init(params: *const ov_init_params) -> *mut ov_context;
pub fn ov_free(ctx: *mut ov_context);
pub fn ov_last_error() -> *const c_char;
pub fn ov_tts_default_params(p: *mut ov_tts_params);
pub fn ov_synthesize(
ctx: *mut ov_context,
params: *const ov_tts_params,
out: *mut ov_audio,
) -> ov_status;
pub fn ov_audio_free(audio: *mut ov_audio);
pub fn ov_n_languages() -> c_int;
pub fn ov_language_id(index: c_int) -> *const c_char;
pub fn ov_language_name(id: *const c_char) -> *const c_char;
pub fn ov_duration_sec_to_tokens(ctx: *const ov_context, seconds: f32) -> c_int;
}
};
}
omnivoice_ffi!();
#[cfg(target_arch = "x86_64")]
mod layout_asserts {
use super::{ov_audio, ov_init_params, ov_tts_params};
const _: () = assert!(std::mem::size_of::<ov_audio>() == 24);
const _: () = assert!(std::mem::size_of::<ov_init_params>() == 40);
const _: () = assert!(std::mem::size_of::<ov_tts_params>() == 168);
}
#[derive(Debug)]
pub struct OmniVoiceError(pub String);
impl fmt::Display for OmniVoiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn estimate_duration_secs(text: &str) -> f32 {
let words: Vec<&str> = text.split_whitespace().collect();
if words.is_empty() {
return 1.0;
}
let total_syllables: f64 = words.iter().map(|w| crate::engine::word_weight(w)).sum();
let secs = total_syllables * 0.26;
secs.clamp(0.5, 60.0) as f32
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
pub fn load_wav_24k_mono(path: &str) -> Option<Vec<f32>> {
let reader = hound::WavReader::open(path).ok()?;
let spec = reader.spec();
let samples: Vec<f32> = match spec.sample_format {
hound::SampleFormat::Float => reader
.into_samples::<f32>()
.filter_map(Result::ok)
.collect(),
hound::SampleFormat::Int => {
let max = (1i64 << (spec.bits_per_sample - 1)) as f32;
reader
.into_samples::<i32>()
.filter_map(Result::ok)
.map(|s| s as f32 / max)
.collect()
}
};
let ch = spec.channels.max(1) as usize;
let mono: Vec<f32> = samples
.chunks(ch)
.map(|c| c.iter().sum::<f32>() / ch as f32)
.collect();
if spec.sample_rate == 24_000 {
return Some(mono);
}
let ratio = 24_000.0 / f64::from(spec.sample_rate);
let n_out = (f64::from(mono.len() as u32) * ratio) as usize;
Some(
(0..n_out)
.map(|i| {
let src = f64::from(i as u32) / ratio;
let i0 = src.floor() as usize;
let i1 = (i0 + 1).min(mono.len().saturating_sub(1));
let frac = (src - src.floor()) as f32;
mono[i0] * (1.0 - frac) + mono[i1] * frac
})
.collect(),
)
}
#[must_use]
pub fn omnivoice_language_codes() -> Vec<crate::types::LanguageCode> {
[
("en", "English"),
("en-GB", "English (British)"),
("en-US", "English (American)"),
("fr", "French"),
("de", "German"),
("es", "Spanish"),
("it", "Italian"),
("pt", "Portuguese"),
("ru", "Russian"),
("zh", "Chinese"),
("ja", "Japanese"),
("ko", "Korean"),
("hi", "Hindi"),
("ar", "Arabic"),
("tr", "Turkish"),
("pl", "Polish"),
("nl", "Dutch"),
("sv", "Swedish"),
("da", "Danish"),
("no", "Norwegian"),
("fi", "Finnish"),
("cs", "Czech"),
("el", "Greek"),
("he", "Hebrew"),
("th", "Thai"),
("vi", "Vietnamese"),
("id", "Indonesian"),
("ms", "Malay"),
("uk", "Ukrainian"),
("ro", "Romanian"),
("hu", "Hungarian"),
("bg", "Bulgarian"),
("hr", "Croatian"),
("sk", "Slovak"),
("sl", "Slovenian"),
("sr", "Serbian"),
("lt", "Lithuanian"),
("lv", "Latvian"),
("et", "Estonian"),
("is", "Icelandic"),
("ga", "Irish"),
("cy", "Welsh"),
("sw", "Swahili"),
("af", "Afrikaans"),
("bn", "Bengali"),
("ta", "Tamil"),
("te", "Telugu"),
("kn", "Kannada"),
("ml", "Malayalam"),
("mr", "Marathi"),
("gu", "Gujarati"),
("pa", "Punjabi"),
("ur", "Urdu"),
("si", "Sinhala"),
("my", "Burmese"),
("km", "Khmer"),
("lo", "Lao"),
("ne", "Nepali"),
("fa", "Persian"),
("ps", "Pashto"),
("az", "Azerbaijani"),
("kk", "Kazakh"),
("ky", "Kyrgyz"),
("uz", "Uzbek"),
]
.iter()
.map(|(code, name)| crate::types::LanguageCode {
bcp47: (*code).to_string(),
iso639_3: String::new(),
display: (*name).to_string(),
})
.collect()
}
#[must_use]
pub fn bcp47_to_omnivoice_lang(bcp47: &str) -> String {
let segments: Vec<&str> = bcp47.split('-').collect();
let base = segments.first().copied().unwrap_or("").to_ascii_lowercase();
let region = segments.get(1).copied().unwrap_or("").to_ascii_lowercase();
match base.as_str() {
"en" => match region.as_str() {
"gb" | "uk" => "English (British)",
"us" => "English (American)",
"au" => "English (Australian)",
_ => "English",
}
.to_string(),
_ => base,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_ov_init_params_40_bytes() {
assert_eq!(std::mem::size_of::<ov_init_params>(), 40);
}
#[test]
fn layout_ov_tts_params_168_bytes() {
assert_eq!(std::mem::size_of::<ov_tts_params>(), 168);
}
#[test]
fn layout_ov_audio_24_bytes() {
assert_eq!(std::mem::size_of::<ov_audio>(), 24);
}
#[test]
fn bcp47_mapping_exact_regions() {
assert_eq!(bcp47_to_omnivoice_lang("en-GB"), "English (British)");
assert_eq!(bcp47_to_omnivoice_lang("en-US"), "English (American)");
assert_eq!(bcp47_to_omnivoice_lang("en"), "English");
assert_eq!(bcp47_to_omnivoice_lang("fr-FR"), "fr"); assert_eq!(bcp47_to_omnivoice_lang("en-custom"), "English");
}
#[test]
fn wav_loader_missing_file() {
assert!(load_wav_24k_mono("/nonexistent/file.wav").is_none());
}
#[test]
fn duration_estimation_reasonable() {
let d = estimate_duration_secs("Hello world this is a test");
assert!(d > 0.5 && d < 10.0, "duration {d}");
}
}