Sync: Complete project state with all MEGA SPRINT V1-V3 features and Codex stubs

This commit is contained in:
renato97
2026-04-08 17:58:47 -03:00
parent c9d3528900
commit 6d080d43b3
372 changed files with 189715 additions and 8590 deletions

View File

@@ -0,0 +1,596 @@
# GRANULAR SPRINT PART 1 — Tareas T001T100
## Enfoque: Bug Fixes + Motor Espectral/Granular + Coherencia Reggaeton
> **Contexto obligatorio para GLM-5:**
> - MCP server corre en WSL. Todos los paths en `server.py` deben usar rutas Windows absolutas (ya configuradas). No cambies PROGRAM_DATA_DIR.
> - El Remote Script (`abletonmcp_init.py`) corre en el hilo de Live. NUNCA uses `time.sleep()` ahí.
> - Compila con `python -m py_compile` después de cada cambio. Reinicia Ableton después de cambiar `abletonmcp_init.py`.
> - Las herramientas MCP disponibles son: `get_session_info`, `get_tracks`, `get_track_info`, `create_arrangement_clip`, `add_notes_to_arrangement_clip`, `create_arrangement_audio_pattern`, `audit_project_coherence`, `set_device_parameter`, `delete_arrangement_clip`.
> - Proyecto activo: `C:\Users\ren\Desktop\song Project\song.als` — 95 BPM, clave Am, 16 tracks.
---
## BLOQUE A — BUG FIXES CRÍTICOS (T001T015)
### T001 — Eliminar time.sleep del hilo Live
**Archivo:** `abletonmcp_init.py` ~línea 14501477
**Acción exacta:** Elimina el bloque `while total_wait < max_wait:` y sus `time.sleep(0.05)` de `_record_session_clip_to_arrangement`. Reemplaza con una sola búsqueda sin sleep:
```python
for tol in (0.05, 0.25, 1.0, 1.5):
clip = self._locate_arrangement_clip(track, start_time, tol, length)
if clip: break
if not clip:
class ProxyClip:
def __init__(self, l, n): self.length=l; self.name=n; self.start_time=start_time
def set_notes(self, n): pass
clip = ProxyClip(length, f"Proxy_{start_time}")
self._recent_arrangement_clips[(int(track_index), round(float(start_time),3))] = clip
return clip
```
**Valida:** `python -m py_compile abletonmcp_init.py` → sin errores.
### T002 — Eliminar time.sleep de duplicate_clip_to_arrangement
**Archivo:** `abletonmcp_init.py` ~línea 1581
**Acción:** Elimina `time.sleep(0.5)` dentro de `_create_arrangement_clip` en el bloque `duplicate_clip_to_arrangement`. Si no encuentra clip tras la búsqueda inmediata, devuelve ProxyClip igual que T001.
### T003 — Arreglar corrupción UTF-8 en headers de sample_selector.py
**Archivo:** `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/sample_selector.py` líneas 517
**Problema:** Strings con `ÃÆ'Â` (doble-encoding latin1→utf8).
**Acción:** Reescribe los docstrings afectados con texto ASCII simple. No cambies lógica, solo los strings de documentación.
### T004 — Cleanup imports no usados en audio_analyzer.py
**Archivo:** `audio_analyzer.py` línea 317
**Acción:** Elimina `import struct` si existe y no se usa. Verifica con `grep -n "struct" audio_analyzer.py`.
### T005 — Cleanup imports no usados en sample_manager.py
**Archivo:** `sample_manager.py`
**Acción:** Elimina las líneas con `import os`, `import shutil`, `import time`, `from typing import Set` si no se usan en el cuerpo del archivo. Verifica antes con grep. Compila tras limpiar.
### T006 — Arreglar file_hash sin usar en sample_manager.py
**Archivo:** `sample_manager.py` ~línea 292
**Acción:** Cambia `file_hash = ...` a `_file_hash = ...` (prefijo underscore para suprimir warning F841) o elimina la asignación si no se usa en ninguna otra parte del método.
### T007 — WSL path normalization en _create_arrangement_audio_pattern
**Archivo:** `abletonmcp_init.py`, función `_create_arrangement_audio_pattern`
**Problema:** Cuando el MCP corre en WSL, los paths `/mnt/c/...` llegan al Remote Script que corre en Windows. El Remote Script necesita `C:\...`.
**Acción:** Al inicio de `_create_arrangement_audio_pattern`, añade:
```python
if str(file_path).startswith('/mnt/'):
parts = file_path[5:].split('/', 1)
file_path = parts[0].upper() + ":\\" + parts[1].replace('/', '\\')
```
### T008 — WSL path normalization en create_arrangement_clip (server.py)
**Archivo:** `server.py`, en el tool handler de `create_arrangement_audio_pattern`
**Acción:** Antes de enviar el comando al Remote Script, normaliza cualquier path `/mnt/c/...` a `C:\...`. Crea una función helper `_normalize_wsl_path(path: str) -> str` y úsala en todos los tool handlers que reciban `file_path` o `sample_path`.
### T009 — Enforce reinicio en KIMI_K2_ACTIVE_HANDOFF.md
**Archivo:** `KIMI_K2_ACTIVE_HANDOFF.md`
**Acción:** Añade sección al final: "## Cambios que requieren reinicio de Ableton" listando explícitamente qué archivos obligan a reiniciar (`abletonmcp_init.py`, `abletonmcp_runtime.py`, `AbletonMCP_AI/__init__.py`).
### T010 — Fix variables no usadas en song_generator.py
**Archivo:** `song_generator.py`
**Acción:** Las variables `materialized_track_roles` y `event_track_roles` se llenan pero nunca se leen. Añade un `self.log_message(f"[COHERENCE] materialized_track_roles={materialized_track_roles}")` donde corresponda, o elimínalas si son realmente inútiles.
### T011 — Arreglar tofix.md y actualizar fecha
**Archivo:** `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/tofix.md`
**Acción:** Actualiza la fecha a 2026-04-05. Agrega las issues de T001T010 a la sección "Ya corregido" una vez que estén resueltas.
### T012 — Verificar que generate_song_async no excede budget de 16 tracks
**Archivo:** `server.py`
**Acción:** Busca en `generate_song_async` o `_generate_track_impl` dónde se crea budget. Verifica que `GenerationBudget(max_tracks=16)` se instancia una sola vez por generación. Si hay múltiples instancias, consolida.
### T013 — Asegurar que MIDI hook tiene slot reservado antes de audio layers
**Archivo:** `server.py`
**Acción:** Después de `reset_budget(max_tracks=16)`, llama a `budget.reserve_slot('hook_midi', 'HARMONY_PIANO_MIDI', 'midi', 'mandatory_midi_hook')` antes de cualquier otra reserva. No elimines esta reserva hasta después de materializar el hook.
### T014 — Compilar todos los archivos modificados
**Acción post T001T013:**
```powershell
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\abletonmcp_init.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\server.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\sample_selector.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\song_generator.py"
```
**Criterio de éxito:** Sin errores de compilación.
### T015 — Pedir reinicio de Ableton y verificar conexión
**Acción:** Informa al usuario: "Por favor reinicia Ableton Live ahora." Luego llama a `get_session_info`. Debe retornar BPM 95 y al menos 16 tracks. Si falla, revisa Log.txt.
---
## BLOQUE B — MOTOR ESPECTRAL GRANULAR (T016T045)
### T016 — Crear módulo spectral_engine.py
**Archivo nuevo:** `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/spectral_engine.py`
**Propósito:** Motor de análisis espectral para comparar samples y asignarlos por similitud tímbrica, no solo por nombre o BPM. Es el núcleo de la "producción granular".
**Estructura mínima:**
```python
"""spectral_engine.py — Análisis espectral para selección por similitud tímbrica."""
import numpy as np
import logging
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
logger = logging.getLogger("SpectralEngine")
@dataclass
class SpectralProfile:
"""Perfil espectral de un sample de audio."""
path: str
centroid_mean: float # Hz — centro de masa espectral
centroid_std: float # Varianza del centroide
rolloff_85: float # Hz donde está el 85% de energía
flux_mean: float # Cambio espectral medio (percusividad)
mfcc: List[float] # 13 coeficientes MFCC normalizados
rms: float # Energía RMS normalizada
spectral_flatness: float # 0=tonal, 1=ruido
duration: float # segundos
genre_hints: List[str] # géneros sugeridos por espectro
class SpectralEngine:
def __init__(self):
self._cache: Dict[str, SpectralProfile] = {}
self._librosa = None
self._np = np
self._init_librosa()
def _init_librosa(self):
try:
import librosa
self._librosa = librosa
logger.info("[SPECTRAL] librosa disponible")
except ImportError:
logger.warning("[SPECTRAL] librosa no disponible, usando análisis básico")
def analyze(self, path: str) -> Optional[SpectralProfile]:
if path in self._cache:
return self._cache[path]
if self._librosa:
profile = self._analyze_librosa(path)
else:
profile = self._analyze_basic(path)
if profile:
self._cache[path] = profile
return profile
def similarity(self, a: SpectralProfile, b: SpectralProfile) -> float:
"""Retorna similitud 0.01.0 entre dos perfiles espectrales."""
if not a or not b:
return 0.0
centroid_sim = 1.0 - min(abs(a.centroid_mean - b.centroid_mean) / max(a.centroid_mean + 1, 1), 1.0)
rolloff_sim = 1.0 - min(abs(a.rolloff_85 - b.rolloff_85) / max(a.rolloff_85 + 1, 1), 1.0)
flux_sim = 1.0 - min(abs(a.flux_mean - b.flux_mean) / max(a.flux_mean + 1, 1), 1.0)
mfcc_sim = 0.0
if a.mfcc and b.mfcc and len(a.mfcc) == len(b.mfcc):
diff = sum((x-y)**2 for x,y in zip(a.mfcc, b.mfcc))
mfcc_sim = 1.0 / (1.0 + diff**0.5)
return 0.35*centroid_sim + 0.25*rolloff_sim + 0.15*flux_sim + 0.25*mfcc_sim
def find_most_similar(self, reference_path: str, candidates: List[str], top_n: int = 5) -> List[Tuple[str, float]]:
"""Dado un sample de referencia, retorna los N candidatos más similares."""
ref = self.analyze(reference_path)
if not ref:
return []
scored = []
for c in candidates:
prof = self.analyze(c)
if prof:
score = self.similarity(ref, prof)
scored.append((c, score))
return sorted(scored, key=lambda x: x[1], reverse=True)[:top_n]
def _analyze_librosa(self, path: str) -> Optional[SpectralProfile]:
try:
lib = self._librosa
y, sr = lib.load(path, sr=None, mono=True, duration=30.0)
centroid = lib.feature.spectral_centroid(y=y, sr=sr)[0]
rolloff = lib.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.85)[0]
flux = lib.feature.spectral_flux(y=y, sr=sr)[0] if hasattr(lib.feature, 'spectral_flux') else np.array([0.0])
mfccs = lib.feature.mfcc(y=y, sr=sr, n_mfcc=13)
rms = lib.feature.rms(y=y)[0]
flatness = lib.feature.spectral_flatness(y=y)[0]
duration = lib.get_duration(y=y, sr=sr)
return SpectralProfile(
path=path,
centroid_mean=float(np.mean(centroid)),
centroid_std=float(np.std(centroid)),
rolloff_85=float(np.mean(rolloff)),
flux_mean=float(np.mean(flux)),
mfcc=[float(np.mean(mfccs[i])) for i in range(13)],
rms=float(np.mean(rms)),
spectral_flatness=float(np.mean(flatness)),
duration=float(duration),
genre_hints=self._infer_genre_hints(float(np.mean(centroid)), float(np.mean(rms)))
)
except Exception as e:
logger.warning(f"[SPECTRAL] Error analizando {path}: {e}")
return None
def _analyze_basic(self, path: str) -> Optional[SpectralProfile]:
import os
name = os.path.basename(path).lower()
centroid = 5000.0 if any(k in name for k in ['hat','shaker','top']) else (200.0 if 'bass' in name or 'sub' in name else 2000.0)
return SpectralProfile(
path=path, centroid_mean=centroid, centroid_std=100.0,
rolloff_85=centroid*2, flux_mean=0.1, mfcc=[0.0]*13,
rms=0.3, spectral_flatness=0.5 if 'noise' in name else 0.1,
duration=2.0, genre_hints=self._infer_genre_hints(centroid, 0.3)
)
def _infer_genre_hints(self, centroid: float, rms: float) -> List[str]:
hints = []
if centroid < 500 and rms > 0.4: hints.append('reggaeton_bass')
if 500 < centroid < 3000: hints.append('reggaeton_perc')
if centroid > 6000: hints.append('hi_freq_perc')
return hints or ['unknown']
_engine_instance: Optional[SpectralEngine] = None
def get_spectral_engine() -> SpectralEngine:
global _engine_instance
if _engine_instance is None:
_engine_instance = SpectralEngine()
return _engine_instance
```
**Valida:** `python -m py_compile spectral_engine.py`
### T017 — Integrar SpectralEngine en sample_selector.py
**Archivo:** `sample_selector.py`
**Acción:** En el método de scoring principal (donde se calcula el score final de un candidato), añade un bonus espectral si `SpectralEngine` está disponible:
```python
try:
from .spectral_engine import get_spectral_engine
eng = get_spectral_engine()
if reference_path and eng:
ref_prof = eng.analyze(reference_path)
cand_prof = eng.analyze(candidate_path)
if ref_prof and cand_prof:
spectral_bonus = eng.similarity(ref_prof, cand_prof) * 0.25
score = score * 0.75 + spectral_bonus
except Exception:
pass
```
### T018 — Añadir MCP tool: analyze_sample_spectrum
**Archivo:** `server.py`
**Acción:** Añade un tool:
```python
@mcp.tool()
async def analyze_sample_spectrum(file_path: str) -> str:
"""Analiza el espectro de un sample y retorna su perfil tímbrico."""
from spectral_engine import get_spectral_engine
eng = get_spectral_engine()
profile = eng.analyze(file_path)
if not profile:
return "[ERROR] No se pudo analizar el sample"
return json.dumps({
"centroid_hz": round(profile.centroid_mean, 1),
"rolloff_85_hz": round(profile.rolloff_85, 1),
"spectral_flatness": round(profile.spectral_flatness, 3),
"duration_s": round(profile.duration, 2),
"genre_hints": profile.genre_hints
}, indent=2)
```
### T019 — Añadir MCP tool: find_similar_samples
**Archivo:** `server.py`
**Acción:** Añade:
```python
@mcp.tool()
async def find_similar_samples(reference_path: str, search_folder: str, top_n: int = 5) -> str:
"""Encuentra los N samples más similares espectralmente al de referencia."""
import os
from spectral_engine import get_spectral_engine
eng = get_spectral_engine()
candidates = [os.path.join(search_folder, f) for f in os.listdir(search_folder)
if f.lower().endswith(('.wav', '.aif', '.aiff', '.mp3'))]
results = eng.find_most_similar(reference_path, candidates, top_n=top_n)
return json.dumps([{"path": p, "similarity": round(s, 3)} for p, s in results], indent=2)
```
### T020 — Crear índice espectral de la librería reggaeton
**Archivo:** crear `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/build_spectral_index.py`
**Propósito:** Script offline que pre-analiza toda la librería y guarda un JSON con perfiles para que en runtime no se recalcule.
```python
#!/usr/bin/env python3
"""Construye índice espectral de la librería de samples."""
import json, os, sys
sys.path.insert(0, os.path.dirname(__file__))
from spectral_engine import get_spectral_engine
LIBRARY = r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria\reggaeton"
INDEX_FILE = os.path.join(os.path.dirname(__file__), "spectral_index.json")
def build():
eng = get_spectral_engine()
index = {}
for root, dirs, files in os.walk(LIBRARY):
for f in files:
if f.lower().endswith(('.wav','.aif','.aiff')):
path = os.path.join(root, f)
prof = eng.analyze(path)
if prof:
index[path] = {
"centroid": prof.centroid_mean,
"rolloff": prof.rolloff_85,
"flux": prof.flux_mean,
"mfcc": prof.mfcc,
"rms": prof.rms,
"flatness": prof.spectral_flatness,
"duration": prof.duration,
"genre_hints": prof.genre_hints
}
print(f"OK: {f}")
with open(INDEX_FILE, 'w') as fh:
json.dump(index, fh, indent=2)
print(f"Índice guardado: {len(index)} samples en {INDEX_FILE}")
if __name__ == "__main__":
build()
```
**Ejecuta:** `python build_spectral_index.py` (puede tardar 25 minutos si librosa está disponible).
### T021 — Cargar índice espectral en SpectralEngine.__init__
**Archivo:** `spectral_engine.py`
**Acción:** En `__init__`, si existe `spectral_index.json`, carga los perfiles al cache para evitar recalcular:
```python
import json, os
INDEX_PATH = os.path.join(os.path.dirname(__file__), "spectral_index.json")
if os.path.exists(INDEX_PATH):
with open(INDEX_PATH) as fh:
data = json.load(fh)
for path, d in data.items():
self._cache[path] = SpectralProfile(path=path, **d)
logger.info(f"[SPECTRAL] Índice cargado: {len(self._cache)} samples")
```
### T022 — Añadir perfil espectral de referencia al genre profile reggaeton
**Archivo:** `sample_selector.py`, en `GENRE_PROFILES['reggaeton']`
**Acción:** Añade el campo `spectral_targets` al `GenreProfile` de reggaeton (requiere modificar la clase `GenreProfile` para aceptar el parámetro opcional):
```python
'reggaeton': GenreProfile(
name='Reggaeton',
bpm_range=(88, 98),
common_keys=['Dm', 'Am', 'Fm', 'Gm', 'Cm'],
drum_pattern='dembow',
bass_style='subby',
characteristics=['latin', 'syncopated', 'urban', 'percussive'],
# T022: spectral targets for reggaeton
# centroid_hz: kick~200, bass~400, perc~3000, hat~8000
spectral_targets={
'kick': {'centroid_range': (100, 400), 'flatness_max': 0.15},
'bass': {'centroid_range': (200, 800), 'flatness_max': 0.2},
'perc': {'centroid_range': (1500, 5000), 'flatness_max': 0.4},
'hat': {'centroid_range': (5000, 16000), 'flatness_max': 0.7},
}
)
```
### T023T030 — Integración espectral profunda en SampleSelector
**T023:** En `SampleSelector.select_for_role(role, genre, ...)`, después del score base, llama a `SpectralEngine` para penalizar samples espectralmente incompatibles con el género.
**T024:** Añade `spectral_coherence_score` al `SampleDecision.to_log_str()` para trackeabilidad.
**T025:** Si el género es `reggaeton` y el rol es `bass`, rechaza muestras con `centroid_mean > 1500 Hz` (serían demasiado brillantes para un bajo dembow).
**T026:** Si el género es `reggaeton` y el rol es `kick`, rechaza muestras con `duration > 1.5s` (los kicks de reggaeton son agresivos y cortos).
**T027:** Para `synth_loop` en reggaeton: prioriza samples con `spectral_flatness < 0.3` (tonal, no ruidoso).
**T028:** Para `top_loop` en reggaeton: acepta `spectral_flatness` hasta 0.6 (las percusiones lat. tienen algo de ruido).
**T029:** Añade log `[SPECTRAL_GATE]` cuando un sample es rechazado por criterios espectrales.
**T030:** Valida con `python -m pytest tests/test_sample_selector.py -v` que los tests existentes siguen pasando.
### T031T040 — Análisis espectral de referencia (reference_listener.py)
**T031:** En `reference_listener.py`, en `_compute_segment_features`, añade llamada a `SpectralEngine.analyze(reference_path)` y almacena el perfil en `self._reference_spectral_profile`.
**T032:** Expón `reference_spectral_profile` en el resultado de `analyze_reference()` como campo `spectral_profile`.
**T033:** En `server.py`, cuando se llama a `analyze_reference`, guarda el perfil espectral en una variable global `_reference_spectral_profile`.
**T034:** Al seleccionar samples para una generación con referencia, pasa `_reference_spectral_profile` al `SampleSelector` para que el score de similitud espectral use la referencia real, no targets genéricos.
**T035:** En `reference_listener.py`, calcula el `centroid_mean` del stem percusivo de la referencia y almacénalo como `reference_perc_centroid`.
**T036:** En `reference_listener.py`, calcula el `centroid_mean` del stem de bajo de la referencia y almacénalo como `reference_bass_centroid`.
**T037:** Expón `reference_perc_centroid` y `reference_bass_centroid` en el resultado de `analyze_reference`.
**T038:** Añade MCP tool `get_reference_spectral_targets() -> str` que retorna los targets espectrales detectados de la referencia activa.
**T039:** En `coherence_analyzer.py`, añade un nuevo metric `SpectralCoherenceMetric` que mide cuántos samples del manifest están dentro del rango espectral de la referencia.
**T040:** Añade `spectral_coherence` al `CoherenceReport.to_dict()`.
### T041T045 — Índice vectorial ligero para búsqueda por similitud
**T041:** En `spectral_engine.py`, añade método `build_similarity_matrix(paths: List[str]) -> np.ndarray` que calcula la matriz de similitud NxN entre todos los samples de la librería.
**T042:** Añade método `cluster_by_role(paths: List[str], n_clusters: int = 5) -> Dict[int, List[str]]` que agrupa samples en N familias tímbricas sin necesidad de scikit-learn (usa K-means manual con numpy).
**T043:** Ejecuta `build_similarity_matrix` sobre la carpeta `libreria/reggaeton/perc loop/` y guarda el resultado como `perc_loop_clusters.json`.
**T044:** En `sample_selector.py`, cuando el rol es `perc_loop` o `top_loop`, consulta `perc_loop_clusters.json` y fuerza que samples de la misma sesión vengan del mismo cluster tímbrico (coherencia de color percusivo).
**T045:** Añade test unitario `test_spectral_engine.py` con: test de creación sin librosa (análisis básico), test de similitud entre dos perfiles iguales (debe ser 1.0), test de similitud entre perfiles opuestos (debe ser < 0.3).
---
## BLOQUE C — REGGAETON ESPECÍFICO (T046T065)
### T046 — Actualizar GENRE_PROFILES['reggaeton'] en sample_selector.py
**Acción:** El perfil actual tiene `bpm_range=(88, 98)`. El proyecto usa 95 BPM. Cambia la descripción del `drum_pattern` a `'dembow_95bpm'` y añade `'moombahton'` como alias.
### T047 — Añadir perfil de género 'perreo' distinto de 'reggaeton'
**Archivo:** `sample_selector.py`
**Acción:** Agrega:
```python
'perreo': GenreProfile(
name='Perreo',
bpm_range=(90, 96),
common_keys=['Am', 'Dm', 'Gm'],
drum_pattern='dembow_hard',
bass_style='reese_sub',
characteristics=['dark', 'hard', 'urban', 'bass_heavy']
)
```
### T048 — Añadir a song_generator.py la progresión Am reggaeton canónica
**Archivo:** `song_generator.py`
**Acción:** Busca donde se definen progresiones de acordes por género. Añade para reggaeton:
```python
'reggaeton': {
'drop': ['Am', 'F', 'G', 'Em'], # clásico perreo
'break': ['Am', 'G', 'F', 'E'], # tensión
'intro': ['Am', 'F', 'C', 'G'], # suave
'build': ['Dm', 'Am', 'G', 'Am'], # sube
}
```
### T049 — Implementar dembow pattern correcto en drum grid
**Archivo:** `song_generator.py`
**Acción:** El patrón dembow estándar en una grilla de 16 corcheas (una barra de 4/4) es:
```
Kick: X . . . . . . X . X . . X . . . (1, 8, 10, 13)
Snare: . . . . X . . . . . . . . . . . (5)
Hat: X . X . X . X . X . X . X . X . (cada corchea par)
```
Verifica que el generador de patrones produce esta distribución para reggaeton/perreo. Si no, corrígela.
### T050 — Bass line dembow bouncy con slides
**Archivo:** `song_generator.py`
**Acción:** La línea de bajo dembow tiene "tumbao": nota en el 1, silencio, nota sincopada corta en el 2-y, nota en el 3. Verifica que `create_bassline(style='dembow')` ó `style='bouncy'` produce notas en posiciones `[0, 0.5, 1.5, 2, 2.5, 3]` (en beats dentro de la barra). Si no, corrígelo.
### T051 — Añadir variante de bajo 'reese_reggaeton'
**Archivo:** `song_generator.py`
**Acción:** El bajo Reese en reggaeton es un bajo distorsionado y subterráneo. Añade el estilo a la función de bajo con parámetros de nota más bajos (octava 1-2) y duración más larga (sostenida).
### T052 — Asegurar que section_aware selection prioriza dembow para drop
**Archivo:** `sample_selector.py`, `SECTION_ROLE_PROFILES['drop']`
**Acción:** Verifica que en drop, `perc_loop` está en `primary`. Añade `perc_alt` como rol secundario si no existe. Para reggaeton, el drop debe tener kick + perc loop dembow siempre activos.
### T053 — Implementar regla: no hay intro sin kick en reggaeton
**Archivo:** `song_generator.py` o `server.py`
**Acción:** Para género reggaeton, en el intro, el kick debe entrar desde el beat 0 (no desde el beat 16 como en techno). Añade guardia en la lógica de intro que force `kick_present=True` para reggaeton desde el inicio.
### T054 — Corrección de pitch: notas MIDI en clave Am
**Archivo:** `song_generator.py`, método de generación harmónica
**Acción:** Verifica que todas las notas generadas para reggaeton en Am corresponden a la escala Am natural: A(69), B(71), C(72), D(74), E(76), F(77), G(79). Si hay notas fuera de escala, añade un filtro `_quantize_to_scale(note, scale_notes)`.
### T055 — Añadir MCP tool: populate_harmony_track
**Archivo:** `server.py`
**Acción:**
```python
@mcp.tool()
async def populate_harmony_track(track_index: int = 15, key: str = "Am", bpm: float = 95.0) -> str:
"""Rellena el track MIDI harmónico con progresiones Am para reggaeton."""
PROGRESSION = [
(0, 32, [('A3',1.0),('C4',0.5),('E4',0.5)]), # Am
(32, 32, [('F3',1.0),('A3',0.5),('C4',0.5)]), # F
(64, 32, [('G3',1.0),('B3',0.5),('D4',0.5)]), # G
(96, 32, [('E3',1.0),('G3',0.5),('B3',0.5)]), # Em
(128, 32, [('A3',1.0),('C4',0.5),('E4',0.5)]), # Am repeat
(160, 32, [('F3',1.0),('A3',0.5),('C4',0.5)]), # F repeat
(192, 32, [('G3',1.0),('D4',1.0),('B3',0.5)]), # G build
(224, 32, [('A3',2.0),('E4',2.0)]), # Am outro
]
results = []
for start, length, notes in PROGRESSION:
r = ableton.send_command("create_arrangement_clip", {"track_index": track_index, "start_time": start, "length": length})
if not _is_error_response(r):
midi_notes = [{"pitch": _note_name_to_midi(n), "start_time": i*4.0, "duration": d*4.0, "velocity": 80} for i,(n,d) in enumerate(notes)]
ableton.send_command("add_notes_to_arrangement_clip", {"track_index": track_index, "start_time": start, "notes": midi_notes})
results.append(f"OK beat {start}")
else:
results.append(f"FAIL beat {start}: {r.get('message','')}")
return "\n".join(results)
```
### T056T065 — Más mejoras reggaeton específicas
**T056:** Añade `_note_name_to_midi(name: str) -> int` a server.py como helper que convierte "A3"→57, "C4"→60, etc.
**T057:** En `coherence_analyzer.py`, para reggaeton, baja el target de `max_harmonic_gap_beats` de 8 a 16 (es aceptable tener breaks de hasta 2 compases sin harmónicos en reggaeton).
**T058:** Añade `reggaeton_perc_density_score` al CoherenceReport: mide si hay perc loop en ≥70% del arrangement.
**T059:** En `sample_selector.py`, para reggaeton, la familia dominante debe ser del pack `Midilatino` o `SentimientoLatino` si están disponibles, no un pack genérico.
**T060:** Añade un guardia en `server.py`: si el género es `reggaeton` y se detecta que el tempo real difiere de 95 BPM ±3, emite un warning `[REGGAETON_WARNING] BPM fuera de rango estándar`.
**T061:** Para achoques de perc en reggaeton (Track 11/12), el clip óptimo es de 16 beats (4 compases), no 8. Actualiza la lógica de `create_arrangement_audio_pattern` para que reggaeton use `default_clip_length=16`.
**T062:** Añade `perreo_style_profile` en `reference_listener.py` que detecta si un audio de referencia tiene patrón dembow (sub 100Hz regular cada ~0.63s a 95 BPM) y setea `detected_style='perreo'`.
**T063:** Si `detected_style='perreo'`, en la selección de samples prioriza packs con keywords 'latin', 'urbano', 'perreo', 'reggaeton', 'dembow' en su path.
**T064:** Añade test: `test_reggaeton_coherence.py` que verifica que una generación reggaeton produce drum_coverage_ratio > 0.6.
**T065:** Actualiza `KIMI_K2_ACTIVE_HANDOFF.md` con el estado de los módulos nuevos (spectral_engine.py, populate_harmony_track tool).
---
## BLOQUE D — COHERENCIA Y DIVERSIDAD (T066T085)
### T066 — Forzar mismo-pack en reggaeton
**Archivo:** `sample_selector.py`
**Acción:** Añade método `force_pack_lock(pack_name: str)` que, cuando se llama, penaliza (score * 0.1) cualquier sample que no pertenezca al pack especificado. Llama a este método después de detectar el pack dominante en la primera selección.
### T067 — Anti-mirror: detectar secciones especulares
**Archivo:** `coherence_analyzer.py`
**Acción:** Añade `MirrorSectionMetric` que cuenta cuántos pares de secciones son idénticos (mismos samples en los mismos beats relativos). Target: `mirror_pairs < 4`. Escribe el métrodo `_count_mirror_pairs(manifest)`.
### T068 — Reducir repetición: sample cooldown entre sections
**Archivo:** `sample_selector.py`
**Acción:** Después de usar un sample en la sección `drop`, agrega el path a una cola `_section_cooldown_queue` con TTL de 2 secciones. En las siguientes 2 secciones, ese sample tiene penalización del 50%.
### T069 — Diversity check antes de confirmar sample
**Archivo:** `sample_selector.py`
**Acción:** Antes de confirmar una selección, verifica que el mismo sample no aparece más de `COOLDOWN_WINDOW=3` veces en el arrangement actual. Si lo hace, fuerza selección del siguiente candidato.
### T070 — Añadir campo 'section_kind' a todos los logs de selección
**Archivo:** `sample_selector.py`
**Acción:** Cada entry de log de `SampleDecision.to_log_str()` debe incluir el `section_kind` actual para poder trazar qué sample se usó en qué sección.
### T071 — Fix pack coherence: _extract_pack no considere carpetas genéricas
**Archivo:** `reference_listener.py` o `sample_selector.py`, método `_extract_pack`
**Acción:** Las carpetas `20 One Shots`, `loop`, `perc loop` son carpetas de categoría, no de pack. El pack debe extraerse del abuelo de la carpeta. Verifica la lógica y corrige si es necesario.
### T072T080 — Métricas de coherencia adicionales
**T072:** Añade `LayerCountBySection` a CoherenceReport: para cada sección, cuenta cuántos layers hay activos. Target: drop tiene más layers que break.
**T073:** Añade `BassPresenceRatio`: ratio de tiempo en que el bass está activo vs total. Target > 0.80 para reggaeton.
**T074:** Añade `KickPresenceRatio`: target > 0.65 para reggaeton.
**T075:** Añade `HatPresenceRatio`: target > 0.65 para reggaeton.
**T076:** Añade `EnergyArcScore`: mide si la energía (capas activas) sube del intro al drop y baja en el break. Target: `arc_score > 0.6`.
**T077:** Expón todas las nuevas métricas en `audit_project_coherence()` MCP tool.
**T078:** Actualiza `CoherenceReport.to_dict()` para incluir todas las nuevas métricas de T072T077.
**T079:** Escribe test unitario para cada nueva métrica de T072T077.
**T080:** Actualiza `roadmap.md` marcando los ítems completados de FASE 4 (Spectral Fingerprinting).
### T081T085 — Diversity Memory improvements
**T081:** En `diversity_memory.py`, añade persistencia de `spectral_family` además de `sample_family`. Cuando se registra un sample usado, guarda también su `centroid_bucket` (low/mid/high freq) para evitar repetición espectral inter-sesión.
**T082:** Añade método `get_spectral_penalty(centroid_bucket: str, role: str) -> float` que devuelve penalización si ese bucket ya fue usado recientemente para ese rol.
**T083:** En `sample_selector.py`, después de calcular score base, aplica `get_spectral_penalty` si diversity_memory está disponible.
**T084:** Añade `diversity_memory.export_stats() -> Dict` que retorna estadísticas de uso: top 5 familias usadas, top 5 cenroid_buckets usados.
**T085:** Expón las stats en un MCP tool `get_diversity_stats() -> str`.
---
## BLOQUE E — ARRANGEMENT INTELIGENTE (T086T100)
### T086 — Crear módulo arrangement_intelligence.py
**Archivo nuevo:** `arrangement_intelligence.py`
**Propósito:** Lógica de arrangement de nivel DJ para reggaeton.
```python
"""arrangement_intelligence.py — Lógica de arrangement para DJ profesional."""
REGGAETON_STRUCTURE_95BPM = {
'intro': {'start': 0, 'length': 32, 'energy': 0.3, 'layers': ['kick','hat','bass']},
'build_a':{'start': 32, 'length': 32, 'energy': 0.6, 'layers': ['kick','hat','clap','bass','perc_main']},
'drop_a': {'start': 64, 'length': 64, 'energy': 1.0, 'layers': ['kick','hat','clap','bass','perc_main','perc_alt','synth']},
'break': {'start': 128, 'length': 32, 'energy': 0.2, 'layers': ['bass','synth','atmos']},
'build_b':{'start': 160, 'length': 32, 'energy': 0.7, 'layers': ['kick','hat','clap','bass','perc_main','synth']},
'drop_b': {'start': 192, 'length': 64, 'energy': 1.0, 'layers': ['kick','hat','clap','bass','perc_main','perc_alt','synth','top_loop']},
'outro': {'start': 256, 'length': 32, 'energy': 0.2, 'layers': ['kick','hat','bass']},
}
```
### T087 — Añadir MCP tool: apply_reggaeton_structure
**Archivo:** `server.py`
**Acción:** Tool que aplica la estructura de T086 al proyecto activo: llama a MCP para verificar qué tracks existen, mapea roles a índices, y configura los clips para seguir la estructura.
### T088 — Implementar mute throws (silencio antes del drop)
**Archivo:** `server.py` o `arrangement_intelligence.py`
**Acción:** Para los beats 6164 (3 beats antes del drop_a) y beats 189192 (antes del drop_b), elimina o silencia los clips de kick, hat y clap. Esto crea el "pull-back" que hace que el drop golpee más fuerte.
### T089 — Implementar energy curve checker
**Archivo:** `arrangement_intelligence.py`
**Acción:** Método `check_energy_curve(track_clips: Dict[str, List]) -> float` que dado el mapa de clips por track, calcula la curva de energía (capas activas por cada 16 beats) y retorna un score de 01 indicando qué tan bien sigue la curva intro→build→drop→break→drop→outro.
### T090 — Añadir tool: audit_arrangement_structure
**Archivo:** `server.py`
**Acción:** Tool que llama a `get_tracks`, analiza los clips por sección y retorna un reporte de energía por sección, gaps detectados, y si la estructura está incompleta.
### T091T100 — Filling y patching de arrangement
**T091:** Para el track de harmónicos (índice 15), si tiene 0 arrangement clips, automáticamente ejecuta `populate_harmony_track` de T055.
**T092:** Para el track de top_loop (índice 12), si tiene gaps > 32 beats, rellena con el sample más frecuentemente usado en ese track.
**T093:** Para el track perc_alt (índice 11), si tiene gaps > 32 beats, rellena con alternancia de perc 1 y perc 2.
**T094:** Añade MCP tool `fill_arrangement_gaps(max_gap_beats: int = 32)` que ejecuta T091T093 automáticamente.
**T095:** En `coherence_analyzer.py`, detecta si hay más de 5 secciones especulares (mirror) y reporta en `redundant_layers`.
**T096:** Añade recomendación automática en el CoherenceReport: si `drum_coverage < 0.55`, sugiere ejecutar `fill_arrangement_gaps`.
**T097:** Añade recomendación: si `harmonic_coverage < 0.60`, sugiere ejecutar `populate_harmony_track`.
**T098:** Crea `docs/SPECTRAL_ENGINE_README.md` documentando cómo usar el motor espectral, cómo regenerar el índice, y cómo interpretar los resultados.
**T099:** Actualiza `AGENTS.md` con los nuevos módulos: `spectral_engine.py`, `arrangement_intelligence.py`, `build_spectral_index.py`.
**T100:** Ejecuta el smoke test completo y documenta los resultados en `docs/SPRINT_GRANULAR_PART1_VALIDATION.md`.