sensevoice 0.1.1

Rust bindings for SenseVoice ASR with a ggml/FSMN-VAD native runtime
Documentation
use std::collections::hash_map::DefaultHasher;
use std::env;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;

fn run(mut command: Command) {
    let status = command.status().unwrap_or_else(|error| {
        panic!("failed to start {:?}: {error}", command);
    });
    if !status.success() {
        panic!("{:?} failed with status {status}", command);
    }
}

fn main() {
    println!("cargo:rerun-if-env-changed=DOCS_RS");
    if env::var_os("DOCS_RS").is_some() {
        return;
    }

    let manifest_dir =
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"));
    let runtime_dir = manifest_dir.join("runtime").join("llama.cpp");
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set"));
    let build_dir = out_dir.join(format!("funasr-cmake-{:x}", path_hash(&runtime_dir)));
    let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
    let build_type = if profile == "release" {
        "Release"
    } else {
        "RelWithDebInfo"
    };

    println!("cargo:rerun-if-env-changed=FUNASR_LLAMA_SOURCE");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/CMakeLists.txt");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-capi/funasr_c_api.h");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-capi/funasr_c_api.cpp");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-common/funasr_sensevoice.h");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-common/funasr_vad.h");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-common/funasr_audio.h");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-common/miniaudio.h");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-sensevoice/funasr-sensevoice.cpp");
    println!("cargo:rerun-if-changed=runtime/llama.cpp/funasr-vad/funasr-vad.cpp");

    let mut configure = Command::new("cmake");
    configure
        .arg("-S")
        .arg(&runtime_dir)
        .arg("-B")
        .arg(&build_dir)
        .arg(format!("-DCMAKE_BUILD_TYPE={build_type}"))
        .arg("-DFUNASR_BUILD_RUST=ON")
        .arg("-DLLAMA_CURL=OFF");
    add_llama_source_override(&mut configure);
    run(configure);

    let mut build = Command::new("cmake");
    build
        .arg("--build")
        .arg(&build_dir)
        .arg("--config")
        .arg(build_type)
        .arg("--target")
        .arg("funasr_rs")
        .arg("--parallel");
    run(build);

    let lib_dirs = native_library_dirs(&build_dir, build_type);
    for lib_dir in &lib_dirs {
        println!("cargo:rustc-link-search=native={}", lib_dir.display());
    }
    println!("cargo:rustc-link-lib=dylib=funasr_rs");

    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
    if target_os == "windows" {
        install_windows_runtime(&build_dir, &out_dir, build_type);
    } else if target_os == "macos" || target_os == "linux" {
        if let Some(lib_dir) = lib_dirs.first() {
            println!(
                "cargo:rustc-link-arg=-Wl,-rpath,{}",
                display_for_linker(lib_dir)
            );
        }
    }
}

fn install_windows_runtime(build_dir: &Path, out_dir: &Path, build_type: &str) {
    let dll = windows_runtime_candidates(build_dir, build_type)
        .into_iter()
        .find(|path| path.is_file())
        .unwrap_or_else(|| {
            panic!(
                "CMake built funasr_rs but its Windows runtime DLL was not found under {}",
                build_dir.display()
            )
        });
    let profile_dir = cargo_profile_dir(out_dir).unwrap_or_else(|| {
        panic!(
            "cannot determine Cargo profile directory from OUT_DIR={}",
            out_dir.display()
        )
    });

    for destination_dir in [
        profile_dir.to_path_buf(),
        profile_dir.join("deps"),
        profile_dir.join("examples"),
    ] {
        fs::create_dir_all(&destination_dir).unwrap_or_else(|error| {
            panic!(
                "failed to create Windows runtime directory {}: {error}",
                destination_dir.display()
            )
        });
        let destination = destination_dir.join("funasr_rs.dll");
        fs::copy(&dll, &destination).unwrap_or_else(|error| {
            panic!(
                "failed to copy {} to {}: {error}",
                dll.display(),
                destination.display()
            )
        });
    }

    println!("cargo::metadata=runtime_dll={}", dll.display());
}

fn windows_runtime_candidates(build_dir: &Path, build_type: &str) -> Vec<PathBuf> {
    let bin_dir = build_dir.join("bin");
    let lib_dir = build_dir.join("lib");
    [
        bin_dir.join("funasr_rs.dll"),
        bin_dir.join(build_type).join("funasr_rs.dll"),
        bin_dir.join("libfunasr_rs.dll"),
        bin_dir.join(build_type).join("libfunasr_rs.dll"),
        lib_dir.join("funasr_rs.dll"),
        lib_dir.join(build_type).join("funasr_rs.dll"),
    ]
    .into_iter()
    .collect()
}

fn cargo_profile_dir(out_dir: &Path) -> Option<&Path> {
    let build_root = out_dir.parent()?.parent()?;
    if build_root.file_name()? != "build" {
        return None;
    }
    build_root.parent()
}

fn add_llama_source_override(configure: &mut Command) {
    let Some(llama_source) = env::var_os("FUNASR_LLAMA_SOURCE") else {
        return;
    };
    let llama_source = PathBuf::from(llama_source);
    if !llama_source.join("CMakeLists.txt").is_file() {
        panic!(
            "FUNASR_LLAMA_SOURCE must point to a llama.cpp checkout containing CMakeLists.txt: {}",
            llama_source.display()
        );
    }
    configure.arg(format!(
        "-DFETCHCONTENT_SOURCE_DIR_LLAMA={}",
        llama_source.display()
    ));
}

fn native_library_dirs(build_dir: &Path, build_type: &str) -> Vec<PathBuf> {
    let lib_dir = build_dir.join("lib");
    let config_lib_dir = lib_dir.join(build_type);

    let mut dirs = vec![lib_dir];
    if config_lib_dir.is_dir() {
        dirs.push(config_lib_dir);
    }
    dirs
}

fn display_for_linker(path: &Path) -> String {
    path.to_string_lossy().into_owned()
}

fn path_hash(path: &Path) -> u64 {
    let mut hasher = DefaultHasher::new();
    path.hash(&mut hasher);
    hasher.finish()
}