veza/veza-stream-server/tools/waveform_generator.rs
2025-12-03 20:36:56 +01:00

237 lines
No EOL
7.1 KiB
Rust

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<String>,
/// 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<dyn std::error::Error>> {
// 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<WaveformData, Box<dyn std::error::Error + Send + Sync>> {
// 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<f32> = (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<f32> = 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<dyn std::error::Error + Send + Sync>> {
let json = serde_json::to_string_pretty(data)?;
tokio::fs::write(path, json).await?;
Ok(())
}
fn find_audio_files(
dir: &str,
extensions: &[String],
) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
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<f32>,
rms: Vec<f32>,
sample_rate: u32,
duration_ms: u32,
generated_at: std::time::SystemTime,
}