Critical fixes: - Pause mechanism: workers now hold requests during pause instead of dropping them. Queue and worker pool share the same AtomicBool flag via VoiceQueue::paused_flag(). Submit() rejects while paused. - Seed type: sr-voice accepts u64 seeds over IPC (explicit u32 truncation for llama.cpp sampler, documented). Warning fixes: - HashMap → BTreeMap in cache.rs and worker.rs (D-010 determinism mandate). Added Ord derives to CacheKey, ContentType, TellCategory. - VoicePipe::generate() watchdog kills child after 120s timeout to prevent indefinite blocking on read_line. - VoiceCacheStore Drop impl calls save_all() on shutdown. - trait-modifiers.ron: fixed 3 wrong trait names (Impulsive→Compassionate, Methodical→Incurious, Stubborn→Ruthless) to match PersonalityTrait enum. Suggestion fixes: - Worker spawn: log error + reduce pool instead of panic on thread failure. - on_battery(): added macOS detection via pmset. - Epistemic markers: lowercased constants, removed redundant to_lowercase(). - cache.rs: documented non-atomic write tradeoff. - queue.rs: reprioritize() bypasses pause check (it runs during pause). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
135 lines
4.5 KiB
Rust
135 lines
4.5 KiB
Rust
use std::time::Instant;
|
|
|
|
use tiny_http::{Header, Method, Response, Server};
|
|
|
|
use crate::inference::InferenceEngine;
|
|
use crate::prompt::PromptPayload;
|
|
|
|
const MAX_TOKENS: u32 = 64;
|
|
const TEMPERATURE: f32 = 0.7;
|
|
const TOP_P: f32 = 0.9;
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct GenerateRequest {
|
|
prompt: String,
|
|
seed: Option<u64>,
|
|
}
|
|
|
|
pub fn run_server(
|
|
engine: InferenceEngine,
|
|
port: u16,
|
|
model_name: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let addr = format!("127.0.0.1:{}", port);
|
|
let server = Server::http(&addr)
|
|
.map_err(|e| format!("Failed to bind {}: {}", addr, e))?;
|
|
|
|
let start = Instant::now();
|
|
eprintln!("sr-voice server ready on http://{}", addr);
|
|
eprintln!(" model: {}", model_name);
|
|
eprintln!(" POST /generate POST /batch GET /health");
|
|
|
|
for request in server.incoming_requests() {
|
|
let path = request.url().to_string();
|
|
let method = request.method().clone();
|
|
|
|
match (method, path.as_str()) {
|
|
(Method::Get, "/health") => {
|
|
let body = serde_json::json!({
|
|
"status": "ready",
|
|
"model": model_name,
|
|
"uptime_secs": start.elapsed().as_secs(),
|
|
});
|
|
respond(request, 200, &body.to_string());
|
|
}
|
|
(Method::Post, "/generate") => handle_generate(&engine, request),
|
|
(Method::Post, "/batch") => handle_batch(&engine, request),
|
|
_ => {
|
|
respond(request, 404, &serde_json::json!({"error": "not found"}).to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_generate(engine: &InferenceEngine, mut request: tiny_http::Request) {
|
|
let mut body = String::new();
|
|
if std::io::Read::read_to_string(request.as_reader(), &mut body).is_err() {
|
|
respond(request, 400, r#"{"error":"failed to read body"}"#);
|
|
return;
|
|
}
|
|
|
|
let req: GenerateRequest = match serde_json::from_str(&body) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
let msg = serde_json::json!({"error": format!("invalid JSON: {}", e)});
|
|
respond(request, 400, &msg.to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
eprintln!(" generate: {} chars", req.prompt.len());
|
|
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);
|
|
respond(request, 200, &serde_json::to_string(&result).unwrap());
|
|
}
|
|
Err(e) => {
|
|
let msg = serde_json::json!({"error": e.to_string()});
|
|
respond(request, 500, &msg.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_batch(engine: &InferenceEngine, mut request: tiny_http::Request) {
|
|
let mut body = String::new();
|
|
if std::io::Read::read_to_string(request.as_reader(), &mut body).is_err() {
|
|
respond(request, 400, r#"{"error":"failed to read body"}"#);
|
|
return;
|
|
}
|
|
|
|
let payloads: Vec<PromptPayload> = match serde_json::from_str(&body) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
let msg = serde_json::json!({"error": format!("invalid JSON: {}", e)});
|
|
respond(request, 400, &msg.to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
eprintln!(" batch: {} prompts", payloads.len());
|
|
let mut output = String::new();
|
|
for payload in &payloads {
|
|
match engine.generate(&payload.prompt, MAX_TOKENS, TEMPERATURE, TOP_P, None) {
|
|
Ok(result) => {
|
|
eprintln!(" -> {}: {} tokens, {:.1} t/s", payload.id, result.tokens_generated, result.tokens_per_sec);
|
|
#[derive(serde::Serialize)]
|
|
struct BatchLine<'a> {
|
|
id: &'a str,
|
|
#[serde(flatten)]
|
|
result: &'a crate::inference::GenerationResult,
|
|
}
|
|
let line = serde_json::to_string(&BatchLine { id: &payload.id, result: &result }).unwrap();
|
|
output.push_str(&line);
|
|
output.push('\n');
|
|
}
|
|
Err(e) => {
|
|
let line = serde_json::json!({"id": payload.id, "error": e.to_string()});
|
|
output.push_str(&line.to_string());
|
|
output.push('\n');
|
|
}
|
|
}
|
|
}
|
|
|
|
respond(request, 200, &output);
|
|
}
|
|
|
|
fn respond(request: tiny_http::Request, status: u16, body: &str) {
|
|
let header = Header::from_bytes("Content-Type", "application/json").unwrap();
|
|
let response = Response::from_string(body)
|
|
.with_status_code(status)
|
|
.with_header(header);
|
|
let _ = request.respond(response);
|
|
}
|