use crate::models::ModelRegistry;
use crate::pipeline_v2::Pipeline;
use crate::types::{Profile, SampleRate};
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_float, c_int};
use std::panic::{AssertUnwindSafe, catch_unwind};
#[repr(C)]
pub enum PolyvoiceProfile {
Mobile = 0,
Balanced = 1,
}
#[repr(C)]
pub enum PolyvoiceStatus {
Ok = 0,
InvalidArg = 1,
AudioTooShort = 2,
AudioTooLong = 3,
ModelLoad = 10,
Inference = 11,
OutOfMemory = 20,
Registry = 30,
Internal = 99,
}
#[repr(C)]
pub enum PolyvoiceFormat {
Json = 0,
Rttm = 1,
Srt = 2,
Vtt = 3,
Txt = 4,
}
const FFI_RTTM_FILE_ID: &str = "audio";
fn render_result(result: &crate::types::DiarizationResult, format: c_int) -> Result<String, c_int> {
let mut buf: Vec<u8> = Vec::new();
match format {
f if f == PolyvoiceFormat::Json as c_int => {
return serde_json::to_string(result).map_err(|_| PolyvoiceStatus::Internal as c_int);
}
f if f == PolyvoiceFormat::Rttm as c_int => {
crate::rttm::write_rttm(&mut buf, FFI_RTTM_FILE_ID, &result.turns)
.map_err(|_| PolyvoiceStatus::Internal as c_int)?;
}
f if f == PolyvoiceFormat::Srt as c_int => {
crate::format::write_srt(&mut buf, &result.turns)
.map_err(|_| PolyvoiceStatus::Internal as c_int)?;
}
f if f == PolyvoiceFormat::Vtt as c_int => {
crate::format::write_vtt(&mut buf, &result.turns)
.map_err(|_| PolyvoiceStatus::Internal as c_int)?;
}
f if f == PolyvoiceFormat::Txt as c_int => {
crate::format::write_txt(&mut buf, &result.turns)
.map_err(|_| PolyvoiceStatus::Internal as c_int)?;
}
_ => return Err(PolyvoiceStatus::InvalidArg as c_int),
}
String::from_utf8(buf).map_err(|_| PolyvoiceStatus::Internal as c_int)
}
pub struct PolyvoicePipeline {
inner: Pipeline,
}
#[unsafe(no_mangle)] #[rustfmt::skip]
pub unsafe extern "C" fn polyvoice_pipeline_create(
profile: c_int,
models_cache_dir: *const c_char,
out_handle: *mut *mut PolyvoicePipeline,
) -> c_int {
let r = catch_unwind(AssertUnwindSafe(
|| -> Result<*mut PolyvoicePipeline, c_int> {
if out_handle.is_null() {
return Err(PolyvoiceStatus::InvalidArg as c_int);
}
let prof = match profile {
0 => Profile::Mobile,
1 => Profile::Balanced,
_ => return Err(PolyvoiceStatus::InvalidArg as c_int),
};
let registry = if models_cache_dir.is_null() {
ModelRegistry::default()
} else {
let s = unsafe { CStr::from_ptr(models_cache_dir)
}
.to_str()
.map_err(|_| PolyvoiceStatus::InvalidArg as c_int)?;
let cache_path = std::path::Path::new(s);
if cache_path.is_absolute() {
return Err(PolyvoiceStatus::InvalidArg as c_int);
}
if cache_path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(PolyvoiceStatus::InvalidArg as c_int);
}
ModelRegistry::with_cache_dir(s)
}
.map_err(|_| PolyvoiceStatus::Registry as c_int)?;
let pipeline = Pipeline::builder()
.profile(prof)
.with_models_from(registry)
.build()
.map_err(|e| match e {
crate::pipeline_v2::ConfigError::Registry(_) |
crate::pipeline_v2::ConfigError::UnknownModel { .. } => {
PolyvoiceStatus::Registry as c_int
}
crate::pipeline_v2::ConfigError::MissingRegistry { .. } |
crate::pipeline_v2::ConfigError::CustomComponentInProfile { .. } |
crate::pipeline_v2::ConfigError::RegistryInCustomProfile |
crate::pipeline_v2::ConfigError::MissingCustomComponent { .. } => {
PolyvoiceStatus::InvalidArg as c_int
}
})?;
Ok(Box::into_raw(Box::new(PolyvoicePipeline { inner: pipeline })))
},
));
match r {
Ok(Ok(handle)) => {
unsafe { *out_handle = handle;
}
PolyvoiceStatus::Ok as c_int
}
Ok(Err(code)) => code,
Err(_) => PolyvoiceStatus::Internal as c_int,
}
}
#[unsafe(no_mangle)] #[rustfmt::skip]
pub unsafe extern "C" fn polyvoice_pipeline_run(
pipeline: *mut PolyvoicePipeline,
samples: *const c_float,
n_samples: usize,
sample_rate: u32,
out_json: *mut *mut c_char,
out_json_len: *mut usize,
) -> c_int {
let r = catch_unwind(AssertUnwindSafe(|| -> Result<(), c_int> {
if pipeline.is_null() || samples.is_null() || out_json.is_null() || out_json_len.is_null() {
return Err(PolyvoiceStatus::InvalidArg as c_int);
}
let pipeline = unsafe { &*pipeline
};
const MAX_SAMPLES: usize = 16000 * 3600; if n_samples > MAX_SAMPLES {
return Err(PolyvoiceStatus::AudioTooLong as c_int);
}
let samples = unsafe { std::slice::from_raw_parts(samples, n_samples)
};
let sr = SampleRate::new(sample_rate)
.ok_or(PolyvoiceStatus::InvalidArg as c_int)?;
let result = pipeline
.inner
.run(samples, sr)
.map_err(|e| match e {
crate::pipeline_v2::PipelineError::UnsupportedSampleRate { .. } => {
PolyvoiceStatus::InvalidArg as c_int
}
crate::pipeline_v2::PipelineError::ModelLoad { .. } => {
PolyvoiceStatus::ModelLoad as c_int
}
crate::pipeline_v2::PipelineError::Registry(_) => {
PolyvoiceStatus::Registry as c_int
}
_ => PolyvoiceStatus::Inference as c_int,
})?;
let json =
serde_json::to_string(&result).map_err(|_| PolyvoiceStatus::Internal as c_int)?;
let len = json.len();
let cstr = CString::new(json).map_err(|_| PolyvoiceStatus::Internal as c_int)?;
let ptr_out = cstr.into_raw();
unsafe { *out_json = ptr_out;
*out_json_len = len;
}
Ok(())
}));
match r {
Ok(Ok(())) => PolyvoiceStatus::Ok as c_int,
Ok(Err(code)) => code,
Err(_) => PolyvoiceStatus::Internal as c_int,
}
}
#[unsafe(no_mangle)] #[rustfmt::skip]
pub unsafe extern "C" fn polyvoice_pipeline_run_format(
pipeline: *mut PolyvoicePipeline,
samples: *const c_float,
n_samples: usize,
sample_rate: u32,
format: c_int,
out_str: *mut *mut c_char,
out_str_len: *mut usize,
) -> c_int {
let r = catch_unwind(AssertUnwindSafe(|| -> Result<(), c_int> {
if pipeline.is_null() || samples.is_null() || out_str.is_null() || out_str_len.is_null() {
return Err(PolyvoiceStatus::InvalidArg as c_int);
}
if !(PolyvoiceFormat::Json as c_int..=PolyvoiceFormat::Txt as c_int).contains(&format) {
return Err(PolyvoiceStatus::InvalidArg as c_int);
}
let pipeline = unsafe { &*pipeline
};
const MAX_SAMPLES: usize = 16000 * 3600; if n_samples > MAX_SAMPLES {
return Err(PolyvoiceStatus::AudioTooLong as c_int);
}
let samples = unsafe { std::slice::from_raw_parts(samples, n_samples)
};
let sr = SampleRate::new(sample_rate)
.ok_or(PolyvoiceStatus::InvalidArg as c_int)?;
let result = pipeline
.inner
.run(samples, sr)
.map_err(|e| match e {
crate::pipeline_v2::PipelineError::UnsupportedSampleRate { .. } => {
PolyvoiceStatus::InvalidArg as c_int
}
crate::pipeline_v2::PipelineError::ModelLoad { .. } => {
PolyvoiceStatus::ModelLoad as c_int
}
crate::pipeline_v2::PipelineError::Registry(_) => {
PolyvoiceStatus::Registry as c_int
}
_ => PolyvoiceStatus::Inference as c_int,
})?;
let rendered = render_result(&result, format)?;
let len = rendered.len();
let cstr = CString::new(rendered).map_err(|_| PolyvoiceStatus::Internal as c_int)?;
let ptr_out = cstr.into_raw();
unsafe { *out_str = ptr_out;
*out_str_len = len;
}
Ok(())
}));
match r {
Ok(Ok(())) => PolyvoiceStatus::Ok as c_int,
Ok(Err(code)) => code,
Err(_) => PolyvoiceStatus::Internal as c_int,
}
}
#[unsafe(no_mangle)] #[rustfmt::skip]
pub unsafe extern "C" fn polyvoice_pipeline_destroy(pipeline: *mut PolyvoicePipeline) {
if !pipeline.is_null()
&& catch_unwind(AssertUnwindSafe(|| {
unsafe { drop(Box::from_raw(pipeline));
}
}))
.is_err()
{
eprintln!("polyvoice: panic during cleanup (foreign thread?)");
}
}
#[unsafe(no_mangle)] #[rustfmt::skip]
pub unsafe extern "C" fn polyvoice_free_string(p: *mut c_char, _n: usize) {
if !p.is_null()
&& catch_unwind(AssertUnwindSafe(|| {
unsafe { drop(CString::from_raw(p));
}
}))
.is_err()
{
eprintln!("polyvoice: panic during cleanup (foreign thread?)");
}
}
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{DiarizationResult, SpeakerId, SpeakerTurn, TimeRange};
fn sample_result() -> DiarizationResult {
let turns = vec![SpeakerTurn {
speaker: SpeakerId(0),
time: TimeRange {
start: 0.5,
end: 2.0,
},
text: Some("hello".to_owned()),
}];
DiarizationResult::new(vec![], turns, 1)
}
#[test]
fn render_result_rejects_unknown_format() {
let result = sample_result();
assert_eq!(
render_result(&result, 42),
Err(PolyvoiceStatus::InvalidArg as c_int)
);
assert_eq!(
render_result(&result, -1),
Err(PolyvoiceStatus::InvalidArg as c_int)
);
}
#[test]
fn render_result_covers_every_format() {
let result = sample_result();
for (format, marker) in [
(PolyvoiceFormat::Json as c_int, "num_speakers"),
(PolyvoiceFormat::Rttm as c_int, "SPEAKER audio 1"),
(PolyvoiceFormat::Srt as c_int, "00:00:00,500"),
(PolyvoiceFormat::Vtt as c_int, "WEBVTT"),
(PolyvoiceFormat::Txt as c_int, "SPEAKER_00: hello"),
] {
let rendered = render_result(&result, format).unwrap();
assert!(
rendered.contains(marker),
"format {format} missing marker {marker:?}: {rendered}"
);
}
}
}