cybermedia's picture
Upload folder using huggingface_hub
343eed9 verified
#![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<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)
}
#[allow(dead_code)]
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(())
}