// Transkripsjons-dispatcher — delegerer til synops-transcribe CLI. // // Maskinrommet orkestrerer, CLI-verktøyet gjør jobben. // Ref: docs/retninger/unix_filosofi.md use crate::cas::CasStore; use crate::jobs::JobRow; use std::process::Stdio; use uuid::Uuid; /// Synops-transcribe binary path. /// Søker i PATH, men kan overrides med SYNOPS_TRANSCRIBE_BIN. fn transcribe_bin() -> String { std::env::var("SYNOPS_TRANSCRIBE_BIN") .unwrap_or_else(|_| "synops-transcribe".to_string()) } /// Handler for whisper_transcribe-jobber. /// /// Spawner synops-transcribe med --write for å gjøre alt arbeidet: /// Whisper-kall, SRT-parsing, DB-skriving, ressurslogging. /// /// Payload forventer: /// - media_node_id: UUID — noden som skal oppdateres /// - cas_hash: String — CAS-nøkkel til lydfilen /// - mime: String — MIME-type (brukes for filnavn-hint) /// - language: String (valgfritt, default "no") /// - initial_prompt: String (valgfritt — navneliste for bedre egennavn) /// - requested_by: UUID (valgfritt — brukeren som utløste jobben) pub async fn handle_whisper_job( job: &JobRow, cas: &CasStore, whisper_url: &str, ) -> Result { let media_node_id: Uuid = job.payload["media_node_id"] .as_str() .ok_or("Mangler media_node_id i payload")? .parse() .map_err(|e| format!("Ugyldig media_node_id: {e}"))?; let cas_hash = job.payload["cas_hash"] .as_str() .ok_or("Mangler cas_hash i payload")?; let mime = job.payload["mime"] .as_str() .unwrap_or("audio/mpeg"); let language = job.payload["language"] .as_str() .unwrap_or("no"); let model = std::env::var("WHISPER_MODEL") .unwrap_or_else(|_| "medium".to_string()); // Hent initial_prompt: payload > miljøvariabel > ingen let initial_prompt = match job.payload["initial_prompt"].as_str() { Some(p) => Some(p.to_string()), None => std::env::var("WHISPER_INITIAL_PROMPT").ok(), }; // Bygg kommando let bin = transcribe_bin(); let mut cmd = tokio::process::Command::new(&bin); cmd.arg("--cas-hash").arg(cas_hash) .arg("--model").arg(&model) .arg("--language").arg(language) .arg("--mime").arg(mime) .arg("--node-id").arg(media_node_id.to_string()) .arg("--write"); if let Some(ref prompt) = initial_prompt { cmd.arg("--initial-prompt").arg(prompt); } if let Some(requested_by) = job.payload["requested_by"].as_str() { cmd.arg("--requested-by").arg(requested_by); } // Sett miljøvariabler CLI-verktøyet trenger let db_url = std::env::var("DATABASE_URL") .map_err(|_| "DATABASE_URL ikke satt".to_string())?; let cas_root = cas.root().to_string_lossy().to_string(); cmd.env("DATABASE_URL", &db_url) .env("CAS_ROOT", &cas_root) .env("WHISPER_URL", whisper_url); cmd.stdout(Stdio::piped()) .stderr(Stdio::piped()); tracing::info!( media_node_id = %media_node_id, cas_hash = %cas_hash, model = %model, bin = %bin, "Starter synops-transcribe" ); // Spawn og vent let child = cmd.spawn().map_err(|e| format!("Kunne ikke starte {bin}: {e}"))?; let output = child .wait_with_output() .await .map_err(|e| format!("Feil ved kjøring av {bin}: {e}"))?; let stderr = String::from_utf8_lossy(&output.stderr); if !stderr.is_empty() { tracing::info!(stderr = %stderr, "synops-transcribe stderr"); } if !output.status.success() { let code = output.status.code().unwrap_or(-1); return Err(format!( "synops-transcribe feilet (exit {code}): {stderr}" )); } // Parse stdout som JSON — det er resultatet let stdout = String::from_utf8_lossy(&output.stdout); let result: serde_json::Value = serde_json::from_str(&stdout) .map_err(|e| format!("Kunne ikke parse synops-transcribe output: {e}"))?; tracing::info!( media_node_id = %media_node_id, segments = result["segment_count"].as_u64().unwrap_or(0), "synops-transcribe fullført" ); Ok(result) }