use clap::Parser; use std::{ fs, path::{Path, PathBuf}, time::Instant, }; use tokio::time::{sleep, Duration}; use tracing::{info, error, warn}; #[derive(Parser)] #[command(name = "waveform_generator")] #[command(about = "Génère les waveforms pour tous les fichiers audio")] struct Args { /// Dossier contenant les fichiers audio #[arg(short, long, default_value = "audio")] input_dir: String, /// Dossier de sortie pour les waveforms #[arg(short, long, default_value = "waveforms")] output_dir: String, /// Résolution de la waveform (nombre de points) #[arg(short, long, default_value = "1000")] resolution: usize, /// Extensions de fichier à traiter #[arg(short, long, default_values_t = vec!["mp3".to_string(), "wav".to_string(), "flac".to_string()])] extensions: Vec, /// Forcer la régénération même si le fichier existe #[arg(short, long)] force: bool, /// Traitement en parallèle (nombre de workers) #[arg(short, long, default_value = "4")] workers: usize, } #[tokio::main] async fn main() -> Result<(), Box> { // Configuration du logging tracing_subscriber::fmt() .with_env_filter("waveform_generator=info") .init(); let args = Args::parse(); info!("🎵 Générateur de waveforms démarré"); info!("📁 Dossier d'entrée: {}", args.input_dir); info!("📁 Dossier de sortie: {}", args.output_dir); info!("📊 Résolution: {} points", args.resolution); info!("⚡ Workers: {}", args.workers); // Créer le dossier de sortie fs::create_dir_all(&args.output_dir)?; // Trouver tous les fichiers audio let audio_files = find_audio_files(&args.input_dir, &args.extensions)?; info!("🔍 {} fichiers audio trouvés", audio_files.len()); if audio_files.is_empty() { warn!("Aucun fichier audio trouvé dans {}", args.input_dir); return Ok(()); } let start_time = Instant::now(); let mut processed = 0; let mut errors = 0; let mut skipped = 0; // Traitement en parallèle avec limite de workers let semaphore = tokio::sync::Semaphore::new(args.workers); let mut handles = Vec::new(); for audio_file in audio_files { let permit = semaphore.clone().acquire_owned().await?; let output_dir = args.output_dir.clone(); let resolution = args.resolution; let force = args.force; let handle = tokio::spawn(async move { let _permit = permit; process_audio_file(&audio_file, &output_dir, resolution, force).await }); handles.push(handle); } // Attendre tous les traitements for handle in handles { match handle.await? { ProcessResult::Processed => processed += 1, ProcessResult::Skipped => skipped += 1, ProcessResult::Error => errors += 1, } } let elapsed = start_time.elapsed(); info!("✅ Traitement terminé en {:.2}s", elapsed.as_secs_f64()); info!("📊 Résultats:"); info!(" - Traités: {}", processed); info!(" - Ignorés: {}", skipped); info!(" - Erreurs: {}", errors); if errors > 0 { warn!("⚠️ {} fichiers n'ont pas pu être traités", errors); } Ok(()) } #[derive(Debug)] enum ProcessResult { Processed, Skipped, Error, } async fn process_audio_file( audio_path: &Path, output_dir: &str, resolution: usize, force: bool, ) -> ProcessResult { let file_stem = audio_path.file_stem().unwrap().to_str().unwrap(); let waveform_path = PathBuf::from(output_dir).join(format!("{}.json", file_stem)); // Vérifier si le fichier existe déjà if !force && waveform_path.exists() { // Vérifier si le fichier waveform est plus récent que l'audio if let (Ok(audio_meta), Ok(waveform_meta)) = (audio_path.metadata(), waveform_path.metadata()) { if let (Ok(audio_modified), Ok(waveform_modified)) = (audio_meta.modified(), waveform_meta.modified()) { if waveform_modified >= audio_modified { return ProcessResult::Skipped; } } } } info!("🔄 Traitement: {}", audio_path.display()); // Simuler le traitement avec l'audio processor // En production, on utiliserait le module audio_processing match generate_waveform_data(audio_path, resolution).await { Ok(waveform_data) => { // Sauvegarder la waveform en JSON if let Err(e) = save_waveform_json(&waveform_path, &waveform_data).await { error!("Erreur sauvegarde waveform pour {}: {}", audio_path.display(), e); return ProcessResult::Error; } info!("✅ Waveform générée: {}", waveform_path.display()); ProcessResult::Processed } Err(e) => { error!("❌ Erreur génération waveform pour {}: {}", audio_path.display(), e); ProcessResult::Error } } } async fn generate_waveform_data( _audio_path: &Path, resolution: usize, ) -> Result> { // Simulation du traitement audio // En production, on utiliserait symphonia pour décoder l'audio sleep(Duration::from_millis(100)).await; // Simule le temps de traitement // Générer des données de test let peaks: Vec = (0..resolution) .map(|i| { let t = i as f32 / resolution as f32; (t * std::f32::consts::PI * 4.0).sin().abs() * (1.0 - t * 0.5) }) .collect(); let rms: Vec = peaks.iter().map(|&p| p * 0.7).collect(); Ok(WaveformData { peaks, rms, sample_rate: 100, // 100 points par seconde duration_ms: (resolution * 10) as u32, // 10ms par point generated_at: std::time::SystemTime::now(), }) } async fn save_waveform_json( path: &Path, data: &WaveformData, ) -> Result<(), Box> { let json = serde_json::to_string_pretty(data)?; tokio::fs::write(path, json).await?; Ok(()) } fn find_audio_files( dir: &str, extensions: &[String], ) -> Result, Box> { let mut files = Vec::new(); let dir_path = Path::new(dir); if !dir_path.exists() { return Err(format!("Le dossier {} n'existe pas", dir).into()); } for entry in fs::read_dir(dir_path)? { let entry = entry?; let path = entry.path(); if path.is_file() { if let Some(ext) = path.extension() { if let Some(ext_str) = ext.to_str() { if extensions.iter().any(|e| e.eq_ignore_ascii_case(ext_str)) { files.push(path); } } } } } files.sort(); Ok(files) } // Structures de données pour la waveform (copiées du module audio_processing) #[derive(serde::Serialize, serde::Deserialize)] struct WaveformData { peaks: Vec, rms: Vec, sample_rate: u32, duration_ms: u32, generated_at: std::time::SystemTime, }