597 lines
35 KiB
Markdown
597 lines
35 KiB
Markdown
# 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`.
|