rvsttd: model fetch mode, build from source, ~/.rvsttd convention
This commit is contained in:
@@ -10,6 +10,9 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
cpal = "0.15"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ureq = "2"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.71"
|
||||
|
||||
+4
-3
@@ -1,9 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let lib_dir = manifest_dir.join("..").join("bench").join("moonshine-voice").join("lib");
|
||||
let include_dir = manifest_dir.join("..").join("bench").join("moonshine-voice").join("include");
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||
let rvsttd_dir = PathBuf::from(&home).join(".rvsttd");
|
||||
let lib_dir = rvsttd_dir.join("lib");
|
||||
let include_dir = rvsttd_dir.join("include");
|
||||
let header = include_dir.join("moonshine-c-api.h");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", header.display());
|
||||
|
||||
@@ -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
@@ -1,10 +1,12 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{SampleFormat, SampleRate};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::CStr;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
@@ -353,11 +355,112 @@ fn start_cpal(
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() -> Result<()> {
|
||||
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() {
|
||||
match a.as_str() {
|
||||
@@ -366,8 +469,10 @@ fn main() -> Result<()> {
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("Usage: rvsttd [--model-dir DIR]");
|
||||
println!(" rvsttd fetch [DIR]");
|
||||
println!("Listens on TCP {}", BIND_ADDR);
|
||||
println!("Model: medium-streaming (Moonshine)");
|
||||
println!("'rvsttd fetch' downloads the English medium-streaming model");
|
||||
return Ok(());
|
||||
}
|
||||
_ => return Err(anyhow!("unknown arg: {}", a)),
|
||||
|
||||
Reference in New Issue
Block a user