refactor(voice): replace HTTP with stdin/stdout IPC for sr-voice workers
Gemma 2 T&C compliance: exposed HTTP ports allow mods or external code to reach the model, complicating license enforcement. Switch to piped stdin/stdout (JSONL protocol) so the model is only reachable through the game server's internal queue. - worker.rs: VoicePipe owns Child + piped stdin/stdout, VoiceProcessConfig replaces port-based config, workers spawn their own sr-voice child - hardware.rs: remove VoiceInstanceManager (port/process lifecycle), replace with evaluate_scaling() free function + HardwareProbe::voice_config() - sr-voice: add --stdio flag to serve command, new stdio.rs JSONL mode - Remove ureq dependency from server crate (no longer needed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
mod inference;
|
||||
mod prompt;
|
||||
mod server;
|
||||
mod stdio;
|
||||
|
||||
use std::io::Read;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -34,7 +35,7 @@ enum Command {
|
||||
/// Path to GGUF model file
|
||||
#[arg(long)]
|
||||
model: String,
|
||||
/// Listen port
|
||||
/// Listen port (ignored when --stdio is set)
|
||||
#[arg(long, default_value = "8321")]
|
||||
port: u16,
|
||||
/// CPU threads for inference
|
||||
@@ -43,6 +44,11 @@ enum Command {
|
||||
/// Context window size in tokens
|
||||
#[arg(long, default_value = "512")]
|
||||
ctx_size: u32,
|
||||
/// Run in stdio mode: read JSONL from stdin, write JSONL to stdout.
|
||||
/// No network port is opened. Used by the game server's worker pool
|
||||
/// for Gemma 2 T&C compliance (no exposed inference endpoint).
|
||||
#[arg(long)]
|
||||
stdio: bool,
|
||||
},
|
||||
/// Generate text from a single prompt (requires running server)
|
||||
Generate {
|
||||
@@ -83,7 +89,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Command::Serve { model, port, threads, ctx_size } => {
|
||||
Command::Serve { model, port, threads, ctx_size, stdio } => {
|
||||
let threads = threads.unwrap_or_else(default_threads);
|
||||
let config = InferenceConfig {
|
||||
model_path: model.clone(),
|
||||
@@ -96,12 +102,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let engine = InferenceEngine::load(&config)?;
|
||||
eprintln!("Model loaded ({} threads, {} ctx)", threads, ctx_size);
|
||||
|
||||
let model_name = std::path::Path::new(&model)
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or(model);
|
||||
if stdio {
|
||||
eprintln!("sr-voice stdio mode — reading JSONL from stdin");
|
||||
stdio::run_stdio(engine)?;
|
||||
} else {
|
||||
let model_name = std::path::Path::new(&model)
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or(model);
|
||||
|
||||
server::run_server(engine, port, &model_name)?;
|
||||
server::run_server(engine, port, &model_name)?;
|
||||
}
|
||||
}
|
||||
Command::Generate { port, seed, prompt_file } => {
|
||||
let prompt = read_prompt(prompt_file)?;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Stdio JSONL mode for sr-voice (D-138, Spike 2).
|
||||
//!
|
||||
//! Reads one JSON object per line from stdin, runs inference, writes one JSON
|
||||
//! object per line to stdout. No network port is opened — the model is only
|
||||
//! reachable through the parent process's pipe (Gemma 2 T&C compliance).
|
||||
//!
|
||||
//! Request format: {"prompt": "...", "seed": 42}
|
||||
//! Response format: {"text": "...", "tokens_generated": N, ...}
|
||||
//! or {"error": "..."}
|
||||
|
||||
use std::io::{self, BufRead, Write};
|
||||
|
||||
use crate::inference::InferenceEngine;
|
||||
|
||||
const MAX_TOKENS: u32 = 64;
|
||||
const TEMPERATURE: f32 = 0.7;
|
||||
const TOP_P: f32 = 0.9;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct StdioRequest {
|
||||
prompt: String,
|
||||
seed: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn run_stdio(engine: InferenceEngine) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let stdin = io::stdin().lock();
|
||||
let mut stdout = io::stdout().lock();
|
||||
|
||||
for line in stdin.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("stdin read error: {}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let response = match serde_json::from_str::<StdioRequest>(&line) {
|
||||
Ok(req) => {
|
||||
eprintln!(" stdio: {} chars", req.prompt.len());
|
||||
match engine.generate(&req.prompt, MAX_TOKENS, TEMPERATURE, TOP_P, req.seed) {
|
||||
Ok(result) => {
|
||||
eprintln!(
|
||||
" -> {} tokens, {:.1} t/s",
|
||||
result.tokens_generated, result.tokens_per_sec
|
||||
);
|
||||
serde_json::to_string(&result).unwrap()
|
||||
}
|
||||
Err(e) => {
|
||||
serde_json::json!({"error": e.to_string()}).to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
serde_json::json!({"error": format!("invalid JSON: {}", e)}).to_string()
|
||||
}
|
||||
};
|
||||
|
||||
writeln!(stdout, "{}", response)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
eprintln!("sr-voice stdio mode — stdin closed, exiting");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user