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>
132 lines
4.2 KiB
Rust
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)
|
|
}
|