Files
ableton-mcp-ai/mcp_server/engines/preset_system.py
OpenCode Agent 5ce8187c65 feat: Implement senior audio injection with 5 fallback methods
- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain
- Method 1: track.insert_arrangement_clip() [Live 12+]
- Method 2: track.create_audio_clip() [Live 11+]
- Method 3: arrangement_clips.add_new_clip() [Live 12+]
- Method 4: Session->duplicate_clip_to_arrangement [Legacy]
- Method 5: Session->Recording [Universal]

- Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow
- Update skills documentation
- Verified: 3 clips created at positions [0, 4, 8] in Arrangement View

Closes: Audio injection in Arrangement View
2026-04-12 14:02:32 -03:00

637 lines
31 KiB
Python

"""
Preset System - Sistema de Presets y Templates para AbletonMCP_AI (T061-T065)
Gestión completa de presets para reggaeton: predefinidos, personalizados,
importación/exportación, y aplicación a proyectos.
"""
import json
import logging
import os
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
logger = logging.getLogger("PresetSystem")
PRESETS_DIR = Path(r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\presets")
# =============================================================================
# DATACLASSES
# =============================================================================
@dataclass
class TrackPreset:
"""Configuración de preset para una pista individual."""
name: str
track_type: str # "midi" o "audio"
role: str
sample_criteria: Dict[str, Any] = field(default_factory=dict)
device_chain: List[Dict[str, Any]] = field(default_factory=list)
volume: float = 0.8
pan: float = 0.0
mute: bool = False
solo: bool = False
color: int = 0
def to_dict(self) -> Dict[str, Any]: return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "TrackPreset": return cls(**data)
@dataclass
class MixingConfig:
"""Configuración de mezcla para un preset."""
eq_low_gain: float = 0.0
eq_mid_gain: float = 0.0
eq_high_gain: float = 0.0
compressor_threshold: float = -6.0
compressor_ratio: float = 3.0
compressor_makeup: float = 3.0
send_reverb: float = 0.3
send_delay: float = 0.2
master_volume: float = 0.85
def to_dict(self) -> Dict[str, Any]: return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "MixingConfig": return cls(**data)
@dataclass
class SampleSelectionCriteria:
"""Criterios de selección de samples para un preset."""
preferred_packs: List[str] = field(default_factory=list)
excluded_packs: List[str] = field(default_factory=list)
min_bpm: float = 0.0
max_bpm: float = 0.0
preferred_key: str = ""
use_similarity_selection: bool = False
similarity_reference: str = ""
priority_roles: List[str] = field(default_factory=lambda: ["kick", "snare", "bass", "hat_closed"])
def to_dict(self) -> Dict[str, Any]: return asdict(self)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "SampleSelectionCriteria": return cls(**data)
@dataclass
class Preset:
"""Preset completo de configuración de canción."""
name: str
description: str
version: str = "1.0"
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
bpm: float = 95.0
key: str = "Am"
style: str = "dembow"
structure: str = "standard"
tracks_config: List[TrackPreset] = field(default_factory=list)
mixing_config: MixingConfig = field(default_factory=MixingConfig)
sample_selection: SampleSelectionCriteria = field(default_factory=SampleSelectionCriteria)
tags: List[str] = field(default_factory=list)
author: str = ""
is_builtin: bool = False
def to_dict(self) -> Dict[str, Any]:
return {
"name": self.name, "description": self.description, "version": self.version,
"created_at": self.created_at, "updated_at": self.updated_at,
"bpm": self.bpm, "key": self.key, "style": self.style, "structure": self.structure,
"tracks_config": [t.to_dict() for t in self.tracks_config],
"mixing_config": self.mixing_config.to_dict(),
"sample_selection": self.sample_selection.to_dict(),
"tags": self.tags, "author": self.author, "is_builtin": self.is_builtin,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Preset":
tracks = [TrackPreset.from_dict(t) for t in data.get("tracks_config", [])]
mixing = MixingConfig.from_dict(data.get("mixing_config", {}))
samples = SampleSelectionCriteria.from_dict(data.get("sample_selection", {}))
return cls(
name=data["name"], description=data.get("description", ""), version=data.get("version", "1.0"),
created_at=data.get("created_at", datetime.now().isoformat()),
updated_at=data.get("updated_at", datetime.now().isoformat()),
bpm=data.get("bpm", 95.0), key=data.get("key", "Am"), style=data.get("style", "dembow"),
structure=data.get("structure", "standard"), tracks_config=tracks, mixing_config=mixing,
sample_selection=samples, tags=data.get("tags", []), author=data.get("author", ""),
is_builtin=data.get("is_builtin", False),
)
# =============================================================================
# PRESETS PREDEFINIDOS
# =============================================================================
def create_builtin_presets() -> Dict[str, Preset]:
"""Crea el diccionario de presets predefinidos del sistema."""
# 1. Reggaeton Clásico 95 BPM
reggaeton_classic = Preset(
name="reggaeton_classic_95bpm",
description="Reggaeton clásico con dembow puro. Ideal para pistas de club.",
bpm=95.0, key="Am", style="dembow", structure="standard",
tags=["classic", "club", "dembow", "standard"], is_builtin=True,
tracks_config=[
TrackPreset(name="Kick", track_type="midi", role="kick", volume=0.9, sample_criteria={"role": "kick", "pack_preference": "classic"}),
TrackPreset(name="Snare", track_type="midi", role="snare", volume=0.75, sample_criteria={"role": "snare"}),
TrackPreset(name="Hi-Hats", track_type="midi", role="hat_closed", volume=0.65, sample_criteria={"role": "hat_closed"}),
TrackPreset(name="Bass", track_type="midi", role="bass", volume=0.85, sample_criteria={"role": "bass", "pack_preference": "classic"}),
TrackPreset(name="Synth Lead", track_type="midi", role="synth_lead", volume=0.7, sample_criteria={"role": "synth"}),
],
mixing_config=MixingConfig(eq_low_gain=2.0, compressor_threshold=-4.0, compressor_ratio=2.5, send_reverb=0.25, master_volume=0.88),
)
# 2. Perreo Intenso 100 BPM
perreo_intenso = Preset(
name="perreo_intenso_100bpm",
description="Perreo intenso con kick heavy y bajo prominente. Alto impacto.",
bpm=100.0, key="Em", style="perreo", structure="standard",
tags=["perreo", "heavy", "club", "energetic"], is_builtin=True,
tracks_config=[
TrackPreset(name="Kick Heavy", track_type="midi", role="kick", volume=0.95, sample_criteria={"role": "kick", "character": "heavy"}),
TrackPreset(name="Snare", track_type="midi", role="snare", volume=0.8),
TrackPreset(name="Clap", track_type="midi", role="clap", volume=0.7),
TrackPreset(name="Hi-Hats", track_type="midi", role="hat_closed", volume=0.7),
TrackPreset(name="Bass Deep", track_type="midi", role="bass", volume=0.9, sample_criteria={"role": "bass", "character": "deep"}),
TrackPreset(name="Lead", track_type="midi", role="synth_lead", volume=0.75),
],
mixing_config=MixingConfig(eq_low_gain=4.0, compressor_threshold=-6.0, compressor_ratio=3.5, send_reverb=0.2, master_volume=0.9),
)
# 3. Reggaeton Romántico 90 BPM
reggaeton_romantico = Preset(
name="reggaeton_romantico_90bpm",
description="Reggaeton romántico con reverb abundante y mezcla balanceada.",
bpm=90.0, key="Gm", style="romantico", structure="extended",
tags=["romantico", "smooth", "reverb", "extended"], is_builtin=True,
tracks_config=[
TrackPreset(name="Kick Soft", track_type="midi", role="kick", volume=0.75, sample_criteria={"role": "kick", "character": "soft"}),
TrackPreset(name="Snare", track_type="midi", role="snare", volume=0.65),
TrackPreset(name="Hi-Hats", track_type="midi", role="hat_closed", volume=0.55),
TrackPreset(name="Bass Smooth", track_type="midi", role="bass", volume=0.7, sample_criteria={"role": "bass", "character": "smooth"}),
TrackPreset(name="Pad", track_type="midi", role="synth_pad", volume=0.6),
TrackPreset(name="Lead Melodic", track_type="midi", role="synth_lead", volume=0.65),
],
mixing_config=MixingConfig(eq_low_gain=0.0, compressor_threshold=-8.0, compressor_ratio=2.0, send_reverb=0.5, send_delay=0.35, master_volume=0.82),
)
# 4. Moombahton 108 BPM
moombahton = Preset(
name="moombahton_108bpm",
description="Moombahton con variación de dembow y estructura minimal.",
bpm=108.0, key="Dm", style="moombahton", structure="minimal",
tags=["moombahton", "dembow", "minimal", "electronic"], is_builtin=True,
tracks_config=[
TrackPreset(name="Kick Moombah", track_type="midi", role="kick", volume=0.9, sample_criteria={"role": "kick", "style": "moombahton"}),
TrackPreset(name="Snare", track_type="midi", role="snare", volume=0.75),
TrackPreset(name="Tom", track_type="midi", role="perc", volume=0.6, sample_criteria={"role": "perc"}),
TrackPreset(name="Hi-Hats", track_type="midi", role="hat_closed", volume=0.65),
TrackPreset(name="Bass", track_type="midi", role="bass", volume=0.8),
TrackPreset(name="Stabs", track_type="midi", role="synth_lead", volume=0.7, sample_criteria={"role": "synth", "character": "stab"}),
],
mixing_config=MixingConfig(eq_low_gain=3.0, compressor_threshold=-5.0, compressor_ratio=3.0, send_reverb=0.3, master_volume=0.87),
)
# 5. Trapeton 140 BPM
trapeton = Preset(
name="trapeton_140bpm",
description="Trapeton con 808s pesados y hi-hat rolls. Fusión trap-reggaeton.",
bpm=140.0, key="Cm", style="trapeton", structure="standard",
tags=["trapeton", "trap", "808", "hihat_rolls", "hard"], is_builtin=True,
tracks_config=[
TrackPreset(name="808 Kick", track_type="midi", role="kick", volume=0.95, sample_criteria={"role": "kick", "character": "808"}),
TrackPreset(name="Snare", track_type="midi", role="snare", volume=0.8, sample_criteria={"role": "snare", "character": "trap"}),
TrackPreset(name="Hi-Hats", track_type="midi", role="hat_closed", volume=0.75, sample_criteria={"role": "hat_closed", "style": "trap"}),
TrackPreset(name="Hi-Hat Rolls", track_type="midi", role="hat_open", volume=0.65, sample_criteria={"role": "hat_open", "style": "trap_rolls"}),
TrackPreset(name="808 Bass", track_type="midi", role="bass", volume=0.9, sample_criteria={"role": "bass", "character": "808"}),
TrackPreset(name="Lead Hard", track_type="midi", role="synth_lead", volume=0.75, sample_criteria={"role": "synth", "character": "aggressive"}),
],
mixing_config=MixingConfig(eq_low_gain=5.0, eq_high_gain=2.0, compressor_threshold=-8.0, compressor_ratio=4.0, compressor_makeup=4.0, send_reverb=0.15, send_delay=0.25, master_volume=0.92),
)
return {
reggaeton_classic.name: reggaeton_classic,
perreo_intenso.name: perreo_intenso,
reggaeton_romantico.name: reggaeton_romantico,
moombahton.name: moombahton,
trapeton.name: trapeton,
}
# =============================================================================
# PRESET MANAGER
# =============================================================================
class PresetManager:
"""Gestor de presets para AbletonMCP_AI."""
def __init__(self, presets_dir: Optional[str] = None):
self._presets_dir = Path(presets_dir) if presets_dir else PRESETS_DIR
self._builtin_presets: Dict[str, Preset] = create_builtin_presets()
self._custom_presets: Dict[str, Preset] = {}
self._ensure_presets_dir()
self._load_custom_presets()
def _ensure_presets_dir(self):
if not self._presets_dir.exists():
try:
self._presets_dir.mkdir(parents=True, exist_ok=True)
logger.info("Created presets directory: %s", self._presets_dir)
except Exception as e:
logger.error("Failed to create presets directory: %s", e)
def _get_preset_path(self, preset_name: str) -> Path:
safe_name = preset_name.replace(" ", "_").lower()
return self._presets_dir / f"{safe_name}.json"
def _load_custom_presets(self):
if not self._presets_dir.exists():
return
for preset_file in self._presets_dir.glob("*.json"):
try:
with open(preset_file, "r", encoding="utf-8") as f:
data = json.load(f)
preset = Preset.from_dict(data)
if not preset.is_builtin:
self._custom_presets[preset.name] = preset
except Exception as e:
logger.warning("Failed to load preset %s: %s", preset_file, e)
logger.info("Loaded %d custom presets", len(self._custom_presets))
def load_preset(self, preset_name: str) -> Optional[Preset]:
"""Carga un preset por nombre. Busca primero en builtins, luego custom."""
if preset_name in self._builtin_presets:
logger.info("Loaded builtin preset: %s", preset_name)
return self._builtin_presets[preset_name]
if preset_name in self._custom_presets:
logger.info("Loaded custom preset: %s", preset_name)
return self._custom_presets[preset_name]
preset_name_lower = preset_name.lower()
for name, preset in {**self._builtin_presets, **self._custom_presets}.items():
if name.lower() == preset_name_lower:
return preset
logger.warning("Preset not found: %s", preset_name)
return None
def save_as_preset(self, config: Dict[str, Any], preset_name: str) -> bool:
"""Guarda una configuración como preset personalizado."""
try:
preset = self._config_to_preset(config, preset_name)
preset.is_builtin = False
preset.updated_at = datetime.now().isoformat()
preset_path = self._get_preset_path(preset_name)
with open(preset_path, "w", encoding="utf-8") as f:
json.dump(preset.to_dict(), f, indent=2, ensure_ascii=False)
self._custom_presets[preset_name] = preset
logger.info("Saved preset: %s", preset_name)
return True
except Exception as e:
logger.error("Failed to save preset %s: %s", preset_name, e)
return False
def _config_to_preset(self, config: Dict[str, Any], name: str) -> Preset:
"""Convierte un diccionario de configuración a un Preset."""
tracks_config = []
for track_data in config.get("tracks", []):
tracks_config.append(TrackPreset(
name=track_data.get("name", "Track"), track_type=track_data.get("track_type", "midi"),
role=track_data.get("instrument_role", "synth"), volume=track_data.get("volume", 0.8),
pan=track_data.get("pan", 0.0), device_chain=track_data.get("device_chain", []),
))
mixing_data = config.get("mixing_config", {})
mixing_config = MixingConfig(
eq_low_gain=mixing_data.get("eq_low_gain", 0.0), eq_mid_gain=mixing_data.get("eq_mid_gain", 0.0),
eq_high_gain=mixing_data.get("eq_high_gain", 0.0), compressor_threshold=mixing_data.get("compressor_threshold", -6.0),
compressor_ratio=mixing_data.get("compressor_ratio", 3.0), send_reverb=mixing_data.get("send_reverb", 0.3),
send_delay=mixing_data.get("send_delay", 0.2), master_volume=mixing_data.get("master_volume", 0.85),
)
return Preset(
name=name, description=config.get("description", f"Custom preset: {name}"),
bpm=config.get("bpm", 95.0), key=config.get("key", "Am"), style=config.get("style", "dembow"),
structure=config.get("structure", "standard"), tracks_config=tracks_config,
mixing_config=mixing_config, tags=config.get("tags", ["custom"]),
)
def list_presets(self, include_builtin: bool = True, filter_tags: Optional[List[str]] = None) -> List[Dict[str, Any]]:
"""Lista todos los presets disponibles."""
all_presets: Dict[str, Preset] = {}
if include_builtin:
all_presets.update(self._builtin_presets)
all_presets.update(self._custom_presets)
if filter_tags:
all_presets = {n: p for n, p in all_presets.items() if any(t in p.tags for t in filter_tags)}
result = [
{"name": n, "description": p.description, "bpm": p.bpm, "key": p.key, "style": p.style,
"structure": p.structure, "tags": p.tags, "is_builtin": p.is_builtin, "track_count": len(p.tracks_config)}
for n, p in all_presets.items()
]
result.sort(key=lambda x: (not x["is_builtin"], x["name"]))
return result
def create_custom_preset(self, current_config: Dict[str, Any], name: str, description: str = "", tags: Optional[List[str]] = None) -> Optional[Preset]:
"""Crea un nuevo preset personalizado desde una configuración."""
try:
preset = self._config_to_preset(current_config, name)
preset.description = description or f"Custom preset: {name}"
preset.tags = tags or ["custom"]
preset.is_builtin = False
preset.author = current_config.get("author", "")
if self.save_as_preset(current_config, name):
return preset
return None
except Exception as e:
logger.error("Failed to create custom preset: %s", e)
return None
def delete_preset(self, preset_name: str) -> bool:
"""Elimina un preset personalizado. No se pueden eliminar builtins."""
if preset_name in self._builtin_presets:
logger.warning("Cannot delete builtin preset: %s", preset_name)
return False
if preset_name not in self._custom_presets:
logger.warning("Preset not found for deletion: %s", preset_name)
return False
try:
preset_path = self._get_preset_path(preset_name)
if preset_path.exists():
preset_path.unlink()
del self._custom_presets[preset_name]
logger.info("Deleted preset: %s", preset_name)
return True
except Exception as e:
logger.error("Failed to delete preset %s: %s", preset_name, e)
return False
def export_preset(self, preset_name: str, export_path: str) -> bool:
"""Exporta un preset a un archivo externo."""
preset = self.load_preset(preset_name)
if not preset:
logger.warning("Cannot export non-existent preset: %s", preset_name)
return False
try:
export_path = Path(export_path)
if not export_path.suffix == ".json":
export_path = export_path.with_suffix(".json")
with open(export_path, "w", encoding="utf-8") as f:
json.dump(preset.to_dict(), f, indent=2, ensure_ascii=False)
logger.info("Exported preset %s to %s", preset_name, export_path)
return True
except Exception as e:
logger.error("Failed to export preset %s: %s", preset_name, e)
return False
def import_preset(self, import_path: str, preset_name: Optional[str] = None) -> Optional[Preset]:
"""Importa un preset desde un archivo externo."""
try:
import_path = Path(import_path)
if not import_path.exists():
logger.error("Import file not found: %s", import_path)
return None
with open(import_path, "r", encoding="utf-8") as f:
data = json.load(f)
preset = Preset.from_dict(data)
preset.is_builtin = False
if preset_name:
preset.name = preset_name
preset_path = self._get_preset_path(preset.name)
with open(preset_path, "w", encoding="utf-8") as f:
json.dump(preset.to_dict(), f, indent=2, ensure_ascii=False)
self._custom_presets[preset.name] = preset
logger.info("Imported preset: %s", preset.name)
return preset
except Exception as e:
logger.error("Failed to import preset from %s: %s", import_path, e)
return None
def get_preset_details(self, preset_name: str) -> Optional[Dict[str, Any]]:
"""Obtiene detalles completos de un preset."""
preset = self.load_preset(preset_name)
if not preset:
return None
return {
"name": preset.name, "description": preset.description, "version": preset.version,
"created_at": preset.created_at, "updated_at": preset.updated_at,
"bpm": preset.bpm, "key": preset.key, "style": preset.style, "structure": preset.structure,
"tracks": [{"name": t.name, "type": t.track_type, "role": t.role, "volume": t.volume, "pan": t.pan} for t in preset.tracks_config],
"mixing": preset.mixing_config.to_dict(),
"sample_selection": preset.sample_selection.to_dict(),
"tags": preset.tags, "author": preset.author, "is_builtin": preset.is_builtin,
}
def duplicate_preset(self, source_name: str, new_name: str) -> bool:
"""Duplica un preset existente con un nuevo nombre."""
source = self.load_preset(source_name)
if not source:
return False
try:
new_preset = Preset.from_dict(source.to_dict())
new_preset.name = new_name
new_preset.is_builtin = False
new_preset.description = f"Copy of {source_name}: {source.description}"
new_preset.created_at = datetime.now().isoformat()
new_preset.updated_at = datetime.now().isoformat()
preset_path = self._get_preset_path(new_name)
with open(preset_path, "w", encoding="utf-8") as f:
json.dump(new_preset.to_dict(), f, indent=2, ensure_ascii=False)
self._custom_presets[new_name] = new_preset
logger.info("Duplicated preset %s to %s", source_name, new_name)
return True
except Exception as e:
logger.error("Failed to duplicate preset: %s", e)
return False
# =============================================================================
# FUNCIONES DE CONVENIENCIA
# =============================================================================
_manager: Optional[PresetManager] = None
def get_preset_manager() -> PresetManager:
"""Retorna la instancia singleton del PresetManager."""
global _manager
if _manager is None:
_manager = PresetManager()
return _manager
def apply_preset_to_project(preset_name: str) -> Dict[str, Any]:
"""Aplica un preset completo al proyecto actual."""
manager = get_preset_manager()
preset = manager.load_preset(preset_name)
if not preset:
return {"success": False, "error": f"Preset not found: {preset_name}"}
config = {
"bpm": preset.bpm, "key": preset.key, "style": preset.style, "structure": preset.structure,
"tracks": [{"name": t.name, "track_type": t.track_type, "instrument_role": t.role,
"volume": t.volume, "pan": t.pan, "device_chain": t.device_chain} for t in preset.tracks_config],
"mixing_config": preset.mixing_config.to_dict(),
"sample_criteria": preset.sample_selection.to_dict(),
}
return {
"success": True, "preset_name": preset_name, "config": config,
"message": f"Preset '{preset_name}' loaded and ready to apply",
}
def get_default_preset() -> str:
"""Retorna el nombre del preset por defecto."""
return "reggaeton_classic_95bpm"
def list_available_presets(style_filter: Optional[str] = None) -> List[Dict[str, Any]]:
"""Lista todos los presets disponibles, opcionalmente filtrados por estilo."""
manager = get_preset_manager()
presets = manager.list_presets()
if style_filter:
presets = [p for p in presets if p.get("style") == style_filter]
return presets
def quick_apply_preset(preset_name: Optional[str] = None) -> Dict[str, Any]:
"""Aplica rápidamente un preset (o el default si no se especifica)."""
if preset_name is None:
preset_name = get_default_preset()
return apply_preset_to_project(preset_name)
# =============================================================================
# HANDLERS MCP
# =============================================================================
def _cmd_load_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Carga un preset por nombre."""
preset_name = params.get("preset_name", "")
if not preset_name:
return {"success": False, "error": "Missing preset_name parameter"}
manager = get_preset_manager()
preset = manager.load_preset(preset_name)
if not preset:
return {"success": False, "error": f"Preset not found: {preset_name}"}
return {"success": True, "preset": preset.to_dict()}
def _cmd_save_as_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Guarda configuración actual como preset."""
config, preset_name = params.get("config", {}), params.get("preset_name", "")
if not preset_name:
return {"success": False, "error": "Missing preset_name parameter"}
success = get_preset_manager().save_as_preset(config, preset_name)
return {"success": success, "preset_name": preset_name, "message": f"Preset '{preset_name}' saved" if success else "Failed to save"}
def _cmd_list_presets(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Lista todos los presets disponibles."""
manager = get_preset_manager()
presets = manager.list_presets(include_builtin=params.get("include_builtin", True), filter_tags=params.get("filter_tags"))
return {"success": True, "count": len(presets), "presets": presets}
def _cmd_create_custom_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Crea un preset personalizado."""
current_config, name = params.get("current_config", {}), params.get("name", "")
if not name:
return {"success": False, "error": "Missing name parameter"}
preset = get_preset_manager().create_custom_preset(current_config, name, params.get("description", ""), params.get("tags"))
return {"success": preset is not None, "preset_name": name, "preset": preset.to_dict() if preset else None}
def _cmd_delete_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Elimina un preset personalizado."""
preset_name = params.get("preset_name", "")
if not preset_name:
return {"success": False, "error": "Missing preset_name parameter"}
success = get_preset_manager().delete_preset(preset_name)
return {"success": success, "message": f"Preset '{preset_name}' deleted" if success else f"Failed to delete '{preset_name}'"}
def _cmd_export_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Exporta un preset a archivo."""
preset_name, export_path = params.get("preset_name", ""), params.get("export_path", "")
if not preset_name or not export_path:
return {"success": False, "error": "Missing preset_name or export_path"}
success = get_preset_manager().export_preset(preset_name, export_path)
return {"success": success, "message": f"Exported to {export_path}" if success else "Export failed"}
def _cmd_import_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Importa un preset desde archivo."""
import_path = params.get("import_path", "")
if not import_path:
return {"success": False, "error": "Missing import_path parameter"}
preset = get_preset_manager().import_preset(import_path, params.get("preset_name"))
return {"success": preset is not None, "preset_name": preset.name if preset else None, "preset": preset.to_dict() if preset else None}
def _cmd_get_preset_details(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Obtiene detalles completos de un preset."""
preset_name = params.get("preset_name", "")
if not preset_name:
return {"success": False, "error": "Missing preset_name parameter"}
details = get_preset_manager().get_preset_details(preset_name)
return {"success": details is not None, "preset": details, "error": f"Preset not found: {preset_name}" if not details else None}
def _cmd_duplicate_preset(params: Dict[str, Any]) -> Dict[str, Any]:
"""Handler MCP: Duplica un preset existente."""
source_name, new_name = params.get("source_name", ""), params.get("new_name", "")
if not source_name or not new_name:
return {"success": False, "error": "Missing source_name or new_name"}
success = get_preset_manager().duplicate_preset(source_name, new_name)
return {"success": success, "message": f"Duplicated: {source_name} -> {new_name}" if success else "Duplication failed"}
# Mapa de handlers disponibles para el MCP server
MCP_HANDLERS = {
"load_preset": _cmd_load_preset,
"save_as_preset": _cmd_save_as_preset,
"list_presets": _cmd_list_presets,
"create_custom_preset": _cmd_create_custom_preset,
"delete_preset": _cmd_delete_preset,
"export_preset": _cmd_export_preset,
"import_preset": _cmd_import_preset,
"get_preset_details": _cmd_get_preset_details,
"duplicate_preset": _cmd_duplicate_preset,
"apply_preset": lambda p: apply_preset_to_project(p.get("preset_name", "")),
}
# =============================================================================
# MAIN / TEST
# =============================================================================
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
print("=" * 70)
print("PRESET SYSTEM - AbletonMCP_AI")
print("=" * 70)
print("\n1. Inicializando PresetManager...")
manager = get_preset_manager()
print(f" OK - Directorio: {manager._presets_dir}")
print("\n2. Presets predefinidos:")
for name, preset in manager._builtin_presets.items():
print(f" - {name}: {preset.description[:45]}...")
print("\n3. Listando todos los presets...")
all_presets = manager.list_presets()
print(f" Total: {len(all_presets)} presets")
for p in all_presets[:5]:
print(f" - {p['name']} ({p['style']}, {p['bpm']} BPM, {p['track_count']} tracks)")
print("\n4. Cargando 'reggaeton_classic_95bpm'...")
classic = manager.load_preset("reggaeton_classic_95bpm")
if classic:
print(f" BPM: {classic.bpm}, Key: {classic.key}, Tracks: {len(classic.tracks_config)}")
print("\n5. Detalles de 'perreo_intenso_100bpm'...")
details = manager.get_preset_details("perreo_intenso_100bpm")
if details:
print(f" EQ Low: {details['mixing']['eq_low_gain']} dB, Comp: {details['mixing']['compressor_threshold']} dB")
print("\n6. Aplicando preset default...")
result = quick_apply_preset()
print(f" Success: {result['success']}, Preset: {result.get('preset_name')}")
print("\n" + "=" * 70)
print("Tests completados!")
print("=" * 70)