use crate::error::{GeneratorError, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Debug, Clone)] pub struct ApiRouterGenerator { pub router_url: String, pub api_secret: String, pub openrouter_api_key: Option, } #[derive(Debug, Serialize)] struct ImageRequest { prompt: String, model: String, } #[derive(Debug, Deserialize)] struct ImageResponse { statut: String, donnees: Option, } #[derive(Debug, Deserialize)] struct ImageData { #[serde(rename = "imageUrl")] image_url: Option, } impl ApiRouterGenerator { pub fn new(router_url: String, api_secret: String, openrouter_api_key: Option) -> Self { Self { router_url, api_secret, openrouter_api_key, } } async fn download_image(&self, url: &str, output_path: &Path) -> Result { let response = reqwest::get(url) .await .map_err(GeneratorError::RequestError)?; let bytes = response.bytes().await.map_err(GeneratorError::RequestError)?; std::fs::write(output_path, bytes)?; Ok(true) } pub async fn generate( &self, prompt: &str, output_path: &Path, scene_index: usize, is_last: bool, ) -> Result { let horror_hook = if scene_index == 1 { "SHOCKING HORROR: terrified face or lunging monster, " } else { "" }; let branding = if is_last { " with 'Darkmedia-X' branding text visible" } else { "" }; let styled_prompt = format!( "Dark Anime Horror, {}{}{branding}. 9:16 ratio, ink-wash, high resolution.", horror_hook, prompt ); let payload = ImageRequest { prompt: styled_prompt, model: "openai/dall-e-3".to_string(), }; let client = reqwest::Client::new(); let url = format!("{}/api/image", self.router_url.trim_end_matches('/')); let response = client .post(&url) .header("Authorization", format!("Bearer {}", self.api_secret)) .header("Content-Type", "application/json") .json(&payload) .timeout(std::time::Duration::from_secs(45)) .send() .await; match response { Ok(resp) => { let status = resp.status(); if status == 404 { eprintln!(" [WARN] Router 404 — fallback direct"); return self.fallback_direct(prompt, output_path, is_last).await; } if !status.is_success() { eprintln!(" [WARN] Router returned status {} — fallback direct", status); return self.fallback_direct(prompt, output_path, is_last).await; } if let Ok(data) = resp.json::().await { if data.statut == "actif" { if let Some(image_data) = data.donnees { if let Some(img_url) = image_data.image_url { return self.download_image(&img_url, output_path).await; } } } } Ok(false) } Err(e) => { eprintln!(" [WARN] Router inaccessible — fallback direct: {}", e); self.fallback_direct(prompt, output_path, is_last).await } } } async fn fallback_direct(&self, prompt: &str, output_path: &Path, is_last: bool) -> Result { let api_key = self.openrouter_api_key.as_ref() .ok_or_else(|| GeneratorError::ApiError("OPENROUTER_API_KEY not configured".to_string()))?; let branding = if is_last { " with 'Darkmedia-X' branding text visible" } else { "" }; let styled_prompt = format!( "Dark Anime Cinematic Horror Style: {}{branding}. High resolution, 9:16 vertical ratio, ink-wash textures, moody lighting.", prompt ); let client = reqwest::Client::new(); #[derive(Serialize)] struct OpenRouterRequest { model: String, prompt: String, n: usize, size: String, } let payload = OpenRouterRequest { model: "openai/dall-e-3".to_string(), prompt: styled_prompt, n: 1, size: "1024x1792".to_string(), }; let response = client .post("https://openrouter.ai/api/v1/images/generations") .header("Authorization", format!("Bearer {}", api_key)) .json(&payload) .timeout(std::time::Duration::from_secs(120)) .send() .await?; if !response.status().is_success() { eprintln!(" 🔴 OpenRouter error: {} status", response.status()); if let Ok(text) = response.text().await { eprintln!(" Response: {}", &text[..std::cmp::min(200, text.len())]); } return Err(GeneratorError::ApiError("OpenRouter API error".to_string())); } #[derive(Deserialize)] struct OpenRouterResponse { data: Vec, } #[derive(Deserialize)] struct OpenRouterImage { url: String, } match response.json::().await { Ok(data) => { if let Some(img) = data.data.first() { return self.download_image(&img.url, output_path).await; } Ok(false) } Err(e) => { eprintln!(" 🔴 OpenRouter JSON error: {}", e); Err(e.into()) } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_api_router_new() { let generator = ApiRouterGenerator::new( "http://localhost:8080".to_string(), "secret".to_string(), Some("openrouter_key".to_string()), ); assert_eq!(generator.router_url, "http://localhost:8080"); assert_eq!(generator.api_secret, "secret"); assert_eq!(generator.openrouter_api_key, Some("openrouter_key".to_string())); } }