use std::ffi::{CStr, CString};
use std::sync::Once;
use bzip2::read::BzDecoder;
use piper_phonemize_sys as sys;
use tar::Archive;
static INIT: Once = Once::new();
static mut INITIALIZED: bool = false;
pub struct PiperPhonemizeResult {
ptr: *const sys::PiperPhonemizeResult,
}
unsafe impl Send for PiperPhonemizeResult {}
unsafe impl Sync for PiperPhonemizeResult {}
impl PiperPhonemizeResult {
pub fn num_sentences(&self) -> usize {
unsafe { sys::PiperPhonemizeResultGetNumSentences(self.ptr) as usize }
}
pub fn num_phonemes(&self, sentence_id: usize) -> Option<usize> {
let n = unsafe {
sys::PiperPhonemizeResultGetNumPhonemes(self.ptr, sentence_id as i32)
};
if n < 0 {
None
} else {
Some(n as usize)
}
}
pub fn get_phonemes(&self, sentence_id: usize) -> Option<Vec<u32>> {
let n = self.num_phonemes(sentence_id)?;
if n == 0 {
return Some(Vec::new());
}
let ptr = unsafe {
sys::PiperPhonemizeResultGetPhonemes(self.ptr, sentence_id as i32)
};
if ptr.is_null() {
return None;
}
let phonemes = unsafe { std::slice::from_raw_parts(ptr, n) };
Some(phonemes.to_vec())
}
pub fn get_phonemes_as_string(&self, sentence_id: usize) -> Option<String> {
let phonemes = self.get_phonemes(sentence_id)?;
let s: String = phonemes
.iter()
.filter_map(|&cp| char::from_u32(cp))
.collect();
Some(s)
}
}
impl Drop for PiperPhonemizeResult {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
sys::PiperPhonemizeDestroyResult(self.ptr as *mut _);
}
}
}
}
const ESPEAK_NG_DATA_BZ2: &[u8] = include_bytes!(env!("ESPEAK_NG_DATA_PATH"));
fn extract_espeak_ng_data() -> Result<String, &'static str> {
let tmp_dir = std::env::temp_dir().join("piper-phonemize-espeak-ng-data");
let data_dir = tmp_dir.join("espeak-ng-data");
if data_dir.join("phontab").exists() {
return Ok(data_dir.to_string_lossy().into_owned());
}
if tmp_dir.exists() {
std::fs::remove_dir_all(&tmp_dir).map_err(|_| "Failed to clean stale espeak-ng-data")?;
}
std::fs::create_dir_all(&tmp_dir).map_err(|_| "Failed to create temp directory")?;
let bz = BzDecoder::new(ESPEAK_NG_DATA_BZ2);
let mut archive = Archive::new(bz);
archive.unpack(&tmp_dir).map_err(|_| "Failed to extract espeak-ng-data")?;
if data_dir.join("phontab").exists() {
Ok(data_dir.to_string_lossy().into_owned())
} else {
Err("espeak-ng-data extraction incomplete: phontab not found")
}
}
fn ensure_initialized() {
unsafe {
INIT.call_once(|| {
match extract_espeak_ng_data() {
Ok(data_dir) => {
let c_dir = CString::new(data_dir).unwrap();
let ret = sys::PiperPhonemizeInitialize(c_dir.as_ptr());
INITIALIZED = ret > 0;
}
Err(e) => {
eprintln!("Failed to extract espeak-ng-data: {}", e);
}
}
});
}
}
pub fn initialize(data_dir: &str) -> Result<i32, &'static str> {
let c_dir = CString::new(data_dir).map_err(|_| "data_dir contains null byte")?;
let ret = unsafe { sys::PiperPhonemizeInitialize(c_dir.as_ptr()) };
if ret < 0 {
Err("Failed to initialize espeak-ng")
} else {
unsafe { INITIALIZED = true; }
Ok(ret)
}
}
pub fn phonemize(text: &str, voice: &str) -> Option<PiperPhonemizeResult> {
ensure_initialized();
let c_text = CString::new(text).ok()?;
let c_voice = CString::new(voice).ok()?;
let ptr = unsafe {
sys::PiperPhonemizeText(c_text.as_ptr(), c_voice.as_ptr())
};
if ptr.is_null() {
None
} else {
Some(PiperPhonemizeResult { ptr })
}
}
pub fn phonemize_to_string(text: &str, voice: &str) -> Option<Vec<String>> {
let result = phonemize(text, voice)?;
let mut sentences = Vec::new();
for i in 0..result.num_sentences() {
if let Some(s) = result.get_phonemes_as_string(i) {
sentences.push(s);
}
}
Some(sentences)
}
pub fn get_version() -> String {
unsafe {
let ptr = sys::PiperPhonemizeGetVersionStr();
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}