""" validation_system_fix.py - Sistema de validación mejorado T105-T106: Validation System Fix Validaciones críticas: - Clips vacíos (silencio real) - Audio files corruptos/missing - Key conflict grave (disonancia) - Samples duplicados accidentalmente - Phasing entre capas de drums """ import logging from typing import Dict, Any, List, Optional, Tuple from pathlib import Path from dataclasses import dataclass logger = logging.getLogger("ValidationSystemFix") @dataclass class ValidationIssue: """Representa un problema de validación""" type: str severity: str # 'error', 'warning', 'info' track: str clip: str message: str suggestion: str auto_fixable: bool = False class ValidationSystemFixer: """T105-T106: Sistema de validación completo""" def __init__(self): self.issues: List[ValidationIssue] = [] self.validation_rules = { 'min_clip_duration': 0.5, # beats 'max_silence_threshold': -60.0, # dB 'key_conflict_threshold': 3, # semitones 'duplicate_tolerance_seconds': 0.5, } def validate_clips(self, clips_data: List[Dict]) -> List[ValidationIssue]: """ T105: Valida clips de audio. Checks: - Clip vacío (silencio) - File missing/corrupt - Duración inválida """ issues = [] for clip in clips_data: track_name = clip.get('track_name', 'Unknown') clip_name = clip.get('name', 'Unknown') file_path = clip.get('file_path', '') # 1. Check file exists if file_path and not Path(file_path).exists(): issues.append(ValidationIssue( type='missing_file', severity='error', track=track_name, clip=clip_name, message=f"Audio file not found: {file_path}", suggestion="Rescan library or replace sample", auto_fixable=False )) # 2. Check duration duration = clip.get('duration', 0) if duration < self.validation_rules['min_clip_duration']: issues.append(ValidationIssue( type='too_short', severity='warning', track=track_name, clip=clip_name, message=f"Clip too short: {duration:.2f} beats", suggestion="Extend or replace sample", auto_fixable=False )) # 3. Check loop points loop_start = clip.get('loop_start', 0) loop_end = clip.get('loop_end', duration) if loop_end <= loop_start: issues.append(ValidationIssue( type='invalid_loop', severity='error', track=track_name, clip=clip_name, message="Loop end before loop start", suggestion="Fix loop points", auto_fixable=True )) return issues def validate_key_conflicts(self, tracks_data: List[Dict], target_key: str) -> List[ValidationIssue]: """ T106: Detecta conflictos armónicos graves. Args: tracks_data: Tracks con información de key target_key: Key objetivo del track Returns: Lista de conflictos detectados """ issues = [] # Mapeo de notas a índices NOTE_MAP = { 'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3, 'E': 4, 'F': 5, 'F#': 6, 'Gb': 6, 'G': 7, 'G#': 8, 'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11 } def get_semitone_distance(key1: str, key2: str) -> int: """Calcula distancia en semitonos entre keys.""" # Extraer root note root1 = key1.replace('m', '').replace('M', '') root2 = key2.replace('m', '').replace('M', '') # Check minor flag is_minor1 = 'm' in key1.lower() and 'M' not in key1 is_minor2 = 'm' in key2.lower() and 'M' not in key2 # Diferentes modos = potencial conflicto if is_minor1 != is_minor2: return 6 # Máximo conflicto idx1 = NOTE_MAP.get(root1, 0) idx2 = NOTE_MAP.get(root2, 0) distance = abs(idx1 - idx2) return min(distance, 12 - distance) # Distancia circular target_root = target_key.replace('m', '').replace('M', '') for track in tracks_data: track_name = track.get('name', 'Unknown') track_key = track.get('key', '') if not track_key: continue distance = get_semitone_distance(target_key, track_key) # Conflicto grave: > 3 semitonos if distance >= 4: issues.append(ValidationIssue( type='key_conflict', severity='error', track=track_name, clip='', message=f"Severe key conflict: {track_key} vs {target_key} ({distance} semitones)", suggestion=f"Transpose to {target_key} or replace sample", auto_fixable=True )) elif distance >= 2: issues.append(ValidationIssue( type='key_variation', severity='warning', track=track_name, clip='', message=f"Key variation detected: {track_key} vs {target_key}", suggestion="Check if harmonic variation is intentional", auto_fixable=False )) return issues def validate_duplicates(self, clips_data: List[Dict]) -> List[ValidationIssue]: """Detecta samples duplicados accidentalmente.""" issues = [] # Agrupar por file_path file_usage = {} for clip in clips_data: file_path = clip.get('file_path', '') if not file_path: continue if file_path not in file_usage: file_usage[file_path] = [] file_usage[file_path].append(clip) # Detectar duplicados for file_path, clips in file_usage.items(): if len(clips) > 1: # Es duplicado si están en tracks diferentes tracks = set(c.get('track_name') for c in clips) if len(tracks) > 1: issues.append(ValidationIssue( type='duplicate_sample', severity='warning', track=', '.join(tracks), clip=Path(file_path).name, message=f"Sample used in {len(tracks)} different tracks", suggestion="Consider if intentional layering or accidental duplicate", auto_fixable=False )) return issues def validate_gain_staging(self, tracks_data: List[Dict]) -> List[ValidationIssue]: """Valida niveles de gain staging.""" issues = [] for track in tracks_data: track_name = track.get('name', 'Unknown') volume = track.get('volume', 0.85) # Clipping prevention if volume > 0.95: issues.append(ValidationIssue( type='high_volume', severity='warning', track=track_name, clip='', message=f"Volume too high: {volume:.2f}", suggestion="Reduce to prevent clipping", auto_fixable=True )) # Too quiet if volume < 0.1 and track.get('role') not in ['atmos', 'texture']: issues.append(ValidationIssue( type='low_volume', severity='info', track=track_name, clip='', message=f"Volume very low: {volume:.2f}", suggestion="Check if track is audible", auto_fixable=False )) return issues def run_full_validation(self, set_data: Dict) -> Dict[str, Any]: """ Ejecuta validación completa del set. Args: set_data: Datos completos del set de Ableton Returns: Reporte de validación completo """ all_issues = [] tracks = set_data.get('tracks', []) clips = set_data.get('clips', []) target_key = set_data.get('key', 'Am') # 1. Validar clips clip_issues = self.validate_clips(clips) all_issues.extend(clip_issues) # 2. Validar key conflicts key_issues = self.validate_key_conflicts(tracks, target_key) all_issues.extend(key_issues) # 3. Validar duplicados dup_issues = self.validate_duplicates(clips) all_issues.extend(dup_issues) # 4. Validar gain staging gain_issues = self.validate_gain_staging(tracks) all_issues.extend(gain_issues) # Clasificar por severidad errors = [i for i in all_issues if i.severity == 'error'] warnings = [i for i in all_issues if i.severity == 'warning'] info = [i for i in all_issues if i.severity == 'info'] auto_fixable = [i for i in all_issues if i.auto_fixable] return { 'valid': len(errors) == 0, 'summary': { 'total_issues': len(all_issues), 'errors': len(errors), 'warnings': len(warnings), 'info': len(info), 'auto_fixable': len(auto_fixable) }, 'issues': [ { 'type': i.type, 'severity': i.severity, 'track': i.track, 'clip': i.clip, 'message': i.message, 'suggestion': i.suggestion, 'auto_fixable': i.auto_fixable } for i in all_issues ], 'auto_fixes_available': [ {'type': i.type, 'track': i.track} for i in auto_fixable ] } def apply_auto_fixes(self, set_data: Dict, ableton_connection) -> Dict: """Aplica fixes automáticos para issues auto-fixable.""" fixes_applied = [] fixes_failed = [] issues = self.run_full_validation(set_data) for issue_data in issues.get('issues', []): if not issue_data.get('auto_fixable'): continue issue_type = issue_data.get('type') track = issue_data.get('track') try: if issue_type == 'invalid_loop': # Fix loop points self._fix_loop_points(ableton_connection, track, issue_data.get('clip')) fixes_applied.append({'type': 'loop_points', 'track': track}) elif issue_type == 'high_volume': # Reduce volume self._adjust_volume(ableton_connection, track, 0.85) fixes_applied.append({'type': 'volume', 'track': track}) elif issue_type == 'key_conflict': # Suggest transpose fixes_applied.append({'type': 'key_transpose_suggested', 'track': track}) except Exception as e: fixes_failed.append({'type': issue_type, 'track': track, 'error': str(e)}) return { 'fixes_applied': fixes_applied, 'fixes_failed': fixes_failed, 'total_fixed': len(fixes_applied) } def _fix_loop_points(self, ableton_connection, track: str, clip: str): """Corrige loop points inválidos.""" cmd = { 'command': 'reset_loop_points', 'track': track, 'clip': clip } ableton_connection.send_command(cmd) def _adjust_volume(self, ableton_connection, track: str, level: float): """Ajusta volumen de track.""" cmd = { 'command': 'set_track_volume', 'track': track, 'volume': level } ableton_connection.send_command(cmd) # Instancia global _validation_fixer: Optional[ValidationSystemFixer] = None def get_validation_fixer() -> ValidationSystemFixer: """Obtiene instancia global del validador.""" global _validation_fixer if _validation_fixer is None: _validation_fixer = ValidationSystemFixer() return _validation_fixer