//! 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, } pub fn run_stdio(engine: InferenceEngine) -> Result<(), Box> { 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::(&line) { Ok(req) => { eprintln!(" stdio: {} chars", req.prompt.len()); // Truncate u64 seed to u32 for llama.cpp sampler (deterministic // within same process, seed space reduction is acceptable). match engine.generate(&req.prompt, MAX_TOKENS, TEMPERATURE, TOP_P, req.seed.map(|s| s as u32)) { 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(()) }