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>
This commit is contained in:
233
AbletonMCP_AI/MCP_Server/audio_fingerprint.py
Normal file
233
AbletonMCP_AI/MCP_Server/audio_fingerprint.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user