rvsttd: model fetch mode, build from source, ~/.rvsttd convention
This commit is contained in:
@@ -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
@@ -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());
|
||||||
|
|||||||
@@ -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 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)),
|
||||||
|
|||||||
Reference in New Issue
Block a user