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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user