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>
223 lines
7.3 KiB
Python
223 lines
7.3 KiB
Python
"""
|
|
validate_key_detection.py - Script de validación T019
|
|
Valida que librosa detecta key correctamente en ≥70% de samples armónicos.
|
|
|
|
Uso:
|
|
python validate_key_detection.py <ruta_libreria> [--samples N]
|
|
"""
|
|
|
|
import sys
|
|
import random
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import List, Dict, Any
|
|
import logging
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger("T019-Validation")
|
|
|
|
# Importar AudioAnalyzer
|
|
try:
|
|
from audio_analyzer import AudioAnalyzer, SampleType
|
|
ANALYZER_AVAILABLE = True
|
|
except ImportError:
|
|
ANALYZER_AVAILABLE = False
|
|
logger.error("No se pudo importar AudioAnalyzer")
|
|
sys.exit(1)
|
|
|
|
|
|
def find_harmonic_samples(library_dir: str, max_samples: int = 50) -> List[Path]:
|
|
"""
|
|
Busca samples armónicos (bass, pad, synth, chord, lead, etc.) en la librería.
|
|
"""
|
|
library_path = Path(library_dir)
|
|
extensions = {'.wav', '.aif', '.aiff', '.mp3'}
|
|
|
|
all_files = []
|
|
for ext in extensions:
|
|
all_files.extend(library_path.rglob(f'*{ext}'))
|
|
all_files.extend(library_path.rglob(f'*{ext.upper()}'))
|
|
|
|
# Filtrar por nombre para encontrar samples armónicos probables
|
|
harmonic_keywords = [
|
|
'bass', 'pad', 'synth', 'lead', 'chord', 'stab', 'pluck',
|
|
'arp', 'vocal', 'keys', 'piano', 'guitar', 'strings', 'pad'
|
|
]
|
|
|
|
harmonic_files = []
|
|
for f in all_files:
|
|
name_lower = f.stem.lower()
|
|
if any(kw in name_lower for kw in harmonic_keywords):
|
|
harmonic_files.append(f)
|
|
|
|
# Seleccionar muestra aleatoria
|
|
if len(harmonic_files) > max_samples:
|
|
return random.sample(harmonic_files, max_samples)
|
|
return harmonic_files
|
|
|
|
|
|
def validate_key_detection(samples: List[Path]) -> Dict[str, Any]:
|
|
"""
|
|
Valida detección de key en samples.
|
|
Retorna estadísticas de la validación.
|
|
"""
|
|
analyzer = AudioAnalyzer()
|
|
|
|
results = {
|
|
'total': len(samples),
|
|
'with_key_detected': 0,
|
|
'with_key_in_name': 0,
|
|
'matching_keys': 0,
|
|
'high_confidence': 0, # confidence > 0.6
|
|
'low_confidence': 0,
|
|
'by_type': {},
|
|
'failures': []
|
|
}
|
|
|
|
for sample_path in samples:
|
|
try:
|
|
features = analyzer.analyze(str(sample_path))
|
|
|
|
# Extraer key del nombre si existe
|
|
key_from_name = analyzer._extract_key_from_name(sample_path.stem)
|
|
|
|
result_entry = {
|
|
'file': str(sample_path),
|
|
'detected_key': features.key,
|
|
'key_confidence': features.key_confidence,
|
|
'key_from_name': key_from_name,
|
|
'sample_type': features.sample_type.value,
|
|
'spectral_centroid': features.spectral_centroid,
|
|
'is_harmonic': features.is_harmonic
|
|
}
|
|
|
|
# Contar key detectada
|
|
if features.key:
|
|
results['with_key_detected'] += 1
|
|
|
|
# Alta confianza
|
|
if features.key_confidence > 0.6:
|
|
results['high_confidence'] += 1
|
|
else:
|
|
results['low_confidence'] += 1
|
|
|
|
# Key en nombre
|
|
if key_from_name:
|
|
results['with_key_in_name'] += 1
|
|
|
|
# Comparar si coinciden
|
|
if features.key and features.key.lower() == key_from_name.lower():
|
|
results['matching_keys'] += 1
|
|
result_entry['match'] = True
|
|
else:
|
|
result_entry['match'] = False
|
|
|
|
# Por tipo
|
|
sample_type = features.sample_type.value
|
|
if sample_type not in results['by_type']:
|
|
results['by_type'][sample_type] = {'total': 0, 'with_key': 0}
|
|
results['by_type'][sample_type]['total'] += 1
|
|
if features.key:
|
|
results['by_type'][sample_type]['with_key'] += 1
|
|
|
|
# Si no detectó key en sample armónico, es un "failure"
|
|
if features.is_harmonic and not features.key:
|
|
results['failures'].append(result_entry)
|
|
|
|
logger.info(f"✓ {sample_path.stem}: key={features.key} "
|
|
f"(conf={features.key_confidence:.2f}, "
|
|
f"type={features.sample_type.value})")
|
|
|
|
except Exception as e:
|
|
logger.error(f"✗ Error analizando {sample_path}: {e}")
|
|
results['failures'].append({'file': str(sample_path), 'error': str(e)})
|
|
|
|
return results
|
|
|
|
|
|
def print_report(results: Dict[str, Any]):
|
|
"""Imprime reporte de validación T019."""
|
|
total = results['total']
|
|
|
|
print("\n" + "=" * 60)
|
|
print("📊 REPORTE DE VALIDACIÓN T019: Key Detection con librosa")
|
|
print("=" * 60)
|
|
|
|
print(f"\n📁 Total samples analizados: {total}")
|
|
print(f"🔑 Keys detectadas: {results['with_key_detected']} "
|
|
f"({results['with_key_detected'] / total * 100:.1f}%)")
|
|
print(f"📋 Keys en nombre de archivo: {results['with_key_in_name']}")
|
|
print(f"✅ Keys coincidentes (detectada vs nombre): {results['matching_keys']}")
|
|
|
|
print(f"\n📈 Distribución de confianza:")
|
|
print(f" Alta (>0.6): {results['high_confidence']} "
|
|
f"({results['high_confidence'] / total * 100:.1f}%)")
|
|
print(f" Baja (≤0.6): {results['low_confidence']} "
|
|
f"({results['low_confidence'] / total * 100:.1f}%)")
|
|
|
|
print(f"\n📊 Por tipo de sample:")
|
|
for sample_type, stats in sorted(results['by_type'].items()):
|
|
rate = stats['with_key'] / stats['total'] * 100 if stats['total'] > 0 else 0
|
|
print(f" {sample_type}: {stats['with_key']}/{stats['total']} con key ({rate:.1f}%)")
|
|
|
|
# Verificar KPI T019
|
|
detection_rate = results['with_key_detected'] / total * 100 if total > 0 else 0
|
|
print(f"\n🎯 KPI T019: Detección de key en ≥70% de samples")
|
|
print(f" Resultado: {detection_rate:.1f}%")
|
|
if detection_rate >= 70:
|
|
print(f" ✅ CUMPLE el objetivo de 70%")
|
|
else:
|
|
print(f" ❌ NO CUMPLE el objetivo (necesita mejorar)")
|
|
|
|
if results['failures']:
|
|
print(f"\n⚠️ {len(results['failures'])} samples armónicos sin key detectada:")
|
|
for f in results['failures'][:10]: # Mostrar primeros 10
|
|
print(f" - {Path(f['file']).name}")
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Validar detección de key con librosa (T019)'
|
|
)
|
|
parser.add_argument(
|
|
'library_dir',
|
|
help='Ruta a la librería de samples'
|
|
)
|
|
parser.add_argument(
|
|
'--samples', '-n',
|
|
type=int,
|
|
default=50,
|
|
help='Número de samples a analizar (default: 50)'
|
|
)
|
|
parser.add_argument(
|
|
'--seed',
|
|
type=int,
|
|
default=42,
|
|
help='Seed para reproducibilidad (default: 42)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
random.seed(args.seed)
|
|
|
|
print(f"🔍 Buscando samples armónicos en: {args.library_dir}")
|
|
samples = find_harmonic_samples(args.library_dir, args.samples)
|
|
|
|
if not samples:
|
|
logger.error("No se encontraron samples armónicos")
|
|
sys.exit(1)
|
|
|
|
print(f"🎵 Analizando {len(samples)} samples...")
|
|
results = validate_key_detection(samples)
|
|
print_report(results)
|
|
|
|
# Exit code según KPI
|
|
detection_rate = results['with_key_detected'] / results['total'] * 100
|
|
sys.exit(0 if detection_rate >= 70 else 1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|