synops/maskinrommet/src/tts.rs
vegard 6bb1665b30 Validering 23.1: fase 1–2 (infra + maskinrommet) verifisert
Systematisk gjennomgang av PG-skjema, auth-middleware, intensjoner,
skrivestien og WebSocket-laget. Alle kjernetabeller matcher docs.
Auth fungerer korrekt (401 for ugyldig/manglende token). Skrivestien
er konsistent: direkte PG-skriving → NOTIFY → WebSocket.

Fikser:
- Fjern død kode: pg_writes enqueue-funksjoner (aldri kalt etter STDB-migrering)
- Fjern ubrukt truncate() i tts.rs
- Legg til #[allow(dead_code)] for sqlx-structs med ubrukte felt
- Rett feilaktig doc-påstand i api_grensesnitt.md om jobbkø
- Fjern utdatert STDB-referanse i agent_api.md
- Kompilerer uten warnings

Se logs/validering-23.1.md for fullstendig rapport.
2026-03-18 13:58:50 +00:00

137 lines
4.3 KiB
Rust

// TTS-dispatcher — delegerer til synops-tts CLI.
//
// Maskinrommet beholder: voice_id-oppslag (payload > node metadata > env).
// Alt annet (ElevenLabs-kall, CAS-lagring, PG-skriving) gjøres av synops-tts.
// PG NOTIFY-triggere sender sanntidsoppdateringer.
//
// Jobbtype: "tts_generate"
// Payload: { "text", "voice_id"?, "language"?, "source_node_id"?, "requested_by" }
//
// Ref: docs/retninger/unix_filosofi.md, docs/proposals/ghost_host_tts.md
use sqlx::PgPool;
use uuid::Uuid;
use crate::cli_dispatch;
use crate::jobs::JobRow;
/// Synops-tts binary path.
fn tts_bin() -> String {
std::env::var("SYNOPS_TTS_BIN")
.unwrap_or_else(|_| "synops-tts".to_string())
}
/// Håndterer tts_generate-jobb.
///
/// Spawner synops-tts med --write for å gjøre alt arbeidet:
/// ElevenLabs-kall, CAS-lagring, PG-skriving, ressurslogging.
/// PG NOTIFY-triggere sender sanntidsoppdateringer til klienter.
pub async fn handle_tts_job(
job: &JobRow,
db: &PgPool,
) -> Result<serde_json::Value, String> {
let text = job.payload["text"]
.as_str()
.ok_or("Mangler 'text' i payload")?;
let source_node_id: Option<Uuid> = job.payload["source_node_id"]
.as_str()
.and_then(|s| s.parse().ok());
let requested_by: Uuid = job.payload["requested_by"]
.as_str()
.and_then(|s| s.parse().ok())
.ok_or("Mangler gyldig 'requested_by' i payload")?;
let language = job.payload["language"]
.as_str()
.unwrap_or("no");
// Bestem voice_id: payload > source-node metadata > env default
let voice_id = resolve_voice_id(job, db, source_node_id).await?;
// Bygg kommando
let bin = tts_bin();
let mut cmd = tokio::process::Command::new(&bin);
cmd.arg("--text").arg(text)
.arg("--voice").arg(&voice_id)
.arg("--language").arg(language)
.arg("--requested-by").arg(requested_by.to_string())
.arg("--write");
if let Some(source_id) = source_node_id {
cmd.arg("--source-node-id").arg(source_id.to_string());
}
// Sett miljøvariabler CLI-verktøyet trenger
cli_dispatch::set_database_url(&mut cmd)?;
cli_dispatch::forward_env(&mut cmd, "CAS_ROOT");
cli_dispatch::forward_env(&mut cmd, "ELEVENLABS_API_KEY");
cli_dispatch::forward_env(&mut cmd, "ELEVENLABS_MODEL");
cli_dispatch::forward_env(&mut cmd, "ELEVENLABS_DEFAULT_VOICE");
tracing::info!(
text_len = text.len(),
voice_id = %voice_id,
bin = %bin,
"Starter synops-tts"
);
let result = cli_dispatch::run_cli_tool(&bin, &mut cmd).await?;
// PG-skriving gjøres av synops-tts med --write.
// PG NOTIFY-triggere sender sanntidsoppdateringer til WebSocket-klienter.
tracing::info!(
cas_hash = result["cas_hash"].as_str().unwrap_or("n/a"),
media_node_id = result["media_node_id"].as_str().unwrap_or("n/a"),
"synops-tts fullført"
);
Ok(result)
}
/// Bestem voice_id: payload-verdi > source-node metadata.voice_preference > env default.
async fn resolve_voice_id(
job: &JobRow,
db: &PgPool,
source_node_id: Option<Uuid>,
) -> Result<String, String> {
// 1. Eksplisitt i payload
if let Some(vid) = job.payload["voice_id"].as_str() {
if !vid.is_empty() {
return Ok(vid.to_string());
}
}
// 2. Source-nodens metadata.voice_preference.voice_id
if let Some(node_id) = source_node_id {
let meta: Option<serde_json::Value> = sqlx::query_scalar(
"SELECT metadata FROM nodes WHERE id = $1",
)
.bind(node_id)
.fetch_optional(db)
.await
.map_err(|e| format!("PG-feil ved henting av voice_preference: {e}"))?
.flatten();
if let Some(meta) = meta {
if let Some(vid) = meta
.get("voice_preference")
.and_then(|vp| vp.get("voice_id"))
.and_then(|v| v.as_str())
{
if !vid.is_empty() {
tracing::info!(node_id = %node_id, voice_id = %vid, "Bruker mottaker-preferanse");
return Ok(vid.to_string());
}
}
}
}
// 3. Miljøvariabel-default
Ok(std::env::var("ELEVENLABS_DEFAULT_VOICE")
.unwrap_or_else(|_| "21m00Tcm4TlvDq8ikWAM".to_string()))
}