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:
renato97
2026-03-29 00:59:24 -03:00
parent ed6f75c49f
commit 4332ff65da
24 changed files with 6586 additions and 38 deletions

View File

@@ -24,6 +24,7 @@ import time
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, field
from collections import defaultdict, deque
from pathlib import Path
# Detección de numpy para cálculos vectorizados
try:
@@ -1066,6 +1067,12 @@ class SampleSelector:
score += energy_score * 0.05
weights += 0.05
# T017: Factor brightness_fit (peso 0.10)
brightness_score = self._calculate_brightness_fit(sample, target_role)
if brightness_score < 1.0:
score += brightness_score * 0.10
weights += 0.10
# 9. Cooldown por familia (penaliza familias recientemente usadas)
if target_role and target_role.lower() in ['kick', 'clap', 'hat', 'bass_loop', 'vocal_loop']:
family = _extract_sample_family(sample.name)
@@ -1087,6 +1094,29 @@ class SampleSelector:
logger.debug("CROSS_GEN: family '%s' has cross-gen penalty %.2f for role '%s' (used in %d prev generations)",
family, cross_penalty, target_role.lower(), _cross_generation_family_memory.get(family, 0))
# T022: Factor de fatiga persistente (opcional - requiere integración con server.py)
# Este factor se aplica si el server.py pasa datos de fatiga al selector
if hasattr(self, '_fatigue_data') and target_role:
sample_path = getattr(sample, 'path', '') or getattr(sample, 'file_path', '') or ''
fatigue_factor = self._get_persistent_fatigue(sample_path, target_role.lower())
if fatigue_factor < 1.0:
score *= fatigue_factor
weights += 0.10
logger.debug("FATIGUE: sample '%s' has fatigue factor %.2f for role '%s'",
Path(sample_path).name, fatigue_factor, target_role.lower())
# T026: Palette bonus (integración con server.py)
if hasattr(self, '_palette_data') and target_role:
sample_path = getattr(sample, 'path', '') or getattr(sample, 'file_path', '') or ''
bus = self._role_to_bus(target_role.lower())
if bus and bus in self._palette_data:
anchor_folder = self._palette_data[bus]
palette_bonus = self._calculate_palette_bonus(sample_path, anchor_folder)
score *= palette_bonus
weights += 0.15
logger.debug("PALETTE: sample '%s' has palette bonus %.2f for bus '%s'",
Path(sample_path).name, palette_bonus, bus)
# Normalizar
return score / weights if weights > 0 else 0.5
@@ -1266,6 +1296,152 @@ class SampleSelector:
return True, ""
def _calculate_brightness_fit(self, sample: 'Sample', target_role: Optional[str]) -> float:
"""
T017: Calcula ajuste de brillo espectral para el rol objetivo.
Retorna score 0-1 donde 1.0 = perfecto ajuste, <1.0 = penalización aplicada.
Reglas:
- atmos, pad, drone: penalizar spectral_centroid > 8000 Hz (demasiado brillante)
- bass, sub_bass: penalizar spectral_centroid > 3000 Hz (pierde sub)
- lead, chord: sin penalización por brillo, pero preferir centrado medio
"""
if not target_role:
return 1.0
target_role_lower = target_role.lower()
# Obtener spectral_centroid del sample (si está disponible)
spectral_centroid = getattr(sample, 'spectral_centroid', None) or 5000.0
# Roles que prefieren sonidos oscuros/cálidos
dark_preferred_roles = ['atmos', 'pad', 'drone', 'ambience', 'texture']
if any(r in target_role_lower for r in dark_preferred_roles):
if spectral_centroid > 8000:
# Penalización progresiva: >8000 = 0.5, >10000 = 0.3
return max(0.3, 1.0 - (spectral_centroid - 8000) / 4000)
elif spectral_centroid > 6000:
return 0.8
else:
return 1.0
# Roles de bajo que necesitan contenido de graves
bass_roles = ['bass', 'sub_bass', 'bassline', '808', 'sub']
if any(r in target_role_lower for r in bass_roles):
if spectral_centroid > 3000:
# Penalización severa para bass sin graves
return max(0.2, 1.0 - (spectral_centroid - 3000) / 2000)
elif spectral_centroid > 1500:
return 0.7
else:
return 1.0
# Roles brillantes permitidos
bright_roles = ['lead', 'chord', 'stab', 'pluck', 'arp', 'synth']
if any(r in target_role_lower for r in bright_roles):
# Preferir rango medio-alto, no demasiado brillante ni opaco
if 2000 <= spectral_centroid <= 8000:
return 1.0
elif spectral_centroid < 1000:
return 0.7 # Quizás demasiado opaco
elif spectral_centroid > 12000:
return 0.8 # Quizás demasiado brillante/agudo
else:
return 0.9
# Default: sin penalización
return 1.0
def set_fatigue_data(self, fatigue_data: Dict[str, Dict[str, Any]]) -> None:
"""
T022: Carga datos de fatiga persistente desde server.py.
Permite que el selector aplique penalización por uso previo.
"""
self._fatigue_data = fatigue_data
logger.debug(f"Fatigue data cargada: {len(fatigue_data)} samples")
def _get_persistent_fatigue(self, sample_path: str, role: str) -> float:
"""
T022: Obtiene factor de fatiga persistente para un sample y rol.
Retorna:
- 1.0: Sin fatiga (0 usos)
- 0.75: Fatiga ligera (1-3 usos)
- 0.50: Fatiga moderada (4-10 usos)
- 0.20: Fatiga severa (10+ usos)
"""
if not hasattr(self, '_fatigue_data') or not self._fatigue_data:
return 1.0
sample_fatigue = self._fatigue_data.get(sample_path, {})
role_data = sample_fatigue.get(role, {})
uses = role_data.get("uses", 0)
if uses == 0:
return 1.0
elif 1 <= uses <= 3:
return 0.75
elif 4 <= uses <= 10:
return 0.50
else:
return 0.20
def set_palette_data(self, palette_data: Dict[str, str]) -> None:
"""
T026: Carga datos de palette desde server.py.
Permite aplicar bonus/penalización por compatibilidad con ancla.
"""
self._palette_data = palette_data
logger.debug(f"Palette data cargada: {palette_data}")
def _role_to_bus(self, role: str) -> Optional[str]:
"""Mapea un rol a su bus correspondiente."""
bus_mapping = {
'kick': 'drums', 'clap': 'drums', 'hat': 'drums', 'snare': 'drums',
'perc': 'drums', 'top_loop': 'drums', 'drum_loop': 'drums',
'bass': 'bass', 'sub_bass': 'bass', 'bass_loop': 'bass', '808': 'bass',
'synth': 'music', 'pad': 'music', 'lead': 'music', 'chord': 'music',
'arp': 'music', 'pluck': 'music', 'synth_loop': 'music',
'vocal': 'vocal', 'vocal_loop': 'vocal', 'vox': 'vocal',
'fx': 'fx', 'riser': 'fx', 'impact': 'fx', 'atmos': 'fx'
}
return bus_mapping.get(role.lower())
def _calculate_palette_bonus(self, sample_path: str, anchor_folder: str) -> float:
"""
T026: Calcula bonus por compatibilidad con folder ancla.
- Folder exacto: 1.4x
- Subfolder del ancla: 1.3x
- Folder hermano (mismo padre): 1.2x
- Diferente: 0.9x
"""
import os
if not anchor_folder:
return 1.0
# Normalize paths to use forward slashes
sample_folder = str(Path(sample_path).parent).replace(os.sep, '/')
anchor = anchor_folder.replace(os.sep, '/')
# Match exacto
if sample_folder == anchor:
return 1.4
# Subfolder del ancla
if sample_folder.startswith(anchor + '/'):
return 1.3
# Mismo padre (hermano)
sample_parent = str(Path(sample_folder).parent).replace(os.sep, '/')
anchor_parent = str(Path(anchor).parent).replace(os.sep, '/')
if sample_parent == anchor_parent:
return 1.2
# Diferente
return 0.9
def _calculate_repetition_penalty(self, sample: 'Sample') -> float:
"""
Calcula penalización por repetición de sample y familia.