- 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
637 lines
31 KiB
Python
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)
|