rvsttd: model fetch mode, build from source, ~/.rvsttd convention

This commit is contained in:
2026-08-13 10:31:46 +00:00
parent 4be00bc4f1
commit b2b0c0d579
4 changed files with 208 additions and 4 deletions
+3
View File
@@ -10,6 +10,9 @@ path = "src/main.rs"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
cpal = "0.15" cpal = "0.15"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ureq = "2"
[build-dependencies] [build-dependencies]
bindgen = "0.71" bindgen = "0.71"
+4 -3
View File
@@ -1,9 +1,10 @@
use std::path::PathBuf; use std::path::PathBuf;
fn main() { fn main() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let lib_dir = manifest_dir.join("..").join("bench").join("moonshine-voice").join("lib"); let rvsttd_dir = PathBuf::from(&home).join(".rvsttd");
let include_dir = manifest_dir.join("..").join("bench").join("moonshine-voice").join("include"); let lib_dir = rvsttd_dir.join("lib");
let include_dir = rvsttd_dir.join("include");
let header = include_dir.join("moonshine-c-api.h"); let header = include_dir.join("moonshine-c-api.h");
println!("cargo:rerun-if-changed={}", header.display()); println!("cargo:rerun-if-changed={}", header.display());
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
RVSTTD_DIR="${HOME}/.rvsttd"
LIB_DIR="${RVSTTD_DIR}/lib"
INCLUDE_DIR="${RVSTTD_DIR}/include"
BUILD_DIR="${RVSTTD_DIR}/build"
ARCH="linux-x86_64"
# Fetch latest release tag from GitHub API
VERSION=$(curl -s https://api.github.com/repos/moonshine-ai/moonshine/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "${VERSION}" ]; then
echo "ERROR: could not fetch latest release tag from GitHub API"
exit 1
fi
PREBUILT_URL="https://github.com/moonshine-ai/moonshine/releases/download/${VERSION}/moonshine-voice-${ARCH}.tar.gz"
SOURCE_URL="https://github.com/moonshine-ai/moonshine/archive/refs/tags/${VERSION}.tar.gz"
echo "=== rvsttd setup ==="
echo "Latest Moonshine release: ${VERSION}"
echo "Target: ${RVSTTD_DIR}"
echo ""
mkdir -p "${LIB_DIR}" "${INCLUDE_DIR}" "${BUILD_DIR}"
# Step 1: Download prebuilt package (for libonnxruntime.so.1 + header)
PREBUILT_TGZ="${BUILD_DIR}/moonshine-voice-${ARCH}.tar.gz"
PREBUILT_EXTRACTED="${BUILD_DIR}/moonshine-voice-${ARCH}"
if [ ! -f "${LIB_DIR}/libonnxruntime.so.1" ]; then
echo ">>> Downloading prebuilt package (for libonnxruntime.so.1)..."
curl -L -o "${PREBUILT_TGZ}" "${PREBUILT_URL}"
mkdir -p "${PREBUILT_EXTRACTED}"
tar xzf "${PREBUILT_TGZ}" -C "${PREBUILT_EXTRACTED}" --strip-components=1
# Copy ONNX Runtime (prebuilt is fine — it has no glibc issue)
cp "${PREBUILT_EXTRACTED}/lib/libonnxruntime.so.1" "${LIB_DIR}/"
echo " Installed libonnxruntime.so.1"
# Copy the header (it's the same in source and prebuilt)
cp "${PREBUILT_EXTRACTED}/include/moonshine-c-api.h" "${INCLUDE_DIR}/"
echo " Installed moonshine-c-api.h"
else
echo ">>> libonnxruntime.so.1 already present, skipping prebuilt download"
fi
# Step 2: Download source
SOURCE_TGZ="${BUILD_DIR}/moonshine-source.tar.gz"
SOURCE_DIR="${BUILD_DIR}/moonshine-source"
if [ ! -d "${SOURCE_DIR}" ]; then
echo ">>> Downloading Moonshine source ${VERSION}..."
curl -L -o "${SOURCE_TGZ}" "${SOURCE_URL}"
mkdir -p "${SOURCE_DIR}"
tar xzf "${SOURCE_TGZ}" -C "${SOURCE_DIR}" --strip-components=1
fi
# Step 3: Build libmoonshine.so from source
CMAKE_BUILD="${BUILD_DIR}/cmake-build"
if [ ! -f "${LIB_DIR}/libmoonshine.so" ]; then
echo ">>> Building libmoonshine.so from source..."
# Point CMake at the prebuilt ONNX Runtime
ORT_LIB_DIR="${LIB_DIR}"
ORT_INCLUDE_DIR="${SOURCE_DIR}/core/third-party/onnxruntime/include"
mkdir -p "${CMAKE_BUILD}"
cd "${CMAKE_BUILD}"
cmake "${SOURCE_DIR}/core" \
-DCMAKE_BUILD_TYPE=Release \
-DONNXRUNTIME_LIB_PATH="${ORT_LIB_DIR}/libonnxruntime.so.1" \
-DMOONSHINE_TTS_BUILD_ONNX=OFF
make -j"$(nproc)" moonshine
cp "${CMAKE_BUILD}/libmoonshine.so" "${LIB_DIR}/"
cp "${SOURCE_DIR}/core/moonshine-c-api.h" "${INCLUDE_DIR}/"
echo " Installed libmoonshine.so (built from source)"
else
echo ">>> libmoonshine.so already present, skipping build"
fi
echo ""
echo "=== Setup complete ==="
echo "Library: ${LIB_DIR}/libmoonshine.so"
echo "Library: ${LIB_DIR}/libonnxruntime.so.1"
echo "Header: ${INCLUDE_DIR}/moonshine-c-api.h"
echo ""
echo "Next: cd rvsttd && cargo build --release"
echo "Then: ./target/release/rvsttd fetch # downloads the model"
echo "Then: ./target/release/rvsttd # starts the server"
+106 -1
View File
@@ -1,10 +1,12 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, SampleRate}; use cpal::{SampleFormat, SampleRate};
use serde::Deserialize;
use std::collections::HashSet; use std::collections::HashSet;
use std::ffi::CStr; use std::ffi::CStr;
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream}; use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
@@ -353,11 +355,112 @@ fn start_cpal(
Ok(stream) Ok(stream)
} }
// ─── model fetch ──────────────────────────────────────────────────────────
#[derive(Deserialize)]
struct Manifest {
groups: Vec<ManifestGroup>,
}
#[derive(Deserialize)]
struct ManifestGroup {
#[allow(dead_code)]
base_url: String,
files: Vec<ManifestFile>,
}
#[derive(Deserialize)]
struct ManifestFile {
name: String,
url: String,
size: Option<u64>,
}
fn fetch_model(model_dir: &str) -> Result<()> {
let dest = PathBuf::from(model_dir);
log(&format!("Fetching medium-streaming-en model to {}", dest.display()));
let lang = std::ffi::CString::new("en").unwrap();
let opt_name = std::ffi::CString::new("model_arch").unwrap();
let opt_value = std::ffi::CString::new("5").unwrap();
let mut options = [moonshine_option_t {
name: opt_name.as_ptr(),
value: opt_value.as_ptr(),
}];
let mut json_ptr: *mut i8 = std::ptr::null_mut();
let rc = unsafe {
moonshine_get_stt_dependencies(
lang.as_ptr(),
options.as_mut_ptr(),
options.len() as u64,
&mut json_ptr,
)
};
if rc != 0 || json_ptr.is_null() {
return Err(anyhow!("moonshine_get_stt_dependencies failed: {}", err_str(rc)));
}
let json_str = unsafe { CStr::from_ptr(json_ptr) }
.to_string_lossy()
.into_owned();
unsafe { moonshine_free_buffer(json_ptr as *mut std::ffi::c_void) };
let manifest: Manifest = serde_json::from_str(&json_str)?;
std::fs::create_dir_all(&dest)?;
let mut total_files = 0;
let mut total_bytes: u64 = 0;
for group in &manifest.groups {
for file in &group.files {
let dest_path = dest.join(&file.name);
if dest_path.exists() {
log(&format!(" SKIP {} (already exists)", file.name));
continue;
}
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent)?;
}
log(&format!(" GET {}", file.url));
let response = ureq::get(&file.url).call()?;
if let Some(expected) = file.size {
log(&format!(" {} bytes", expected));
total_bytes += expected;
}
let mut file_handle = std::fs::File::create(&dest_path)?;
std::io::copy(&mut response.into_reader(), &mut file_handle)?;
total_files += 1;
}
}
log(&format!("Done: {} files, ~{} MB", total_files, total_bytes / (1024 * 1024)));
log(&format!("Model directory: {}", dest.display()));
Ok(())
}
// ─── main ───────────────────────────────────────────────────────────────── // ─── main ─────────────────────────────────────────────────────────────────
fn main() -> Result<()> { fn main() -> Result<()> {
let mut args = std::env::args().skip(1); let mut args = std::env::args().skip(1);
let mut model_dir = String::from("../bench/medium-streaming-en");
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let default_model = format!("{}/.rvsttd/model", home);
let first = args.next();
if first.as_deref() == Some("fetch") {
let model_dir = args.next().unwrap_or_else(|| default_model.clone());
return fetch_model(&model_dir);
}
let mut args = first.into_iter().chain(args);
let mut model_dir = default_model;
while let Some(a) = args.next() { while let Some(a) = args.next() {
match a.as_str() { match a.as_str() {
@@ -366,8 +469,10 @@ fn main() -> Result<()> {
} }
"--help" | "-h" => { "--help" | "-h" => {
println!("Usage: rvsttd [--model-dir DIR]"); println!("Usage: rvsttd [--model-dir DIR]");
println!(" rvsttd fetch [DIR]");
println!("Listens on TCP {}", BIND_ADDR); println!("Listens on TCP {}", BIND_ADDR);
println!("Model: medium-streaming (Moonshine)"); println!("Model: medium-streaming (Moonshine)");
println!("'rvsttd fetch' downloads the English medium-streaming model");
return Ok(()); return Ok(());
} }
_ => return Err(anyhow!("unknown arg: {}", a)), _ => return Err(anyhow!("unknown arg: {}", a)),