synops/maskinrommet/src/transcribe.rs
vegard bd12bed77e Implementer synops-transcribe CLI-verktøy (oppgave 21.1)
Bryter ut Whisper-transkribering fra maskinrommet til selvstendig
CLI-verktøy i tools/synops-transcribe/, i tråd med unix-filosofien.

Verktøyet:
- Leser lydfil fra CAS, sender til faster-whisper API (SRT-format)
- Parser SRT til segmenter, skriver JSON til stdout
- Med --write: skriver segmenter til PG, oppdaterer node metadata,
  logger ressursforbruk
- Støtter --cas-hash, --model, --initial-prompt, --language, --mime,
  --node-id, --requested-by

Maskinrommet sin transcribe.rs er nå en tynn dispatcher som spawner
synops-transcribe som subprosess med riktige env-variabler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:01:06 +00:00

132 lines
4.2 KiB
Rust

// 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<serde_json::Value, String> {
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)
}