#![allow(dead_code)] mod config; mod scene; mod generators; mod error; mod renderer; mod progress_tracker; mod ml_backend; mod orchestrator; mod optimization; mod video_assembler; mod audio_analyzer; mod qdrant_client; mod prompt_remixer; use anyhow::Result; use indicatif::ProgressBar; use std::path::{Path, PathBuf}; use std::sync::Arc; use walkdir::WalkDir; use generators::api_router::ApiRouterGenerator; use generators::wan2::Wan2Generator; use generators::ssd::SsdGenerator; use video_assembler::VideoAssembler; use progress_tracker::ProgressTracker; #[tokio::main] async fn main() -> Result<()> { #[cfg(windows)] { // Enforce UTF-8 for Windows Console use windows::Win32::System::Console::{GetConsoleOutputCP, SetConsoleOutputCP, CP_UTF8}; unsafe { let _ = SetConsoleOutputCP(CP_UTF8); } // Mandatory UAC Elevation Check (Directive 00-system-directives.md) use windows::Win32::Security::IsUserAnAdmin; if unsafe { !IsUserAnAdmin().as_bool() } { eprintln!(" [FATAL] Error: Administrative privileges required."); eprintln!(" [INFO] Please restart this application as Administrator."); std::process::exit(1); } } dotenv::dotenv().ok(); let config = Arc::new(config::Config::from_env()?); // Ouverture automatique du navigateur vers le Dashboard au lancement #[cfg(target_os = "windows")] let _ = std::process::Command::new("cmd").args(["/C", "start", "http://localhost:8000"]).spawn(); #[cfg(target_os = "linux")] let _ = std::process::Command::new("xdg-open").arg("http://localhost:8000").spawn(); #[cfg(target_os = "macos")] let _ = std::process::Command::new("open").arg("http://localhost:8000").spawn(); println!("\n[ASSETS] DARKMEDIA-X ASSET GENERATOR (RUST)"); println!(" [INFO] Mode: {:?}", config.image_gen_mode); println!(" ----------------------------------------"); // Active Checks let timeout = std::time::Duration::from_millis(2000); let ollama_addr = "127.0.0.1:11434".parse() .map_err(|e| anyhow::anyhow!("Failed to parse Ollama address: {}", e))?; let ollama_active = std::net::TcpStream::connect_timeout(&ollama_addr, timeout).is_ok(); let sd_addr = "127.0.0.1:7860".parse() .map_err(|e| anyhow::anyhow!("Failed to parse SD address: {}", e))?; let sd_active = std::net::TcpStream::connect_timeout(&sd_addr, timeout).is_ok(); let blender_path = std::env::var("BLENDER_PATH").unwrap_or_default(); let blender_active = !blender_path.is_empty() && std::path::Path::new(&blender_path).exists(); println!(" [ENGINE] Ollama: {}", if ollama_active { "✅ ACTIVE" } else { "❌ INACTIVE" }); println!(" [ENGINE] Stability Matrix: {}", if sd_active { "✅ ACTIVE" } else { "❌ INACTIVE" }); println!(" [ENGINE] Wan2.2: {}", if config.wan2_path.is_some() { "✅ ACTIVE" } else { "❌ INACTIVE" }); println!(" [ENGINE] Open-Sora: {}", if config.opensora_path.is_some() { "✅ ACTIVE" } else { "❌ INACTIVE" }); println!(" [ENGINE] SSD-1B: ✅ READY (Internal)"); println!(" [ENGINE] Blender 3D: {}", if blender_active { "✅ ACTIVE" } else { "❌ INACTIVE" }); println!(" ----------------------------------------"); println!(" [STUDIO] Voice: {} | {}", config.voice_pitch, config.voice_rate); println!(" [STUDIO] Audio Pitch: {} | Echo: {}ms", config.audio_demonic_pitch, config.audio_echo_delay); println!(" [STUDIO] Vol (V/M): {} / {}", config.audio_voice_vol, config.audio_music_vol); println!(" ----------------------------------------\n"); let stories = discover_stories(&config.stories_root)?; let selected_story = std::env::var("SELECTED_STORY").ok(); let rushs_mode = std::env::var("RUSHS_MODE").map(|v| v.to_lowercase() == "true").unwrap_or(false); if rushs_mode { println!(" ⚡ RUSHS_MODE: ACTIVE (Multi-variation generation, skipping video assembly)"); } if let Some(ref selected) = selected_story { if selected == "NEW_STORY" { println!(" 🪄 NEW_STORY detected. Delegating to Python backend for AI Generation..."); // Initialiser le tracker pour éviter le gap de progression let progress_file = Path::new("../current_task.json"); let progress = ProgressTracker::new(progress_file, "NEW_STORY".to_string(), "Pending".to_string()); let _ = progress.update_task_status("Initialisation AI...", 2).await; let mut cmd = std::process::Command::new(&config.python_exe); cmd.arg("asset_generator.py").arg("--story").arg("NEW_STORY"); // Transmettre les variables d'environnement nécessaires if let Some(music) = &config.selected_music { cmd.env("SELECTED_MUSIC", music); } if let Some(music_dir) = &config.music_dir { cmd.env("MUSIC_DIR", music_dir); } if rushs_mode { cmd.env("RUSHS_MODE", "true"); } let status = cmd.status()?; if status.success() { println!(" ✅ AI Generation finished. Scanning for the new story..."); // Rechercher le dossier le plus récent dans stories/Generated let gen_dir = config.stories_root.join("Generated"); if let Ok(entries) = std::fs::read_dir(&gen_dir) { let mut folders: Vec<_> = entries.flatten() .filter(|e| e.path().is_dir()) .collect(); folders.sort_by_key(|e| e.metadata().and_then(|m| m.modified()).ok()); if let Some(latest) = folders.last() { let story_file = latest.path().join("story.md"); if story_file.exists() { println!(" 🚀 Automatically processing new story: {:?}", latest.file_name()); process_story(&story_file, config.clone(), rushs_mode).await?; println!("\n ✅ PROCESS COMPLETE: Assets generated for {:?}", latest.file_name()); return Ok(()); } } } println!(" ⚠️ AI Story generated but couldn't find it for auto-processing."); } else { eprintln!(" ❌ Python backend failed to generate new story."); } return Ok(()); } } let targets: Vec = if let Some(ref selected) = selected_story { if selected.is_empty() { stories } else { stories.into_iter().filter(|p| { let story_id = p.parent().and_then(|p| p.file_name()).map(|n| n.to_string_lossy().to_lowercase()).unwrap_or_default(); let target = selected.to_lowercase(); story_id.contains(&target) || target.contains(&target) }).collect() } } else { stories }; println!(" 📂 {} histoire(s) à traiter\n", targets.len()); if targets.is_empty() { return Ok(()); } let pb = ProgressBar::new(targets.len() as u64); for story_path in targets { pb.inc(1); if let Err(e) = process_story(&story_path, config.clone(), rushs_mode).await { eprintln!(" ❌ Error: {}", e); } } pb.finish_with_message("Done"); Ok(()) } fn discover_stories(stories_root: &Path) -> Result> { let mut stories = Vec::new(); for entry in WalkDir::new(stories_root).into_iter().flatten() { let path = entry.path(); if path.is_file() && path.extension().is_some_and(|ext| ext == "md") { if let Some(name) = path.file_name() { let name_str = name.to_string_lossy(); if !name_str.starts_with("README") && name_str != "music_prompt.md" { stories.push(path.to_path_buf()); } } } } stories.sort_by_key(|p| { let is_rimouski = p.to_string_lossy().to_lowercase().contains("rimouski"); (if is_rimouski { 0 } else { 1 }, p.clone()) }); Ok(stories) } #[allow(dead_code)] async fn process_story(story_path: &Path, config: Arc, rushs_mode: bool) -> Result<()> { let story_id = story_path.parent() .and_then(|p| p.file_name()) .map(|n| n.to_string_lossy().to_string()) .ok_or_else(|| anyhow::anyhow!("Failed to extract story ID from path: {:?}", story_path))?; println!(" 🎬 Processing: '{}' ", story_id); let progress_file = Path::new("../current_task.json"); let progress = ProgressTracker::new(progress_file, story_id.clone(), story_path.to_string_lossy().to_string()); // Update Indicators for Engines let img_mode_str = format!("{:?}", config.image_gen_mode).to_lowercase(); let voc_mode_str = format!("{:?}", config.voice_gen_mode).to_lowercase(); progress.set_img_mode(&img_mode_str).await?; progress.set_voc_mode(&voc_mode_str).await?; progress.set_vfx_mode(config.vfx_profile.as_str()).await?; // Set actual core version let core_v = match config.image_gen_mode { config::ImageGenMode::RustNative => "Rust (Candle)", config::ImageGenMode::TensorRT => "Rust (TensorRT)", _ => "Rust (Standard)", }; { let mut lock = progress.current.lock().await; lock.core_version = core_v.to_string(); } progress.update_task_status("Initialisation...", 5).await?; let full_music_path = if let Some(dir) = &config.music_dir { if let Some(name) = &config.selected_music { let p = dir.join(name); if p.exists() { Some(p) } else { None } } else if dir.exists() { // Auto-selection d'une musique au hasard si aucune n'est choisie let mut music_files = Vec::new(); if let Ok(entries) = std::fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() && path.extension().is_some_and(|ext| ext == "mp3" || ext == "wav") { music_files.push(path); } } } if !music_files.is_empty() { use rand::seq::SliceRandom; let mut rng = rand::thread_rng(); music_files.choose(&mut rng).cloned() } else { None } } else { None } } else { None }; let music_ref = full_music_path.as_deref(); if let Some(ref m) = full_music_path { println!(" 🎵 Auto-selected music: {:?}", m.file_name().unwrap_or_default()); } let scenes = scene::extract_scene_data(story_path)?; if scenes.is_empty() { return Ok(()); } let assets_dir = story_path.parent() .ok_or_else(|| anyhow::anyhow!("Story path has no parent: {:?}", story_path))? .join("assets"); create_asset_dirs(&assets_dir)?; let img_dir = assets_dir.join("images"); let scenes_dir = assets_dir.join("scenes"); // === PHASE 1: GENERATION DES IMAGES === match config.image_gen_mode { config::ImageGenMode::Api => { if let (Some(url), Some(secret)) = (&config.ai_router_url, &config.api_secret) { let gen = ApiRouterGenerator::new(url.clone(), secret.clone(), config.openrouter_api_key.clone()); for (i, scene) in scenes.iter().enumerate() { let progress_val = ((i as f32 / scenes.len() as f32) * 60.0) as u32; progress.update_task_status(&format!("Génération {}/{}", i+1, scenes.len()), progress_val).await?; let img_file = img_dir.join(format!("scene_{}.png", scene.id)); if !img_file.exists() { if let Ok(true) = gen.generate(&scene.prompt, &img_file, scene.id, scene.id == scenes.len()).await { let _ = apply_vfx_to_image(&img_file, config.vfx_profile); } } } } }, config::ImageGenMode::Wan2 => { if let (Some(p), Some(c)) = (&config.wan2_path, &config.wan2_ckpt_dir) { let gen = Wan2Generator::new(p.clone(), c.clone(), "t2v".to_string(), config.python_exe.clone(), config.ffmpeg_path.clone()); for (i, scene) in scenes.iter().enumerate() { let progress_val = ((i as f32 / scenes.len() as f32) * 60.0) as u32; progress.update_task_status(&format!("Génération Wan2 {}/{}", i+1, scenes.len()), progress_val).await?; let img_file = img_dir.join(format!("scene_{}.png", scene.id)); if !img_file.exists() { let full_prompt = format!("{}, {}", scene.prompt, std::env::var("GLOBAL_STYLE").unwrap_or_default()); if let Ok(true) = gen.generate(&full_prompt, &img_file).await { let _ = apply_vfx_to_image(&img_file, config.vfx_profile); } } } } }, config::ImageGenMode::RustNative => { use generators::candle_native::CandleNativeGenerator; // On cherche les poids dans Repository/SSD-1B ou à défaut dans le dossier engine let model_dir = std::env::var("SSD_MODEL_PATH") .unwrap_or_else(|_| "/home/jfvallee/Repository/SSD-1B".to_string()); if let Ok(gen) = CandleNativeGenerator::new(&model_dir) { for (i, scene) in scenes.iter().enumerate() { let progress_val = ((i as f32 / scenes.len() as f32) * 60.0) as u32; progress.update_task_status(&format!("Génération Candle {}/{}", i+1, scenes.len()), progress_val).await?; let img_file = img_dir.join(format!("scene_{}.png", scene.id)); if !img_file.exists() { // On utilise le seed de l'histoire pour la cohérence let seed = std::env::var("STORY_SEED") .and_then(|s| s.parse::().map_err(|_| std::env::VarError::NotPresent)) .ok(); if let Ok(true) = generators::ImageGenerator::generate(&gen, &scene.prompt, &img_file, scene.id, scene.id == scenes.len()).await { let _ = apply_vfx_to_image(&img_file, config.vfx_profile); } } } } else { eprintln!(" ❌ Failed to initialize Candle Native Engine."); } }, _ => {} } if rushs_mode { println!(" 🎬 RUSHS_MODE: Skipping video assembly."); progress.update_task_status("Rushs terminés", 100).await?; return Ok(()); } // === PHASE 2: ASSEMBLAGE VIDEO (SYSTÉMATIQUE) === progress.update_task_status("Assembling video...", 85).await?; let video_assembler = VideoAssembler::new(); let story_dir = story_path.parent().unwrap_or(story_path); match video_assembler.assemble_video(story_dir, &scenes, &img_dir, "final_video.mp4", music_ref, &progress, config.clone()).await { Ok(_) => println!(" ✅ Final video created!"), Err(e) => eprintln!(" ❌ Assembly error: {}", e), } Ok(()) } fn apply_vfx_to_image(img_path: &Path, profile: renderer::VfxProfile) -> Result<()> { if matches!(profile, renderer::VfxProfile::None) { return Ok(()); } let img = image::open(img_path)?; let processed = renderer::vfx::apply_vfx(img, profile)?; processed.save(img_path)?; Ok(()) } fn create_asset_dirs(assets_dir: &Path) -> Result<()> { let dirs = vec!["images", "depths", "sounds", "scenes", "on-screen text"]; for dir in dirs { std::fs::create_dir_all(assets_dir.join(dir))?; } Ok(()) }