""" Baby Cry AI - Data Cleaning Pipeline Removes corrupted, duplicate, and low-quality audio files safely """ import os import sys import shutil import json from pathlib import Path from datetime import datetime import argparse # Add parent directory to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from data_quality_analyzer import DataQualityAnalyzer class DataCleaner: """Clean dataset by removing problematic files""" def __init__(self, data_dir="../data", backup_dir=None, dry_run=True): """ Initialize cleaner Args: data_dir: Path to data directory backup_dir: Path to backup directory (default: data_dir/../data_backup_TIMESTAMP) dry_run: If True, only show what would be removed without actually removing """ self.data_dir = Path(data_dir) self.dry_run = dry_run if backup_dir: self.backup_dir = Path(backup_dir) else: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") self.backup_dir = self.data_dir.parent / f"data_backup_{timestamp}" self.removed_files = [] self.backed_up_files = [] def create_backup(self): """Create backup of data directory before cleaning""" if not self.dry_run: print(f"๐Ÿ“ฆ Creating backup to: {self.backup_dir}") self.backup_dir.mkdir(parents=True, exist_ok=True) # Copy entire data directory structure for category_dir in self.data_dir.iterdir(): if category_dir.is_dir(): backup_category = self.backup_dir / category_dir.name backup_category.mkdir(exist_ok=True) print(f" Backing up {category_dir.name}...") else: print(f"๐Ÿ“ฆ Would create backup to: {self.backup_dir}") def backup_file(self, file_path): """Backup a single file before removal""" if self.dry_run: return relative_path = file_path.relative_to(self.data_dir) backup_path = self.backup_dir / relative_path backup_path.parent.mkdir(parents=True, exist_ok=True) try: shutil.copy2(file_path, backup_path) self.backed_up_files.append(str(backup_path)) except Exception as e: print(f" โš ๏ธ Warning: Could not backup {file_path}: {e}") def remove_file(self, file_path, reason=""): """Remove a file (with backup)""" file_path = Path(file_path) if not file_path.exists(): return False # Backup before removal self.backup_file(file_path) if self.dry_run: print(f" [DRY RUN] Would remove: {file_path.name} ({reason})") self.removed_files.append({ 'file': str(file_path), 'reason': reason, 'removed': False }) return True try: file_path.unlink() self.removed_files.append({ 'file': str(file_path), 'reason': reason, 'removed': True }) return True except Exception as e: print(f" โŒ Error removing {file_path}: {e}") return False def clean_corrupted_files(self, analyzer): """Remove corrupted files""" print("\n๐Ÿ—‘๏ธ Removing corrupted files...") count = 0 for file_info in analyzer.results['corrupted_files']: file_path = Path(file_info['file']) if self.remove_file(file_path, f"corrupted: {file_info.get('issue', 'unknown')}"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} corrupted files") return count def clean_duplicate_files(self, analyzer): """Remove duplicate files (keep first occurrence)""" print("\n๐Ÿ—‘๏ธ Removing duplicate files...") count = 0 # Track which files we've seen (keep first) seen_hashes = {} for dup_info in analyzer.results['duplicate_files']: file_path = Path(dup_info['file']) original = dup_info.get('duplicate_of') # Remove the duplicate (not the original) if original and str(file_path) != original: if self.remove_file(file_path, f"duplicate of {Path(original).name}"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} duplicate files") return count def clean_short_files(self, analyzer, min_duration=3.0): """Remove files that are too short""" print(f"\n๐Ÿ—‘๏ธ Removing files shorter than {min_duration}s...") count = 0 for file_info in analyzer.results['short_files']: file_path = Path(file_info['file']) duration = file_info.get('duration', 0) if self.remove_file(file_path, f"too short: {duration:.1f}s"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} short files") return count def clean_long_files(self, analyzer, max_duration=30.0): """Remove files that are too long""" print(f"\n๐Ÿ—‘๏ธ Removing files longer than {max_duration}s...") count = 0 for file_info in analyzer.results['long_files']: file_path = Path(file_info['file']) duration = file_info.get('duration', 0) if self.remove_file(file_path, f"too long: {duration:.1f}s"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} long files") return count def clean_quiet_files(self, analyzer, min_level_db=-40): """Remove files that are too quiet""" print(f"\n๐Ÿ—‘๏ธ Removing files quieter than {min_level_db} dB...") count = 0 for file_info in analyzer.results['quiet_files']: file_path = Path(file_info['file']) level = file_info.get('audio_level_db', -100) if self.remove_file(file_path, f"too quiet: {level:.1f} dB"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} quiet files") return count def clean_silent_files(self, analyzer, max_silence_ratio=0.80): """Remove files that are mostly silence""" print(f"\n๐Ÿ—‘๏ธ Removing files with >{max_silence_ratio*100:.0f}% silence...") count = 0 for file_info in analyzer.results['silent_files']: file_path = Path(file_info['file']) silence = file_info.get('silence_ratio', 1.0) if self.remove_file(file_path, f"too silent: {silence*100:.0f}%"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} silent files") return count def clean_low_snr_files(self, analyzer, min_snr_db=10.0): """Remove files with low signal-to-noise ratio""" print(f"\n๐Ÿ—‘๏ธ Removing files with SNR < {min_snr_db} dB...") count = 0 for file_info in analyzer.results['low_snr_files']: file_path = Path(file_info['file']) snr = file_info.get('snr_db', -100) if self.remove_file(file_path, f"low SNR: {snr:.1f} dB"): count += 1 print(f" {'Would remove' if self.dry_run else 'Removed'}: {count} low SNR files") return count def clean_all(self, analyzer, remove_corrupted=True, remove_duplicates=True, remove_short=True, remove_long=True, remove_quiet=True, remove_silent=True, remove_low_snr=False): # Low SNR is optional, might be too aggressive """Run complete cleaning pipeline""" print("๐Ÿงน Starting Data Cleaning Pipeline") print("=" * 60) if self.dry_run: print("โš ๏ธ DRY RUN MODE - No files will be removed") else: print("โš ๏ธ LIVE MODE - Files will be permanently removed") # Create backup self.create_backup() total_removed = 0 # Remove files based on options if remove_corrupted: total_removed += self.clean_corrupted_files(analyzer) if remove_duplicates: total_removed += self.clean_duplicate_files(analyzer) if remove_short: total_removed += self.clean_short_files(analyzer) if remove_long: total_removed += self.clean_long_files(analyzer) if remove_quiet: total_removed += self.clean_quiet_files(analyzer) if remove_silent: total_removed += self.clean_silent_files(analyzer) if remove_low_snr: total_removed += self.clean_low_snr_files(analyzer) # Save cleaning report self.save_report() print("\n" + "=" * 60) print(f"โœ… Cleaning complete!") print(f" {'Would remove' if self.dry_run else 'Removed'}: {total_removed} files") if not self.dry_run: print(f" Backed up to: {self.backup_dir}") print("=" * 60) return total_removed def save_report(self): """Save cleaning report""" report_path = self.data_dir.parent / 'data_cleaning_report.json' report = { 'timestamp': datetime.now().isoformat(), 'dry_run': self.dry_run, 'backup_dir': str(self.backup_dir), 'total_removed': len(self.removed_files), 'removed_files': self.removed_files[:500], # Limit to first 500 'backed_up_files': len(self.backed_up_files) } with open(report_path, 'w') as f: json.dump(report, f, indent=2) print(f"\n๐Ÿ’พ Cleaning report saved to: {report_path}") if __name__ == "__main__": parser = argparse.ArgumentParser(description='Clean dataset by removing problematic files') parser.add_argument('--data-dir', type=str, default='../data', help='Path to data directory') parser.add_argument('--backup-dir', type=str, default=None, help='Path to backup directory') parser.add_argument('--execute', action='store_true', help='Actually remove files (default is dry-run)') parser.add_argument('--min-snr', type=float, default=10.0, help='Minimum acceptable SNR in dB') parser.add_argument('--remove-low-snr', action='store_true', help='Also remove files with low SNR') args = parser.parse_args() # First, analyze data quality print("Step 1: Analyzing data quality...") analyzer = DataQualityAnalyzer(data_dir=args.data_dir, min_snr_db=args.min_snr) analyzer.analyze_dataset() # Then, clean based on analysis print("\nStep 2: Cleaning data...") cleaner = DataCleaner( data_dir=args.data_dir, backup_dir=args.backup_dir, dry_run=not args.execute ) cleaner.clean_all( analyzer, remove_low_snr=args.remove_low_snr ) if cleaner.dry_run: print("\n๐Ÿ’ก To actually remove files, run with --execute flag")