""" audio_fingerprint.py - Sistema de fingerprint de samples T033-T039: Wild Card, Section Casting, Fingerprint """ import hashlib import json import logging from typing import Dict, Any, List, Optional, Set from pathlib import Path from collections import defaultdict logger = logging.getLogger("AudioFingerprint") class SampleFingerprint: """ T033-T039: Sistema de fingerprint para identificación única de samples. Permite tracking, matching y deduplicación. """ def __init__(self, file_path: str): self.file_path = Path(file_path) self.hash = None self.metadata = {} self._generate() def _generate(self): """Genera fingerprint del archivo.""" if not self.file_path.exists(): self.hash = None return # Hash basado en nombre y tamaño (rápido) stat = self.file_path.stat() content = f"{self.file_path.name}_{stat.st_size}_{stat.st_mtime}" self.hash = hashlib.md5(content.encode()).hexdigest() # Metadata adicional self.metadata = { 'name': self.file_path.stem, 'size': stat.st_size, 'modified': stat.st_mtime, 'extension': self.file_path.suffix, } def to_dict(self) -> Dict[str, Any]: return { 'hash': self.hash, 'path': str(self.file_path), 'metadata': self.metadata } class FingerprintDatabase: """Base de datos de fingerprints para tracking.""" def __init__(self, db_path: Optional[str] = None): self.db_path = Path(db_path) if db_path else Path.home() / ".abletonmcp_ai" / "fingerprints.json" self.db_path.parent.mkdir(parents=True, exist_ok=True) self._fingerprints: Dict[str, Dict] = {} self._load() def _load(self): """Carga base de datos existente.""" if self.db_path.exists(): try: with open(self.db_path, 'r', encoding='utf-8') as f: self._fingerprints = json.load(f) logger.info(f"Loaded {len(self._fingerprints)} fingerprints") except Exception as e: logger.warning(f"Could not load fingerprints: {e}") self._fingerprints = {} def _save(self): """Guarda base de datos.""" with open(self.db_path, 'w', encoding='utf-8') as f: json.dump(self._fingerprints, f, indent=2) def add(self, sample_path: str) -> Optional[str]: """Agrega sample a la base de datos.""" fp = SampleFingerprint(sample_path) if fp.hash: self._fingerprints[fp.hash] = fp.to_dict() self._save() return fp.hash return None def find_duplicates(self) -> List[List[str]]: """Encuentra samples duplicados por hash.""" hash_to_paths = defaultdict(list) for hash_val, data in self._fingerprints.items(): hash_to_paths[hash_val].append(data['path']) # Retornar grupos con más de 1 archivo return [paths for paths in hash_to_paths.values() if len(paths) > 1] def find_by_name(self, name_pattern: str) -> List[Dict]: """Busca por nombre.""" results = [] for data in self._fingerprints.values(): if name_pattern.lower() in data['metadata']['name'].lower(): results.append(data) return results class WildCardMatcher: """ T033-T034: Wild Card system para matching flexible. """ WILD_PATTERNS = { 'any_drum': ['*kick*', '*snare*', '*clap*', '*hat*', '*perc*'], 'any_bass': ['*bass*', '*sub*', '*808*', '*low*'], 'any_synth': ['*synth*', '*pad*', '*lead*', '*chord*', '*arp*'], 'any_vocal': ['*vocal*', '*vox*', '*voice*', '*chant*'], 'any_fx': ['*riser*', '*downlifter*', '*impact*', '*fx*'], } @classmethod def get_wildcard_query(cls, category: str) -> List[str]: """Retorna patrones wildcard para una categoría.""" return cls.WILD_PATTERNS.get(category.lower(), [f'*{category}*']) class SectionCastingEngine: """ T035-T037: Section Casting - asignación de roles por sección. """ SECTION_ROLES = { 'intro': { 'primary': ['atmos', 'pad', 'texture'], 'secondary': ['kick', 'bass'], 'avoid': ['lead', 'full_drums'] }, 'build': { 'primary': ['snare_roll', 'riser', 'perc'], 'secondary': ['bass', 'pad'], 'avoid': ['full_atmos'] }, 'drop': { 'primary': ['kick', 'bass', 'lead', 'full_drums'], 'secondary': ['synth', 'pad'], 'avoid': ['atmos', 'break_atmos'] }, 'break': { 'primary': ['pad', 'atmos', 'vocal', 'pluck'], 'secondary': ['light_perc'], 'avoid': ['heavy_kick', 'full_bass'] }, 'outro': { 'primary': ['pad', 'atmos', 'texture'], 'secondary': ['kick'], 'avoid': ['lead', 'full_drums', 'heavy_bass'] } } def get_roles_for_section(self, section_kind: str) -> Dict[str, List[str]]: """Retorna roles recomendados para una sección.""" return self.SECTION_ROLES.get(section_kind.lower(), { 'primary': [], 'secondary': [], 'avoid': [] }) def filter_samples_for_section(self, samples: List[Dict], section_kind: str) -> List[Dict]: """Filtra samples apropiados para una sección.""" roles = self.get_roles_for_section(section_kind) primary = set(roles['primary']) filtered = [] for sample in samples: sample_type = sample.get('type', '').lower() if any(p in sample_type for p in primary): sample['section_priority'] = 'primary' filtered.append(sample) elif not any(a in sample_type for a in roles['avoid']): sample['section_priority'] = 'secondary' filtered.append(sample) return sorted(filtered, key=lambda x: x.get('section_priority', '') != 'primary') class SampleFamilyTracker: """ T038-T039: Tracking de familias de samples. """ def __init__(self): self.families: Dict[str, Set[str]] = defaultdict(set) self.usage_count: Dict[str, int] = defaultdict(int) def register_family(self, family_name: str, sample_path: str): """Registra un sample como parte de una familia.""" self.families[family_name].add(sample_path) def record_usage(self, family_name: str): """Registra uso de una familia.""" self.usage_count[family_name] += 1 def get_least_used_family(self, families: List[str]) -> str: """Retorna la familia menos usada.""" if not families: return '' return min(families, key=lambda f: self.usage_count.get(f, 0)) def get_family_diversity_score(self) -> float: """Calcula score de diversidad (0-1).""" if not self.usage_count: return 1.0 total = sum(self.usage_count.values()) unique = len(self.usage_count) # Más familias usadas = mejor diversidad return min(1.0, unique / max(1, total / 3)) # Instancias globales _fingerprint_db: Optional[FingerprintDatabase] = None _family_tracker: Optional[SampleFamilyTracker] = None def get_fingerprint_db() -> FingerprintDatabase: """Obtiene instancia global de fingerprint database.""" global _fingerprint_db if _fingerprint_db is None: _fingerprint_db = FingerprintDatabase() return _fingerprint_db def get_family_tracker() -> SampleFamilyTracker: """Obtiene instancia global de family tracker.""" global _family_tracker if _family_tracker is None: _family_tracker = SampleFamilyTracker() return _family_tracker