Files
ableton-mcp-ai/docs/GRANULAR_SPRINT_PART1_T001_T100.md

597 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.