# GRANULAR SPRINT PART 1 — Tareas T001–T100 ## 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 (T001–T015) ### T001 — Eliminar time.sleep del hilo Live **Archivo:** `abletonmcp_init.py` ~línea 1450–1477 **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 5–17 **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 T001–T010 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 T001–T013:** ```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 (T016–T045) ### 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.0–1.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 2–5 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}, } ) ``` ### T023–T030 — 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. ### T031–T040 — 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()`. ### T041–T045 — Í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 (T046–T065) ### 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) ``` ### T056–T065 — 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 (T066–T085) ### 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. ### T072–T080 — 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 T072–T077. **T079:** Escribe test unitario para cada nueva métrica de T072–T077. **T080:** Actualiza `roadmap.md` marcando los ítems completados de FASE 4 (Spectral Fingerprinting). ### T081–T085 — 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 (T086–T100) ### 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 61–64 (3 beats antes del drop_a) y beats 189–192 (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 0–1 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. ### T091–T100 — 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 T091–T093 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`.