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

35 KiB
Raw Blame History

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:

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:

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:

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:

"""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:

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:

@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:

@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.

#!/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:

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):

'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:

'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:

'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:

@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.

"""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.