Files
ableton-mcp-ai/AbletonMCP_AI/MCP_Server/audio_fingerprint.py
renato97 4332ff65da Implement FASE 3, 4, 6 - 15 new MCP tools, 76/110 tasks complete
FASE 3 - Human Feel & Dynamics (10/11 tasks):
- apply_clip_fades() - T041: Fade automation per section
- write_volume_automation() - T042: Curves (linear, exp, s_curve, punch)
- apply_sidechain_pump() - T045: Sidechain by intensity/style
- inject_pattern_fills() - T048: Snare rolls, fills by density
- humanize_set() - T050: Timing + velocity + groove automation

FASE 4 - Key Compatibility & Tonal (9/12 tasks):
- audio_key_compatibility.py: Full KEY_COMPATIBILITY_MATRIX
- analyze_key_compatibility() - T053: Harmonic compatibility scoring
- suggest_key_change() - T054: Circle of fifths modulation
- validate_sample_key() - T055: Sample key validation
- analyze_spectral_fit() - T057/T062: Spectral role matching

FASE 6 - Mastering & QA (8/13 tasks):
- calibrate_gain_staging() - T079: Auto gain by bus targets
- run_mix_quality_check() - T085: LUFS, peaks, L/R balance
- export_stem_mixdown() - T087: 24-bit/44.1kHz stem export

New files:
- audio_key_compatibility.py (T052)
- bus_routing_fix.py (T101-T104)
- validation_system_fix.py (T105-T106)

Total: 76/110 tasks (69%), 71 MCP tools exposed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 00:59:24 -03:00

234 lines
7.7 KiB
Python

"""
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