940 lines
30 KiB
Markdown
940 lines
30 KiB
Markdown
# AbletonMCP-AI - Consolidado de Cambios v0.1.1 + v0.1.2
|
|
|
|
**Fecha**: 2026-03-30
|
|
**Agentes**: Kimi K2 (5 agentes desplegados por sprint)
|
|
**Total de sprints**: 2 (v0.1.1 y v0.1.2)
|
|
**Estado**: Código implementado ~85%, Validado parcialmente (~40% runtime verified)
|
|
|
|
---
|
|
|
|
## 📋 Resumen Ejecutivo
|
|
|
|
Este documento consolida todo el trabajo realizado en los sprints v0.1.1 y v0.1.2 del proyecto AbletonMCP-AI. Incluye:
|
|
|
|
- Todas las tareas completadas
|
|
- Archivos modificados con líneas específicas
|
|
- Código de cambios importantes
|
|
- Estado de validación
|
|
- Issues conocidos
|
|
- Próximos pasos recomendados
|
|
|
|
**Hallazgo clave**: El 80% del código estaba implementado pero sin validación runtime. El sprint v0.1.2 se enfocó en verificar la realidad vs. la documentación histórica.
|
|
|
|
---
|
|
|
|
## 🎯 Tareas Completadas
|
|
|
|
### Sprint v0.1.1 (5 tareas)
|
|
|
|
| # | Tarea | Estado | Archivos |
|
|
|---|-------|--------|----------|
|
|
| 1 | Arreglar `clear_all_tracks` | ✅ Implementado + ✅ Validado | `abletonmcp_init.py:2664-2698` |
|
|
| 2 | Backoff/retry/cache Z.ai | ✅ Implementado | `zai_judges.py` |
|
|
| 3 | Same-pack estricto atmos/vocal | ✅ Implementado | `sample_selector.py` |
|
|
| 4 | Groove extraction dembow | ✅ Implementado | `groove_extractor.py`, `audio_analyzer.py` |
|
|
| 5 | Smoke test async | ✅ Implementado | `temp\smoke_test_async.py` |
|
|
|
|
### Sprint v0.1.2 (5 tareas)
|
|
|
|
| # | Tarea | Estado | Archivos |
|
|
|---|-------|--------|----------|
|
|
| 1 | Validar clear_all_tracks runtime | ✅ Validado | `abletonmcp_init.py:529` (timeout fix) |
|
|
| 2 | End-to-end async real | ⚠️ Issue encontrado | `server.py` (blocking) |
|
|
| 3 | Expandir corpus groove | ✅ Expandido | `groove_extractor.py` (16 templates) |
|
|
| 4 | Selector por sección | ✅ Implementado | `sample_selector.py`, `pack_brain.py` |
|
|
| 5 | Documentación honesta | ✅ Actualizada | 3 archivos MD |
|
|
|
|
---
|
|
|
|
## 🔧 Cambios Detallados
|
|
|
|
### 1. clear_all_tracks - FIXED ✅
|
|
|
|
**Problema original**: Error blando "Couldn't delete track" al limpiar + timeout en sesiones grandes
|
|
|
|
**Solución aplicada**:
|
|
|
|
```python
|
|
# abletonmcp_init.py:529
|
|
# CAMBIO: Extender timeout para clear_all_tracks
|
|
if command_type in ("generate_track", "clear_all_tracks"):
|
|
timeout_seconds = 180.0 # Era solo 10s
|
|
else:
|
|
timeout_seconds = 10.0
|
|
```
|
|
|
|
```python
|
|
# abletonmcp_init.py:2664-2698
|
|
# _clear_all_tracks method - Lógica completa
|
|
|
|
def _clear_all_tracks(self, params):
|
|
"""Clear all tracks and leave exactly one empty track."""
|
|
tracks_deleted = 0
|
|
|
|
# Delete tracks from the end to avoid index shifting
|
|
while len(self._song.tracks) > 1:
|
|
track_idx = len(self._song.tracks) - 1
|
|
self._song.delete_track(track_idx)
|
|
tracks_deleted += 1
|
|
|
|
# Clear the remaining track (can't delete last one)
|
|
if len(self._song.tracks) == 1:
|
|
track = self._song.tracks[0]
|
|
|
|
# Clear all clip slots
|
|
if hasattr(track, 'clip_slots'):
|
|
for slot in track.clip_slots:
|
|
if slot.has_clip:
|
|
slot.delete_clip()
|
|
|
|
# Remove all devices
|
|
if hasattr(track, 'devices'):
|
|
while len(track.devices) > 0:
|
|
track.delete_device(0)
|
|
|
|
# Reset name and color
|
|
track.name = "1-MIDI"
|
|
if hasattr(track, 'color'):
|
|
track.color = 0
|
|
|
|
return {
|
|
"status": "success",
|
|
"tracks_deleted": tracks_deleted,
|
|
"message": f"Cleared {tracks_deleted} tracks, left 1 empty track"
|
|
}
|
|
```
|
|
|
|
**Validación**:
|
|
- ✅ 3 limpiezas consecutivas sin crash
|
|
- ✅ Sesiones de 16+ tracks limpiadas correctamente
|
|
- ✅ No más timeout en sesiones grandes
|
|
- ✅ `get_session_info` devuelve consistentemente 1 track
|
|
|
|
---
|
|
|
|
### 2. Z.ai Backoff/Retry/Cache - IMPLEMENTADO ✅
|
|
|
|
**Archivo**: `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/zai_judges.py`
|
|
|
|
**Configuración**:
|
|
```python
|
|
# zai_judges.py:29-34
|
|
CACHE_TTL_SECONDS = 300 # 5 minutos
|
|
MAX_RETRIES = 3
|
|
BACKOFF_DELAYS = [1.0, 2.0, 4.0] # Exponencial
|
|
```
|
|
|
|
**Cache con SHA256**:
|
|
```python
|
|
# zai_judges.py:37-53
|
|
def _generate_cache_key(self, system_prompt: str, payload: Dict) -> str:
|
|
"""Generate cache key from prompt and payload."""
|
|
cache_data = {
|
|
"prompt_prefix": system_prompt[:200],
|
|
"genre": payload.get("genre", ""),
|
|
"style": payload.get("style", ""),
|
|
"bpm": payload.get("bpm", 0),
|
|
"key": payload.get("key", ""),
|
|
"judge_role": payload.get("judge_role", ""),
|
|
"candidates": [c.get("id", "") for c in payload.get("candidates", [])[:4]]
|
|
}
|
|
json_str = json.dumps(cache_data, sort_keys=True)
|
|
return hashlib.sha256(json_str.encode()).hexdigest()
|
|
```
|
|
|
|
**Retry loop con backoff**:
|
|
```python
|
|
# zai_judges.py:155-205
|
|
def _call(self, system_prompt: str, payload: Dict) -> Dict:
|
|
"""Call Z.ai API with retry and cache."""
|
|
cache_key = self._generate_cache_key(system_prompt, payload)
|
|
|
|
# Check cache first
|
|
cached_result = self._get_cached_result(cache_key)
|
|
if cached_result is not None:
|
|
logger.debug(f"Cache hit for key: {cache_key[:8]}...")
|
|
return cached_result
|
|
|
|
# Try API with retries
|
|
for attempt in range(1, MAX_RETRIES + 1):
|
|
try:
|
|
response = self._make_api_call(system_prompt, payload)
|
|
self._set_cached_result(cache_key, response)
|
|
return response
|
|
|
|
except HTTPError as e:
|
|
if e.code == 429:
|
|
if attempt < MAX_RETRIES:
|
|
delay = BACKOFF_DELAYS[attempt - 1]
|
|
logger.warning(f"Judge API 429 on attempt {attempt}/{MAX_RETRIES}, retrying in {delay}s...")
|
|
time.sleep(delay)
|
|
continue
|
|
raise
|
|
except (URLError, TimeoutError) as e:
|
|
if attempt < MAX_RETRIES:
|
|
delay = BACKOFF_DELAYS[attempt - 1]
|
|
logger.warning(f"Judge API error on attempt {attempt}: {e}, retrying...")
|
|
time.sleep(delay)
|
|
continue
|
|
raise
|
|
|
|
return {} # Fallback empty
|
|
```
|
|
|
|
**Fallback heurístico**:
|
|
```python
|
|
# zai_judges.py:225-242
|
|
def judge_palette_candidates(self, candidates: List[Dict], context: Dict) -> Dict:
|
|
"""Judge palette candidates with API or heuristic fallback."""
|
|
try:
|
|
result = self._call(system_prompt, payload)
|
|
if not result:
|
|
# API failed - use heuristic fallback
|
|
logger.warning("Z.ai judges failed, using heuristic fallback")
|
|
return {
|
|
"mode": "heuristic_fallback",
|
|
"selected": candidates[0] if candidates else None,
|
|
"directives": {
|
|
"rhythm_density": "moderate",
|
|
"bass_motion": "rolling",
|
|
"arrangement_emphasis": "balanced",
|
|
"vocal_strategy": "sparse"
|
|
}
|
|
}
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Judge panel failed: {e}")
|
|
return {"mode": "error", "selected": candidates[0] if candidates else None}
|
|
```
|
|
|
|
**Estado**: Implementado, necesita validación contra API real con 429
|
|
|
|
---
|
|
|
|
### 3. Same-Pack Selection - IMPLEMENTADO ✅
|
|
|
|
**Archivo**: `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/sample_selector.py`
|
|
|
|
**Roles con same-pack estricto**:
|
|
```python
|
|
# sample_selector.py:1222-1243
|
|
SAME_PACK_STRICT_ROLES = [
|
|
'atmos_fx', # Atmósferas
|
|
'vocal_shot', # Vocales one-shot
|
|
'fill_fx', # FX de transición (NUEVO v0.1.2)
|
|
'snare_roll' # Redobles (NUEVO v0.1.2)
|
|
]
|
|
```
|
|
|
|
**Bonus/penalty system**:
|
|
```python
|
|
# sample_selector.py:1578-1632
|
|
def _calculate_same_pack_strict_bonus(
|
|
self,
|
|
sample_path: str,
|
|
main_pack_folders: List[str]
|
|
) -> Tuple[float, str, str]:
|
|
"""
|
|
Calculate bonus for selecting from same pack.
|
|
|
|
Returns:
|
|
(bonus_multiplier, selection_type, reason)
|
|
"""
|
|
if not main_pack_folders:
|
|
return 1.0, "neutral", "No main pack context"
|
|
|
|
sample_folder = os.path.dirname(sample_path)
|
|
sample_parts = Path(sample_folder).parts
|
|
|
|
for main_folder in main_pack_folders:
|
|
main_parts = Path(main_folder).parts
|
|
|
|
# Check relationships
|
|
if sample_folder == main_folder:
|
|
return 2.0, "same_pack", "Exact folder match"
|
|
|
|
if sample_folder.startswith(main_folder + os.sep):
|
|
return 1.8, "same_pack", "Subfolder of main pack"
|
|
|
|
# Check if same parent (sibling folders)
|
|
if len(sample_parts) > 1 and len(main_parts) > 1:
|
|
if sample_parts[-2] == main_parts[-2]:
|
|
return 1.5, "same_parent", "Sibling folder (same parent)"
|
|
|
|
# Check if same grandparent (cousin folders)
|
|
if len(sample_parts) > 2 and len(main_parts) > 2:
|
|
if sample_parts[-3] == main_parts[-3]:
|
|
return 1.3, "same_grandparent", "Cousin folder (shared grandparent)"
|
|
|
|
# Different pack - penalty
|
|
return 0.4, "fallback", "Cross-pack selection"
|
|
```
|
|
|
|
**Section-aware selection** (NUEVO v0.1.2):
|
|
```python
|
|
# sample_selector.py:750-806
|
|
SECTION_ROLE_PROFILES = {
|
|
'intro': {
|
|
'primary': ['kick', 'hat', 'atmos_fx', 'pad', 'bass_loop'],
|
|
'secondary': ['clap', 'synth_loop', 'vocal_shot'],
|
|
'avoid': ['snare_roll', 'fill_fx', 'crash_fx', 'vocal_loop'],
|
|
'intensity': 'low',
|
|
},
|
|
'build': {
|
|
'primary': ['kick', 'hat', 'snare_roll', 'fill_fx', 'synth_loop', 'bass_loop'],
|
|
'secondary': ['clap', 'atmos_fx', 'vocal_shot'],
|
|
'avoid': ['vocal_loop', 'pad'],
|
|
'intensity': 'rising',
|
|
},
|
|
'drop': {
|
|
'primary': ['kick', 'clap', 'hat', 'bass_loop', 'synth_loop', 'vocal_shot'],
|
|
'secondary': ['snare_roll', 'atmos_fx'],
|
|
'avoid': ['pad', 'vocal_loop'],
|
|
'intensity': 'high',
|
|
},
|
|
'break': {
|
|
'primary': ['atmos_fx', 'pad', 'vocal_loop', 'vocal_shot'],
|
|
'secondary': ['hat', 'synth_loop'],
|
|
'avoid': ['kick', 'clap', 'snare_roll'],
|
|
'intensity': 'low',
|
|
},
|
|
'outro': {
|
|
'primary': ['kick', 'hat', 'atmos_fx', 'pad'],
|
|
'secondary': ['clap', 'synth_loop'],
|
|
'avoid': ['snare_roll', 'fill_fx', 'crash_fx', 'vocal_loop'],
|
|
'intensity': 'low',
|
|
}
|
|
}
|
|
```
|
|
|
|
**Joint scoring** (NUEVO v0.1.2):
|
|
```python
|
|
# sample_selector.py:807-820
|
|
JOINT_SCORING_GROUPS = {
|
|
'drum_kit': ['kick', 'snare', 'clap', 'hat', 'hat_closed', 'hat_open'],
|
|
'music_group': ['bass_loop', 'synth_loop', 'pad', 'lead', 'chord'],
|
|
'vocal_fx_group': ['vocal_loop', 'vocal_shot', 'atmos_fx', 'fill_fx'],
|
|
'transition_group': ['fill_fx', 'snare_roll', 'crash_fx'],
|
|
}
|
|
|
|
FOLDER_COMPATIBILITY_BONUS = {
|
|
'exact_same': 1.5,
|
|
'same_parent': 1.3,
|
|
'same_grandparent': 1.15,
|
|
'different': 0.85,
|
|
}
|
|
```
|
|
|
|
**Estado**: Implementado, necesita prueba en generación real
|
|
|
|
---
|
|
|
|
### 4. Groove Extractor - IMPLEMENTADO ✅
|
|
|
|
**Archivo**: `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/groove_extractor.py` (663 líneas)
|
|
|
|
**Escaneo recursivo** (v0.1.2):
|
|
```python
|
|
# groove_extractor.py:65-105
|
|
class DembowGrooveExtractor:
|
|
"""Extract groove templates from dembow drum loops."""
|
|
|
|
SCAN_DIRS = ['drumloops', 'perc loop', 'oneshots']
|
|
|
|
IGNORED_FOLDERS = {
|
|
'.sample_cache', '.segment_rag', '.git',
|
|
'trash', 'recycle', 'deleted', '__pycache__'
|
|
}
|
|
|
|
IGNORED_EXTENSIONS = {'.json', '.txt', '.md', '.doc', '.docx'}
|
|
|
|
def scan_library(self, library_path: str) -> List[str]:
|
|
"""Recursively scan for drum loops."""
|
|
audio_files = []
|
|
lib_path = Path(library_path)
|
|
|
|
for subdir in self.SCAN_DIRS:
|
|
subdir_path = lib_path / subdir
|
|
if not subdir_path.exists():
|
|
continue
|
|
|
|
# Recursive scan with rglob
|
|
for audio_file in subdir_path.rglob('*.wav'):
|
|
# Skip hidden and ignored
|
|
if any(part.startswith('.') for part in audio_file.parts):
|
|
continue
|
|
if any(ignored in audio_file.parts for ignored in self.IGNORED_FOLDERS):
|
|
continue
|
|
|
|
audio_files.append(str(audio_file))
|
|
|
|
return audio_files
|
|
```
|
|
|
|
**Estructura de template**:
|
|
```python
|
|
# groove_extractor.py:40-62
|
|
@dataclass
|
|
class GrooveTemplate:
|
|
source_file: str
|
|
bpm: float
|
|
kick_positions: List[float] # 0-4 beats
|
|
snare_positions: List[float]
|
|
hat_positions: List[float]
|
|
kick_velocities: List[float] # 0.0-1.0
|
|
snare_velocities: List[float]
|
|
hat_velocities: List[float]
|
|
timing_variance_ms: float
|
|
density: float
|
|
style: str = "dembow"
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
'source_file': self.source_file,
|
|
'bpm': self.bpm,
|
|
'kick_positions': self.kick_positions,
|
|
# ... etc
|
|
}
|
|
```
|
|
|
|
**Detección de transientes**:
|
|
```python
|
|
# audio_analyzer.py:180-220
|
|
def _detect_transients_librosa(self, audio: np.ndarray, sr: int) -> np.ndarray:
|
|
"""Detect transient positions using librosa onset detection."""
|
|
# Onset envelope
|
|
onset_env = librosa.onset.onset_strength(
|
|
y=audio,
|
|
sr=sr,
|
|
hop_length=512
|
|
)
|
|
|
|
# Peak picking
|
|
onset_frames = librosa.util.peak_pick(
|
|
onset_env,
|
|
pre_max=3,
|
|
post_max=3,
|
|
pre_avg=3,
|
|
post_avg=3,
|
|
delta=0.5,
|
|
wait=3
|
|
)
|
|
|
|
# Convert to timestamps
|
|
onset_times = librosa.frames_to_time(onset_frames, sr=sr, hop_length=512)
|
|
|
|
# Filter by energy (RMS)
|
|
onset_times = self._filter_by_energy(audio, sr, onset_times)
|
|
|
|
return onset_times
|
|
```
|
|
|
|
**Resultados**:
|
|
- **v0.1.1**: 11 templates (solo drumloops/*.wav)
|
|
- **v0.1.2**: 16 templates (76 archivos escaneados recursivamente)
|
|
- Cache: `~/.abletonmcp_ai/dembow_groove_templates.json`
|
|
|
|
**Estado**: Implementado y expandido, probado con librería real
|
|
|
|
---
|
|
|
|
### 5. Async Infrastructure - IMPLEMENTADO ⚠️ CON ISSUE
|
|
|
|
**Archivo**: `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/server.py`
|
|
|
|
**4 Tools MCP expuestas**:
|
|
```python
|
|
# server.py:6503-6614
|
|
|
|
@mcp.tool()
|
|
async def generate_track_async(
|
|
genre: str,
|
|
style: str = "",
|
|
bpm: int = 0,
|
|
key: str = "",
|
|
structure: str = "standard"
|
|
) -> str:
|
|
"""Generate a track asynchronously."""
|
|
job_id = _submit_generation_job(
|
|
job_type="track",
|
|
params={"genre": genre, "style": style, "bpm": bpm, "key": key, "structure": structure}
|
|
)
|
|
return json.dumps({
|
|
"status": "queued",
|
|
"job_id": job_id,
|
|
"message": "Track generation queued"
|
|
})
|
|
|
|
@mcp.tool()
|
|
async def generate_song_async(
|
|
genre: str,
|
|
style: str = "",
|
|
bpm: int = 0,
|
|
key: str = "",
|
|
structure: str = "standard",
|
|
auto_play: bool = True,
|
|
apply_automation: bool = True
|
|
) -> str:
|
|
"""Generate a full song asynchronously."""
|
|
job_id = _submit_generation_job(
|
|
job_type="song",
|
|
params={...}
|
|
)
|
|
return json.dumps({
|
|
"status": "queued",
|
|
"job_id": job_id,
|
|
"message": "Song generation queued"
|
|
})
|
|
|
|
@mcp.tool()
|
|
async def get_generation_job_status(job_id: str) -> str:
|
|
"""Get status of a generation job."""
|
|
with _generation_job_lock:
|
|
job = _generation_jobs.get(job_id)
|
|
if not job:
|
|
return json.dumps({"status": "not_found", "job_id": job_id})
|
|
|
|
return json.dumps({
|
|
"status": job["status"],
|
|
"job_id": job_id,
|
|
"result": job.get("result"),
|
|
"error": job.get("error"),
|
|
"future_done": job["future"].done() if job.get("future") else False
|
|
})
|
|
|
|
@mcp.tool()
|
|
async def cancel_generation_job(job_id: str) -> str:
|
|
"""Cancel a queued or running generation job."""
|
|
with _generation_job_lock:
|
|
job = _generation_jobs.get(job_id)
|
|
if not job:
|
|
return json.dumps({"status": "not_found", "job_id": job_id})
|
|
|
|
if job["status"] == "queued":
|
|
job["status"] = "cancelled"
|
|
return json.dumps({"status": "cancelled", "job_id": job_id})
|
|
|
|
return json.dumps({
|
|
"status": "cannot_cancel",
|
|
"job_id": job_id,
|
|
"current_status": job["status"]
|
|
})
|
|
```
|
|
|
|
**Infrastructure interna**:
|
|
```python
|
|
# server.py:4734-5101
|
|
|
|
# Global state
|
|
_generation_jobs: Dict[str, Any] = {}
|
|
_generation_job_lock = threading.RLock()
|
|
|
|
# Thread pool for async jobs
|
|
_generation_executor = ThreadPoolExecutor(max_workers=2)
|
|
|
|
def _submit_generation_job(job_type: str, params: Dict) -> str:
|
|
"""Submit a generation job to the thread pool."""
|
|
job_id = str(uuid.uuid4())[:12]
|
|
|
|
with _generation_job_lock:
|
|
_generation_jobs[job_id] = {
|
|
"job_id": job_id,
|
|
"type": job_type,
|
|
"status": "queued",
|
|
"params": params,
|
|
"result": None,
|
|
"error": None,
|
|
"created_at": time.time()
|
|
}
|
|
|
|
# Submit to thread pool
|
|
future = _generation_executor.submit(_run_generation_job, job_id, job_type, params)
|
|
|
|
with _generation_job_lock:
|
|
_generation_jobs[job_id]["future"] = future
|
|
_generation_jobs[job_id]["status"] = "running"
|
|
|
|
return job_id
|
|
|
|
def _run_generation_job(job_id: str, job_type: str, params: Dict):
|
|
"""Actually run the generation job."""
|
|
try:
|
|
if job_type == "track":
|
|
result = _generate_track_internal(params)
|
|
else:
|
|
result = _generate_song_internal(params)
|
|
|
|
with _generation_job_lock:
|
|
_generation_jobs[job_id]["status"] = "completed"
|
|
_generation_jobs[job_id]["result"] = result
|
|
|
|
except Exception as e:
|
|
with _generation_job_lock:
|
|
_generation_jobs[job_id]["status"] = "failed"
|
|
_generation_jobs[job_id]["error"] = str(e)
|
|
```
|
|
|
|
**⚠️ ISSUE CRÍTICO ENCONTRADO**:
|
|
|
|
**Problema**: El servidor MCP se bloquea completamente durante la generación
|
|
|
|
**Síntomas**:
|
|
1. Job se encola correctamente (status: "queued")
|
|
2. Job cambia a "running"
|
|
3. Servidor deja de responder a cualquier comando MCP
|
|
4. `get_generation_job_status` timeout
|
|
5. Después de 10+ minutos, servidor crashea
|
|
|
|
**Logs de error**:
|
|
```
|
|
MCP error -32001: Request timed out
|
|
Connection closed
|
|
[WinError 10054] An existing connection was forcibly closed
|
|
```
|
|
|
|
**Causa root**: ThreadPoolExecutor no libera el GIL de Python durante la generación, bloqueando todo el servidor MCP.
|
|
|
|
**Posibles soluciones**:
|
|
1. Usar `multiprocessing.Process` en vez de `ThreadPoolExecutor`
|
|
2. Añadir `asyncio` con `run_in_executor` y checkpoints
|
|
3. Separar el job runner en proceso independiente con queue
|
|
4. Usar `fastapi` o similar para endpoint de status separado
|
|
|
|
---
|
|
|
|
### 6. Smoke Test - IMPLEMENTADO ⚠️ CON ISSUE
|
|
|
|
**Archivo**: `temp\smoke_test_async.py` (547 líneas)
|
|
|
|
**Estructura**:
|
|
```python
|
|
class MCPServerClient:
|
|
"""Client to invoke MCP tools directly from server.py."""
|
|
|
|
def __init__(self):
|
|
self.server_module = self._load_server()
|
|
|
|
def _load_server(self):
|
|
spec = importlib.util.spec_from_file_location(
|
|
"server",
|
|
r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\server.py"
|
|
)
|
|
server = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(server)
|
|
return server
|
|
|
|
async def generate_song_async(self, **kwargs):
|
|
return await self.server_module.generate_song_async(**kwargs)
|
|
|
|
async def get_generation_job_status(self, job_id):
|
|
return await self.server_module.get_generation_job_status(job_id)
|
|
|
|
class SmokeTest:
|
|
"""End-to-end smoke test for async generation."""
|
|
|
|
async def run(self):
|
|
# 1. Test connection
|
|
# 2. Launch async job
|
|
# 3. Poll status
|
|
# 4. Verify tracks
|
|
# 5. Check manifest
|
|
pass
|
|
```
|
|
|
|
**Uso**:
|
|
```powershell
|
|
# Test básico
|
|
python temp\smoke_test_async.py
|
|
|
|
# Con opciones
|
|
python temp\smoke_test_async.py --use-track --genre tech-house --poll-interval 2
|
|
|
|
# Con reporte JSON
|
|
python temp\smoke_test_async.py --save-report report.json --json
|
|
```
|
|
|
|
**⚠️ Issue encontrado**:
|
|
El smoke test carga server.py mediante `importlib.util.spec_from_file_location()`, lo que crea una instancia de módulo separada. Esto significa que el diccionario global `_generation_jobs` no es compartido entre la llamada de submit y la de status check.
|
|
|
|
**Fix necesario**: Usar una sola instancia del cliente MCP o usar el socket directo de Live para status.
|
|
|
|
---
|
|
|
|
## 📁 Archivos Tocados
|
|
|
|
### Archivos Modificados (8):
|
|
|
|
| Archivo | Líneas | Cambios |
|
|
|---------|--------|---------|
|
|
| `abletonmcp_init.py` | 47 | Timeout fix para clear_all_tracks, método _clear_all_tracks |
|
|
| `sample_selector.py` | ~300 | Same-pack strict, section-aware, joint scoring |
|
|
| `pack_brain.py` | ~150 | Folder compatibility methods |
|
|
| `groove_extractor.py` | 663 | Nuevo módulo + expansión recursiva |
|
|
| `audio_analyzer.py` | 43 | Transient detection para groove |
|
|
| `song_generator.py` | 89 | Aplicación de groove en patrones |
|
|
| `server.py` | ~200 | 4 tools async, infrastructure |
|
|
| `zai_judges.py` | 362 | Nuevo módulo, retry/cache |
|
|
|
|
### Archivos Creados (3):
|
|
|
|
| Archivo | Líneas | Propósito |
|
|
|---------|--------|-----------|
|
|
| `temp\smoke_test_async.py` | 547 | Test suite end-to-end |
|
|
| `docs/SPRINT_v0.1.2_CHANGES.md` | 293 | Documentación de realidad |
|
|
| `docs/SPRINT_v0.1.1_CHANGES.md` | 297 | Resumen v0.1.1 |
|
|
|
|
### Archivos de Documentación Actualizados (3):
|
|
|
|
| Archivo | Cambios |
|
|
|---------|---------|
|
|
| `KIMI_K2_ACTIVE_HANDOFF.md` | Estado real verificado |
|
|
| `docs/SPRINT_v0.1.2_NEXT.md` | Sprint activo actualizado |
|
|
| `docs/ROADMAP.md` | Referencia canonical |
|
|
|
|
---
|
|
|
|
## ✅ Validaciones Realizadas
|
|
|
|
### Compilación
|
|
```powershell
|
|
✅ python -m py_compile "abletonmcp_init.py"
|
|
✅ python -m py_compile "AbletonMCP_AI/AbletonMCP_AI/MCP_Server/server.py"
|
|
✅ python -m py_compile "AbletonMCP_AI/AbletonMCP_AI/MCP_Server/zai_judges.py"
|
|
✅ python -m py_compile "AbletonMCP_AI/AbletonMCP_AI/MCP_Server/sample_selector.py"
|
|
✅ python -m py_compile "AbletonMCP_AI/AbletonMCP_AI/MCP_Server/pack_brain.py"
|
|
✅ python -m py_compile "AbletonMCP_AI/AbletonMCP_AI/MCP_Server/groove_extractor.py"
|
|
✅ python -m py_compile "temp\smoke_test_async.py"
|
|
```
|
|
|
|
### Validación Runtime
|
|
| Componente | Estado | Detalle |
|
|
|------------|--------|---------|
|
|
| clear_all_tracks | ✅ VALIDADO | 3/3 tests pasaron en Live |
|
|
| async job queuing | ✅ VALIDADO | Jobs se encolan correctamente |
|
|
| async status polling | ⚠️ PARCIAL | Funciona pero server bloquea |
|
|
| groove extraction | ✅ VALIDADO | 16 templates de librería real |
|
|
| same-pack selection | ⚠️ SIN VALIDAR | Código listo, falta generación real |
|
|
| Z.ai retry/cache | ⚠️ SIN VALIDAR | Código listo, falta test con 429 |
|
|
|
|
---
|
|
|
|
## ⚠️ Issues Conocidos
|
|
|
|
### Críticos
|
|
|
|
1. **Server MCP se bloquea durante generación async**
|
|
- **Impacto**: Clientes no pueden consultar status, timeout
|
|
- **Causa**: ThreadPoolExecutor mantiene GIL
|
|
- **Workaround**: Ninguno, necesita fix
|
|
- **Prioridad**: ALTA
|
|
|
|
2. **Smoke test module isolation**
|
|
- **Impacto**: "Job not found" en primer poll
|
|
- **Causa**: `_generation_jobs` no compartido entre instancias
|
|
- **Fix**: Usar socket directo o singleton
|
|
- **Prioridad**: MEDIA
|
|
|
|
3. **BPM detection en loops**
|
|
- **Impacto**: Todos los templates muestran 95.0 BPM
|
|
- **Causa**: librosa clasifica loops como one-shots
|
|
- **Fix**: Mejorar algoritmo o usar metadata
|
|
- **Prioridad**: BAJA
|
|
|
|
### Importantes
|
|
|
|
4. **clear_all_tracks error blando**
|
|
- **Impacto**: Mensaje "Couldn't delete track" al final (aunque funciona)
|
|
- **Estado**: Fix de timeout aplicado, error puede persistir en logs
|
|
- **Prioridad**: BAJA
|
|
|
|
5. **Async generation toma 10+ minutos**
|
|
- **Impacto**: Tests timeout antes de completar
|
|
- **Causa**: Generación heavy + server blocking
|
|
- **Workaround**: Necesita fix del blocking
|
|
- **Prioridad**: ALTA
|
|
|
|
---
|
|
|
|
## 🎯 Próximos Pasos Recomendados
|
|
|
|
### URGENTE - Fix Server Blocking
|
|
|
|
**Opción A: Multiprocessing** (Recomendado)
|
|
```python
|
|
# En lugar de ThreadPoolExecutor
|
|
from multiprocessing import Process, Queue
|
|
|
|
def _submit_generation_job(job_type, params):
|
|
job_id = generate_uuid()
|
|
queue = Queue()
|
|
process = Process(
|
|
target=_run_generation_in_process,
|
|
args=(job_id, job_type, params, queue)
|
|
)
|
|
process.start()
|
|
|
|
# Main process sigue libre para responder MCP
|
|
return job_id
|
|
```
|
|
|
|
**Opción B: Asyncio con checkpoints**
|
|
```python
|
|
async def _generate_with_checkpoints(params):
|
|
for section in ['intro', 'build', 'drop', 'break', 'outro']:
|
|
await generate_section(section)
|
|
await asyncio.sleep(0.1) # Yield control
|
|
```
|
|
|
|
**Opción C: Servidor de jobs separado**
|
|
- Crear `job_runner.py` como proceso independiente
|
|
- Comunicación via socket o archivo
|
|
- MCP server solo orquesta, no genera
|
|
|
|
### Media Prioridad
|
|
|
|
6. **Validar same-pack selection**
|
|
- Generar track y inspeccionar logs
|
|
- Verificar fill_fx/snare_roll vienen de pack principal
|
|
|
|
7. **Validar Z.ai retry**
|
|
- Probar contra API real
|
|
- Forzar 429 si es posible (rate limiting)
|
|
|
|
8. **Fix smoke test**
|
|
- Usar socket directo de Live (127.0.0.1:9877)
|
|
- O mantener singleton del server module
|
|
|
|
### Baja Prioridad
|
|
|
|
9. **Mejorar BPM detection**
|
|
- Usar tempo detection más robusto
|
|
- O parsear BPM del filename
|
|
|
|
10. **Documentar groove templates**
|
|
- Listar todos los templates extraídos
|
|
- Documentar qué loops son mejores
|
|
|
|
---
|
|
|
|
## 📊 Métricas Finales
|
|
|
|
```
|
|
Tareas implementadas: 9/10 (90%)
|
|
Tareas validadas: 4/10 (40%)
|
|
Archivos compilables: 11/11 (100%)
|
|
Issues críticos: 1
|
|
Issues totales: 5
|
|
Líneas de código nuevas: ~2000
|
|
Tests creados: 1 (smoke_test_async.py)
|
|
Documentación creada: 3 archivos MD
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Referencias
|
|
|
|
### Entrypoints Críticos
|
|
- MCP Server: `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/server.py`
|
|
- Runtime Live: `abletonmcp_init.py`
|
|
- Wrapper: `mcp_wrapper.py`
|
|
- Shim: `AbletonMCP_AI/__init__.py`
|
|
|
|
### Documentación
|
|
- `KIMI_K2_BOOTSTRAP.md` - Orden de lectura para nuevos agentes
|
|
- `KIMI_K2_ACTIVE_HANDOFF.md` - Estado actual verificado
|
|
- `CLAUDE.md` - Reglas del proyecto
|
|
- `docs/ROADMAP.md` - Roadmap canonical
|
|
- `docs/SPRINT_v0.1.2_NEXT.md` - Sprint activo
|
|
- `docs/KNOWN_ISSUES.md` - Issues conocidos
|
|
|
|
### Comandos Útiles
|
|
```powershell
|
|
# Compilar
|
|
python -m py_compile "abletonmcp_init.py"
|
|
|
|
# Ver logs Ableton
|
|
Get-Content "$env:APPDATA\Ableton\Live 12.0.15\Preferences\Log.txt" -Tail 100
|
|
|
|
# Ver puerto
|
|
netstat -an | findstr 9877
|
|
|
|
# Correr smoke test
|
|
python temp\smoke_test_async.py --use-track --genre tech-house
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Notas para Codex
|
|
|
|
1. **No confíes ciegamente en docs históricos**: Siempre verificar con código real primero
|
|
2. **Separar implementación de validación**: El código puede estar listo pero sin probar en vivo
|
|
3. **Server blocking es el issue más crítico**: Arreglar esto primero antes de más features
|
|
4. **Usar PowerShell en Windows**: No bash, rutas absolutas Windows
|
|
5. **Validar con runtime**: `get_session_info`, `get_tracks`, logs de Ableton
|
|
6. **El puerto 9877 escucha**: Pero eso no significa que todo funcione
|
|
|
|
---
|
|
|
|
**Documento creado por**: Kimi K2 (opencode)
|
|
**Para**: Codex / Próximo agente
|
|
**Fecha**: 2026-03-30
|
|
**Estado**: Listo para handoff con Reality Check incluido
|
|
|
|
---
|
|
|
|
## Reality Check (Added 2026-03-30)
|
|
|
|
### Claims vs Reality
|
|
|
|
| Claim | Reality | Status |
|
|
|-------|---------|--------|
|
|
| "Código implementado 100%" | Code exists but not all wired to real flow | PARTIAL (85% wired) |
|
|
| Section-aware selection works | Code exists in `sample_selector.py` but not called from server.py during generation | NOT WIRED |
|
|
| Joint scoring (drum kit coherence) | `JOINT_SCORING_GROUPS` defined but selections not recorded, joint scoring not applied | NOT WIRED |
|
|
| `record_section_selection` | Method exists but never called | DEAD CODE |
|
|
| `section_context` tracking | `SECTION_ROLE_PROFILES` exists but section context never set | NOT WIRED |
|
|
| Async jobs work | Infrastructure exists but server blocks during generation | ISSUE FOUND |
|
|
| Same-pack strict selection | Code ready but not validated in real generation | UNVALIDATED |
|
|
| Z.ai retry/cache | Implemented but not tested against real 429s | UNVALIDATED |
|
|
| Groove extractor | Implemented and tested with real library | ✅ WORKS |
|
|
| clear_all_tracks | Implemented and validated in Live | ✅ WORKS |
|
|
|
|
### What's Actually True
|
|
|
|
- ✅ **clear_all_tracks**: Implemented and validated in Live 3/3 times
|
|
- ✅ **Z.ai retry/cache infrastructure**: Implemented with exponential backoff
|
|
- ✅ **Groove extractor**: 16 templates extracted from real library
|
|
- ✅ **Async job queuing**: Jobs queue correctly
|
|
- ⚠️ **Section-aware selection**: Code exists but DEAD (not wired to server.py flow)
|
|
- ⚠️ **Joint scoring**: Groups defined but no selection recording → no joint scoring
|
|
- ⚠️ **Async status polling**: Infrastructure ready but server blocking prevents status checks
|
|
- ❌ **Async completion**: Jobs start but server blocks, causing timeouts
|
|
|
|
### What Needs Wiring
|
|
|
|
1. **section_context** needs to be set from server.py during generation
|
|
- Currently `SECTION_ROLE_PROFILES` exists but never used
|
|
- Generation flow doesn't know which section it's in
|
|
|
|
2. **record_section_selection** needs to be called after each selection
|
|
- Method exists in `sample_selector.py`
|
|
- Never called from generation flow
|
|
- Required for joint scoring to work
|
|
|
|
3. **joint_scoring** needs selections to be recorded first
|
|
- `JOINT_SCORING_GROUPS` and `FOLDER_COMPATIBILITY_BONUS` defined
|
|
- Can't apply joint scoring without recorded selections
|
|
|
|
4. **Section-aware filtering** needs to be integrated into selection flow
|
|
- `SECTION_ROLE_PROFILES` defines primary/secondary/avoid per section
|
|
- Not used in actual `select_samples()` call chain
|
|
|
|
### Honest Assessment
|
|
|
|
**What works**: Infrastructure, extraction, caching, clearing tracks, compiling
|
|
**What exists but is dead**: Section-aware selection, joint scoring, same-pack strict enforcement
|
|
**What has issues**: Async blocking, smoke test module isolation
|
|
**What's unvalidated**: Same-pack selection, Z.ai 429 handling
|
|
|
|
**Bottom line**: ~40% of features are runtime validated, ~45% exist but aren't wired, ~15% needs fixing.
|