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
+106 -1
View File
@@ -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)),