Spaces:
Sleeping
Sleeping
| 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; | |
| async fn main() -> Result<()> { | |
| { | |
| // 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 | |
| let _ = std::process::Command::new("cmd").args(["/C", "start", "http://localhost:8000"]).spawn(); | |
| let _ = std::process::Command::new("xdg-open").arg("http://localhost:8000").spawn(); | |
| 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<PathBuf> = 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<Vec<PathBuf>> { | |
| 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) | |
| } | |
| async fn process_story(story_path: &Path, config: Arc<config::Config>, 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::<u64>().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(()) | |
| } | |