// describe_image — AI-beskrivelse av bilder via synops-ai CLI. // // Jobbtype: "describe_image" // Payload: { "media_node_id", "cas_hash", "mime" } // // Flyten: // 1. Sender bildefilen (CAS-sti) til synops-ai prompt --image // 2. synops-ai sender multimodal melding til LLM via LiteLLM // 3. Oppdaterer media-nodens metadata med description-feltet // // Ref: docs/retninger/unix_filosofi.md, oppgave 29.1 use sqlx::PgPool; use uuid::Uuid; use crate::cas::CasStore; use crate::jobs::JobRow; /// Håndterer describe_image-jobb. /// /// Sender bildet til synops-ai for beskrivelse via vision-modell, /// og lagrer resultatet i media-nodens metadata. pub async fn handle_describe_image( job: &JobRow, db: &PgPool, cas: &CasStore, ) -> Result { let media_node_id: Uuid = job.payload["media_node_id"] .as_str() .and_then(|s| s.parse().ok()) .ok_or("Mangler gyldig 'media_node_id' i payload")?; 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("image/png"); // Verifiser at bildet finnes i CAS let cas_path = cas.path_for(cas_hash); if !cas_path.exists() { return Err(format!("Bilde ikke funnet i CAS: {cas_hash}")); } let bin = std::env::var("SYNOPS_AI_BIN") .unwrap_or_else(|_| "synops-ai".to_string()); tracing::info!( media_node_id = %media_node_id, cas_hash, "Sender bilde til AI for beskrivelse" ); let output = tokio::process::Command::new(&bin) .arg("prompt") .arg("--prompt") .arg("Beskriv dette bildet kort og konsist på norsk. Fokuser på innholdet, ikke formatet. Maks 2-3 setninger.") .arg("--image") .arg(cas_path.to_str().unwrap_or("")) .arg("--image-mime") .arg(mime) .arg("--job-type") .arg("describe_image") .arg("--temperature") .arg("0.3") .output() .await .map_err(|e| format!("Kunne ikke starte {bin}: {e}"))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("synops-ai feilet: {stderr}")); } let description = String::from_utf8_lossy(&output.stdout).trim().to_string(); if description.is_empty() { return Err("synops-ai returnerte tom beskrivelse".into()); } tracing::info!( media_node_id = %media_node_id, description_len = description.len(), "Bildebeskrivelse mottatt fra AI" ); // Oppdater media-nodens metadata med description sqlx::query( r#"UPDATE nodes SET metadata = metadata || jsonb_build_object('description', $2::text), updated_at = now() WHERE id = $1"#, ) .bind(media_node_id) .bind(&description) .execute(db) .await .map_err(|e| format!("Kunne ikke oppdatere media-node med beskrivelse: {e}"))?; // PG NOTIFY-trigger på nodes-tabellen sender sanntidsoppdatering automatisk. Ok(serde_json::json!({ "media_node_id": media_node_id.to_string(), "description": description, })) }