st-tts 0.2.0

High-level on-device TTS via Supertonic — one-liner synthesize, auto-download models
use std::io::Read;
use std::path::PathBuf;

fn main() {
    let target_dir = get_target_dir();
    let lib_path = target_dir.join(native_lib_name());

    if lib_path.exists() {
        println!("cargo:warning=ORT native library exists at {}", lib_path.display());
        return;
    }

    println!("cargo:warning=Downloading ORT native library from NuGet...");

    let url = "https://www.nuget.org/api/v2/package/Microsoft.ML.OnnxRuntime/1.24.2";
    let response = ureq::get(url).call().unwrap_or_else(|e| {
        panic!("Failed to download ORT from {url}: {e}");
    });

    let mut zip_bytes: Vec<u8> = Vec::new();
    response
        .into_body()
        .into_reader()
        .read_to_end(&mut zip_bytes)
        .expect("Failed to read ORT NuGet response body");

    let cursor = std::io::Cursor::new(zip_bytes);
    let mut archive = zip::ZipArchive::new(cursor).expect("Failed to parse ORT NuGet package as ZIP");

    let runtime_path = nuget_runtime_path();
    let mut found = false;

    for i in 0..archive.len() {
        let mut file = archive.by_index(i).unwrap();
        if file.name().replace('\\', "/") == runtime_path {
            let mut dll_bytes: Vec<u8> = Vec::new();
            file.read_to_end(&mut dll_bytes)
                .unwrap_or_else(|e| panic!("Failed to read '{}' from ZIP: {e}", file.name()));
            std::fs::write(&lib_path, &dll_bytes)
                .unwrap_or_else(|e| panic!("Failed to write {}: {e}", lib_path.display()));
            println!(
                "cargo:warning=ORT native library written to {} ({} bytes)",
                lib_path.display(),
                dll_bytes.len()
            );
            found = true;
            break;
        }
    }

    if !found {
        panic!(
            "Could not find '{}' in ORT NuGet package ({} entries examined)",
            runtime_path,
            archive.len()
        );
    }
}

fn get_target_dir() -> PathBuf {
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));
    out_dir
        .ancestors()
        .nth(3)
        .expect("OUT_DIR has fewer than 4 ancestors")
        .to_path_buf()
}

fn native_lib_name() -> String {
    if cfg!(target_os = "windows") {
        "onnxruntime.dll".into()
    } else if cfg!(target_os = "linux") {
        "libonnxruntime.so".into()
    } else if cfg!(target_os = "macos") {
        "libonnxruntime.dylib".into()
    } else {
        panic!("unsupported target OS");
    }
}

fn nuget_runtime_path() -> String {
    let target = std::env::var("TARGET").expect("TARGET not set");
    if target.contains("windows") {
        "runtimes/win-x64/native/onnxruntime.dll"
    } else if target.contains("linux") {
        "runtimes/linux-x64/native/libonnxruntime.so"
    } else if target.contains("apple-darwin") {
        if target.contains("aarch64") {
            "runtimes/osx-arm64/native/libonnxruntime.dylib"
        } else {
            "runtimes/osx-x64/native/libonnxruntime.dylib"
        }
    } else if target.contains("apple-ios") {
        panic!("iOS not supported with load-dynamic");
    } else {
        panic!("unsupported target triple: {target}");
    }
    .into()
}