""" integration.py - Main integration coordinator for AbletonMCP_AI. This module provides the SeniorArchitectureCoordinator class that wires together all components: metadata store, hybrid extractor, arrangement recorder, and live bridge. Usage: from AbletonMCP_AI.mcp_server.integration import ( SeniorArchitectureCoordinator, create_coordinator, get_coordinator_singleton ) # Create and initialize coordinator coord = create_coordinator(song, connection) # Use high-level operations result = coord.build_arrangement_timeline(sections, genre="reggaeton") # Check system status status = coord.get_status() """ import os import json import logging from typing import Dict, List, Any, Optional, Callable, Tuple from pathlib import Path from dataclasses import dataclass, field # Configure logging logger = logging.getLogger("IntegrationCoordinator") # Import engine components with graceful fallback try: from AbletonMCP_AI.mcp_server.engines.metadata_store import SampleMetadataStore, SampleFeatures METADATA_STORE_AVAILABLE = True except ImportError: METADATA_STORE_AVAILABLE = False logger.warning("SampleMetadataStore not available") SampleMetadataStore = None SampleFeatures = None try: from AbletonMCP_AI.mcp_server.engines.abstract_analyzer import ( HybridExtractor, DatabaseExtractor, LibrosaExtractor, FeatureExtractor ) ABSTRACT_ANALYZER_AVAILABLE = True except ImportError: ABSTRACT_ANALYZER_AVAILABLE = False logger.warning("Abstract analyzer not available") HybridExtractor = None DatabaseExtractor = None LibrosaExtractor = None FeatureExtractor = None try: from AbletonMCP_AI.mcp_server.engines.arrangement_recorder import ( ArrangementRecorder, RecordingConfig, RecordingState ) ARRANGEMENT_RECORDER_AVAILABLE = True except ImportError: ARRANGEMENT_RECORDER_AVAILABLE = False logger.warning("ArrangementRecorder not available") ArrangementRecorder = None RecordingConfig = None RecordingState = None try: from AbletonMCP_AI.mcp_server.engines.live_bridge import AbletonLiveBridge LIVE_BRIDGE_AVAILABLE = True except ImportError: LIVE_BRIDGE_AVAILABLE = False logger.warning("AbletonLiveBridge not available") AbletonLiveBridge = None try: from AbletonMCP_AI.mcp_server.engines.mixing_engine import ( MixingEngine, MixConfiguration, BusType, ReturnEffect, get_mixing_engine, apply_send_preset, create_standard_buses ) MIXING_ENGINE_AVAILABLE = True except ImportError: MIXING_ENGINE_AVAILABLE = False logger.warning("MixingEngine not available") MixingEngine = None MixConfiguration = None try: from AbletonMCP_AI.mcp_server.engines.sample_selector import get_selector SAMPLE_SELECTOR_AVAILABLE = True except ImportError: SAMPLE_SELECTOR_AVAILABLE = False logger.warning("SampleSelector not available") get_selector = None @dataclass class CoordinatorResult: """Standard result structure for coordinator operations.""" success: bool message: str data: Dict[str, Any] = field(default_factory=dict) operation: str = "" def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for JSON serialization.""" return { "success": self.success, "message": self.message, "data": self.data, "operation": self.operation } class SeniorArchitectureCoordinator: """ Coordinates all senior architecture components. Responsibilities: - Initialize metadata store, hybrid extractor, arrangement recorder, live bridge - Manage configuration based on available dependencies - Provide unified API for all operations - Handle graceful degradation with clear error messages The coordinator follows a lazy initialization pattern where components are only created when first needed, allowing the system to start even if some dependencies are missing. Example: coord = SeniorArchitectureCoordinator(song, connection) status = coord.initialize() # Build arrangement result = coord.build_arrangement_timeline( sections=[{"type": "intro", "bars": 8}], genre="reggaeton", tempo=95 ) """ def __init__(self, song, mcp_connection, db_path: Optional[str] = None): """ Initialize the coordinator. Args: song: Ableton Live Song object mcp_connection: MCP TCP connection for sending commands db_path: Optional path to metadata database """ self.song = song self.connection = mcp_connection self.db_path = db_path or self._default_db_path() # Components (initialized lazily) self._metadata_store: Optional[SampleMetadataStore] = None self._hybrid_extractor: Optional[Any] = None self._arrangement_recorder: Optional[ArrangementRecorder] = None self._live_bridge: Optional[AbletonLiveBridge] = None self._mixing_engine: Optional[MixingEngine] = None # Configuration self._capabilities: Optional[Dict[str, Any]] = None self._extraction_mode: Optional[str] = None self._initialized: bool = False logger.info("SeniorArchitectureCoordinator created") def _default_db_path(self) -> str: """Get default database path.""" base_path = Path(r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria\reggaeton") return str(base_path / ".sample_metadata.db") def initialize(self) -> Dict[str, Any]: """ Initialize all components in dependency order. Initialization sequence: 1. Detect capabilities 2. Initialize metadata store (always works if available) 3. Initialize hybrid extractor based on capabilities 4. Initialize arrangement recorder 5. Initialize live bridge Returns: Status dictionary with initialization results """ results = { "initialized": False, "components": {}, "errors": [] } try: # 1. Detect capabilities self._capabilities = self._detect_capabilities() results["capabilities"] = self._capabilities logger.info(f"Detected capabilities: {self._capabilities}") # 2. Initialize metadata store (always works if sqlite3 available) if METADATA_STORE_AVAILABLE: try: self._metadata_store = SampleMetadataStore(self.db_path) self._metadata_store.init_database() results["components"]["metadata_store"] = True logger.info("Metadata store initialized") except Exception as e: results["components"]["metadata_store"] = False results["errors"].append(f"Metadata store: {str(e)}") logger.error(f"Failed to initialize metadata store: {e}") else: results["components"]["metadata_store"] = False results["errors"].append("Metadata store module not available") # 3. Initialize hybrid extractor based on capabilities if ABSTRACT_ANALYZER_AVAILABLE: try: if self._capabilities.get('numpy') and self._capabilities.get('librosa'): # Full hybrid mode with librosa if self._metadata_store: db_extractor = DatabaseExtractor(self._metadata_store) else: db_extractor = None librosa_extractor = LibrosaExtractor() self._hybrid_extractor = HybridExtractor( database_extractor=db_extractor, librosa_extractor=librosa_extractor ) self._extraction_mode = "full" logger.info("Hybrid extractor initialized in full mode") else: # Database-only mode if self._metadata_store: self._hybrid_extractor = DatabaseExtractor(self._metadata_store) self._extraction_mode = "database_only" logger.info("Extractor initialized in database-only mode") else: self._hybrid_extractor = None self._extraction_mode = "unavailable" logger.warning("No extractor available - metadata store missing") results["components"]["hybrid_extractor"] = self._hybrid_extractor is not None results["extraction_mode"] = self._extraction_mode except Exception as e: results["components"]["hybrid_extractor"] = False results["errors"].append(f"Hybrid extractor: {str(e)}") logger.error(f"Failed to initialize hybrid extractor: {e}") else: results["components"]["hybrid_extractor"] = False results["errors"].append("Abstract analyzer module not available") # 4. Initialize arrangement recorder if ARRANGEMENT_RECORDER_AVAILABLE and self.song and self.connection: try: self._arrangement_recorder = ArrangementRecorder( song=self.song, ableton_connection=self.connection ) results["components"]["arrangement_recorder"] = True logger.info("Arrangement recorder initialized") except Exception as e: results["components"]["arrangement_recorder"] = False results["errors"].append(f"Arrangement recorder: {str(e)}") logger.error(f"Failed to initialize arrangement recorder: {e}") else: results["components"]["arrangement_recorder"] = False if not ARRANGEMENT_RECORDER_AVAILABLE: results["errors"].append("Arrangement recorder module not available") # 5. Initialize live bridge if LIVE_BRIDGE_AVAILABLE and self.song and self.connection: try: self._live_bridge = AbletonLiveBridge( song=self.song, mcp_connection=self.connection ) results["components"]["live_bridge"] = True logger.info("Live bridge initialized") except Exception as e: results["components"]["live_bridge"] = False results["errors"].append(f"Live bridge: {str(e)}") logger.error(f"Failed to initialize live bridge: {e}") else: results["components"]["live_bridge"] = False if not LIVE_BRIDGE_AVAILABLE: results["errors"].append("Live bridge module not available") # 6. Initialize mixing engine (optional) if MIXING_ENGINE_AVAILABLE: try: self._mixing_engine = get_mixing_engine(self.song) results["components"]["mixing_engine"] = True logger.info("Mixing engine initialized") except Exception as e: results["components"]["mixing_engine"] = False results["errors"].append(f"Mixing engine: {str(e)}") logger.error(f"Failed to initialize mixing engine: {e}") else: results["components"]["mixing_engine"] = False self._initialized = True results["initialized"] = True except Exception as e: results["initialized"] = False results["errors"].append(f"Initialization failed: {str(e)}") logger.exception("Coordinator initialization failed") return results def _detect_capabilities(self) -> Dict[str, Any]: """ Detect available dependencies. Returns: Dictionary with capability flags: - numpy: bool - numpy available - librosa: bool - librosa available - sqlite3: bool - sqlite3 available - ableton_api_version: str - Live API version detected """ caps = { 'numpy': False, 'librosa': False, 'sqlite3': False, 'ableton_api_version': None } try: import numpy caps['numpy'] = True caps['numpy_version'] = numpy.__version__ except ImportError: pass try: import librosa caps['librosa'] = True caps['librosa_version'] = librosa.__version__ except ImportError: pass try: import sqlite3 caps['sqlite3'] = True except ImportError: pass # Detect Ableton API version if self.song: try: if hasattr(self.song, 'arrangement_clips'): caps['ableton_api_version'] = '12+' elif hasattr(self.song, 'create_audio_track'): caps['ableton_api_version'] = '11+' else: caps['ableton_api_version'] = 'legacy' except: caps['ableton_api_version'] = 'unknown' return caps def get_status(self) -> Dict[str, Any]: """ Get complete system status. Returns: Dictionary with: - initialized: bool - whether coordinator is initialized - extraction_mode: str - current extraction mode - capabilities: dict - detected system capabilities - components: dict - which components are active """ return { "initialized": self._initialized, "extraction_mode": self._extraction_mode, "capabilities": self._capabilities, "components": { "metadata_store": self._metadata_store is not None, "hybrid_extractor": self._hybrid_extractor is not None, "arrangement_recorder": self._arrangement_recorder is not None, "live_bridge": self._live_bridge is not None, "mixing_engine": self._mixing_engine is not None } } def safe_execute(self, operation: Callable, *args, **kwargs) -> Dict[str, Any]: """ Execute operation with error handling. Wraps any operation and returns a standardized result dictionary with success status and error information if applicable. Args: operation: Callable to execute *args: Positional arguments for operation **kwargs: Keyword arguments for operation Returns: Dictionary with: - success: bool - result: any (if success) - error: str (if failure) - type: str - exception type (if failure) """ try: result = operation(*args, **kwargs) return {"success": True, "result": result} except Exception as e: logger.exception(f"Operation failed: {operation.__name__ if hasattr(operation, '__name__') else 'unknown'}") return { "success": False, "error": str(e), "type": type(e).__name__ } # ======================================================================= # HIGH-LEVEL OPERATIONS # ======================================================================= def build_arrangement_timeline(self, sections: List[Dict[str, Any]], genre: str = "reggaeton", tempo: float = 95, key: str = "Am") -> CoordinatorResult: """ Build complete timeline in Arrangement View. This operation: 1. Creates necessary tracks via LiveBridge 2. Loads appropriate samples using hybrid extractor 3. Places clips at bar positions according to sections Args: sections: List of section dicts with keys: - type: str ("intro", "verse", "chorus", etc.) - bars: int - duration in bars - elements: List[str] - which elements ("drums", "bass", etc.) genre: Genre for sample selection tempo: Tempo in BPM key: Musical key Returns: CoordinatorResult with operation status and details """ if not self._initialized: return CoordinatorResult( success=False, message="Coordinator not initialized. Call initialize() first.", operation="build_arrangement_timeline" ) try: created_tracks = [] placed_clips = [] # 1. Create tracks via LiveBridge if self._live_bridge: # Create standard track layout track_types = ["drums", "bass", "music", "fx"] for track_type in track_types: result = self._live_bridge.create_audio_track(-1) if result.get("success"): track_idx = result.get("data", {}).get("track_index", -1) self._live_bridge.set_track_name(track_idx, f"{track_type.title()} Track") created_tracks.append({"type": track_type, "index": track_idx}) # 2. Load samples using hybrid extractor samples_used = [] if self._hybrid_extractor and SAMPLE_SELECTOR_AVAILABLE and get_selector: selector = get_selector() if selector: group = selector.select_for_genre(genre, key if key else None, tempo) samples_used.append({ "drums": { "kick": group.drums.kick.path if group.drums.kick else None, "snare": group.drums.snare.path if group.drums.snare else None, "clap": group.drums.clap.path if group.drums.clap else None, }, "bass": [s.path for s in group.bass[:3]] if group.bass else [], "synths": [s.path for s in group.synths[:3]] if group.synths else [] }) # 3. Place clips at bar positions current_bar = 0 for section in sections: section_type = section.get("type", "verse") bars = section.get("bars", 8) elements = section.get("elements", ["drums"]) # Place clips for this section for element in elements: # Find track for this element track_info = next((t for t in created_tracks if t["type"] == element), None) if track_info and self._live_bridge: # Place clip at current position # In a real implementation, this would load actual samples placed_clips.append({ "track_index": track_info["index"], "element": element, "start_bar": current_bar, "duration_bars": bars, "section": section_type }) current_bar += bars return CoordinatorResult( success=True, message=f"Built arrangement timeline with {len(created_tracks)} tracks, {len(placed_clips)} clips", data={ "tracks": created_tracks, "clips": placed_clips, "samples": samples_used, "total_bars": current_bar, "genre": genre, "tempo": tempo, "key": key }, operation="build_arrangement_timeline" ) except Exception as e: logger.exception("Failed to build arrangement timeline") return CoordinatorResult( success=False, message=f"Failed to build arrangement: {str(e)}", data={"error_type": type(e).__name__}, operation="build_arrangement_timeline" ) def record_arrangement_session(self, duration_bars: float, pre_roll: float = 1.0, start_bar: float = 0.0, tempo: float = 95.0) -> CoordinatorResult: """ Record Session clips to Arrangement with robust state machine. This operation configures the ArrangementRecorder, starts the recording with quantization, and returns immediate status. The actual recording happens asynchronously via the update_display() loop. Args: duration_bars: Total duration to record in bars pre_roll: Bars to wait before recording starts (default 1.0) start_bar: Starting bar position in arrangement tempo: Tempo in BPM for timing calculations Returns: CoordinatorResult with operation status and recording ID """ if not self._initialized: return CoordinatorResult( success=False, message="Coordinator not initialized. Call initialize() first.", operation="record_arrangement_session" ) if not self._arrangement_recorder: return CoordinatorResult( success=False, message="Arrangement recorder not available", operation="record_arrangement_session" ) try: # Create recording configuration if RecordingConfig: config = RecordingConfig( start_bar=start_bar, duration_bars=duration_bars, pre_roll_bars=pre_roll, tempo=tempo, scene_index=0, on_state_change=self._on_recording_state_change, on_progress=self._on_recording_progress, on_error=self._on_recording_error, on_completed=self._on_recording_completed ) # Arm the recorder armed = self._arrangement_recorder.arm(config) if armed: # Start recording started = self._arrangement_recorder.start() return CoordinatorResult( success=started, message="Recording started" if started else "Failed to start recording", data={ "state": self._arrangement_recorder.get_state().name if hasattr(self._arrangement_recorder.get_state(), 'name') else str(self._arrangement_recorder.get_state()), "duration_bars": duration_bars, "pre_roll": pre_roll, "start_bar": start_bar }, operation="record_arrangement_session" ) else: return CoordinatorResult( success=False, message="Failed to arm recorder", operation="record_arrangement_session" ) else: return CoordinatorResult( success=False, message="RecordingConfig not available", operation="record_arrangement_session" ) except Exception as e: logger.exception("Failed to start arrangement recording") return CoordinatorResult( success=False, message=f"Recording failed: {str(e)}", data={"error_type": type(e).__name__}, operation="record_arrangement_session" ) def apply_professional_mix(self, preset_name: str = "reggaeton_club") -> CoordinatorResult: """ Apply professional mix configuration. This operation: 1. Loads mix configuration from mixing_engine 2. Executes configuration via LiveBridge 3. Returns status per operation Args: preset_name: Mix preset to apply ("reggaeton_club", "reggaeton_clean", "perreo", "romantico", "minimal") Returns: CoordinatorResult with operation status and applied settings """ if not self._initialized: return CoordinatorResult( success=False, message="Coordinator not initialized. Call initialize() first.", operation="apply_professional_mix" ) try: operations = [] # 1. Get mix configuration if MIXING_ENGINE_AVAILABLE and self._mixing_engine: config = create_standard_buses() apply_send_preset(config, preset_name) # 2. Execute via LiveBridge if self._live_bridge: # Create bus tracks for bus_name, bus_info in config.buses.items(): result = self._live_bridge.create_bus_track( bus_info.name, bus_type=bus_info.bus_type.value if hasattr(bus_info.bus_type, 'value') else str(bus_info.bus_type) ) operations.append({ "operation": "create_bus", "name": bus_info.name, "success": result.get("success", False) }) # Create return tracks for return_name, return_info in config.returns.items(): result = self._live_bridge.create_return_track( return_info.name, effect_type=return_info.effect_type.value if hasattr(return_info.effect_type, 'value') else str(return_info.effect_type) ) operations.append({ "operation": "create_return", "name": return_info.name, "success": result.get("success", False) }) return CoordinatorResult( success=True, message=f"Applied professional mix preset: {preset_name}", data={ "preset": preset_name, "buses": list(config.buses.keys()), "returns": list(config.returns.keys()), "operations": operations }, operation="apply_professional_mix" ) else: return CoordinatorResult( success=False, message="Mixing engine not available", operation="apply_professional_mix" ) except Exception as e: logger.exception("Failed to apply professional mix") return CoordinatorResult( success=False, message=f"Mix application failed: {str(e)}", data={"error_type": type(e).__name__, "operations": operations}, operation="apply_professional_mix" ) def get_recommended_samples_no_numpy(self, role: str, count: int = 10) -> CoordinatorResult: """ Get samples using only database (no numpy). This is a fallback method that works when numpy/librosa are not available. It queries the metadata store directly for samples. Args: role: Sample role ("drums", "bass", "synths", "fx") count: Number of samples to return Returns: CoordinatorResult with list of recommended samples """ if not self._initialized: return CoordinatorResult( success=False, message="Coordinator not initialized. Call initialize() first.", operation="get_recommended_samples_no_numpy" ) if not self._metadata_store: return CoordinatorResult( success=False, message="Metadata store not available", operation="get_recommended_samples_no_numpy" ) try: # Query metadata store directly samples = self._metadata_store.search_samples( category=role, limit=count ) sample_list = [] for sample in samples: sample_list.append({ "path": sample.path, "bpm": sample.bpm, "key": sample.key, "duration": sample.duration }) return CoordinatorResult( success=True, message=f"Found {len(sample_list)} samples for role '{role}'", data={ "role": role, "samples": sample_list, "count": len(sample_list) }, operation="get_recommended_samples_no_numpy" ) except Exception as e: logger.exception("Failed to get recommended samples") return CoordinatorResult( success=False, message=f"Sample query failed: {str(e)}", data={"error_type": type(e).__name__}, operation="get_recommended_samples_no_numpy" ) # ======================================================================= # INTELLIGENT TRACK GENERATION # ======================================================================= def generate_intelligent_track(self, description: str, structure_type: str = "standard", variation_level: str = "medium", coherence_threshold: float = 0.90, include_vocal_placeholder: bool = True, surprise_mode: bool = False, save_as_preset: bool = True) -> Dict[str, Any]: """Generate complete professional track with intelligent sample selection. This is the MAIN WORKFLOW for one-prompt music creation. Workflow: 1. Parse description → genre, tempo, key, style 2. Select structure template 3. Use IntelligentSampleSelector to find coherent samples 4. Use IterationEngine to achieve target coherence 5. Use VariationEngine to evolve samples per section 6. Create arrangement in Ableton via LiveBridge 7. Apply automatic mixing 8. Save preset if requested 9. Log all rationale Args: description: Natural language track description structure_type: "tiktok", "short", "standard", "extended" variation_level: "low", "medium", "high" coherence_threshold: Minimum coherence score (default 0.90) include_vocal_placeholder: Add vocal track surprise_mode: Random variation save_as_preset: Save kit as preset Returns: { "success": True, "track_name": str, "structure": List[SectionConfig], "samples_used": Dict[role, SampleKit], "coherence_scores": Dict[str, float], "coherence_overall": float, "rationale_id": str, # Reference to database log "preset_saved": Optional[str], "duration_seconds": float, "warnings": List[str], "next_steps": List[str] } Raises: ProfessionalCoherenceError: If cannot achieve coherence_threshold after all iteration strategies """ import time from typing import List as TypingList start_time = time.time() warnings = [] next_steps = [] # Check initialization if not self._initialized: error_msg = "Coordinator not initialized. Call initialize() first." logger.error(error_msg) return { "success": False, "track_name": None, "structure": [], "samples_used": {}, "coherence_scores": {}, "coherence_overall": 0.0, "rationale_id": None, "preset_saved": None, "duration_seconds": 0.0, "warnings": [error_msg], "next_steps": ["Call coordinator.initialize() first"] } # Check LiveBridge availability (required for Ableton integration) if not self._live_bridge: error_msg = "LiveBridge not available - cannot create arrangement in Ableton" logger.error(error_msg) warnings.append(error_msg) next_steps.append("Ensure Ableton Live connection is active") # Parse description using available components parsed_config = self._parse_description(description) genre = parsed_config.get("genre", "reggaeton") tempo = parsed_config.get("tempo", 95) key = parsed_config.get("key", "Am") style = parsed_config.get("style", "classic") logger.info(f"Parsed description: genre={genre}, tempo={tempo}, key={key}, style={style}") # Generate track name based on parsed config track_name = f"{style.title()} {genre.title()} {structure_type.title()}" # Get structure template based on structure_type structure = self._get_structure_template(structure_type) logger.info(f"Using structure template: {structure_type} with {len(structure)} sections") samples_used = {} coherence_scores = {} try: # Step 1: Intelligent Sample Selection with iteration if SAMPLE_SELECTOR_AVAILABLE and get_selector: logger.info("Starting intelligent sample selection...") selector = get_selector() if selector: # Select samples for genre/key/tempo sample_group = selector.select_for_genre(genre, key if key else None, tempo) if sample_group: # Calculate coherence drums_paths = [] if sample_group.drums.kick: drums_paths.append(sample_group.drums.kick.path) if sample_group.drums.snare: drums_paths.append(sample_group.drums.snare.path) if sample_group.drums.clap: drums_paths.append(sample_group.drums.clap.path) bass_paths = [s.path for s in sample_group.bass[:3]] if sample_group.bass else [] synth_paths = [s.path for s in sample_group.synths[:3]] if sample_group.synths else [] # Calculate coherence for each role drums_coherence = self._calculate_coherence(drums_paths) if drums_paths else 0.0 bass_coherence = self._calculate_coherence(bass_paths) if bass_paths else 0.0 synth_coherence = self._calculate_coherence(synth_paths) if synth_paths else 0.0 coherence_scores = { "drums": drums_coherence, "bass": bass_coherence, "synths": synth_coherence } # Calculate overall coherence (weighted average) coherence_overall = ( drums_coherence * 0.5 + bass_coherence * 0.3 + synth_coherence * 0.2 ) samples_used = { "drums": { "kick": sample_group.drums.kick.path if sample_group.drums.kick else None, "snare": sample_group.drums.snare.path if sample_group.drums.snare else None, "clap": sample_group.drums.clap.path if sample_group.drums.clap else None, "coherence": drums_coherence }, "bass": { "paths": bass_paths, "coherence": bass_coherence }, "synths": { "paths": synth_paths, "coherence": synth_coherence } } logger.info(f"Sample coherence - drums: {drums_coherence:.2f}, " f"bass: {bass_coherence:.2f}, synths: {synth_coherence:.2f}") logger.info(f"Overall coherence: {coherence_overall:.2f} (target: {coherence_threshold:.2f})") # Iterate if coherence below threshold (simple iteration) iteration_attempts = 0 max_iterations = 3 while coherence_overall < coherence_threshold and iteration_attempts < max_iterations: iteration_attempts += 1 logger.info(f"Coherence below threshold, iteration attempt {iteration_attempts}") # Try to get alternative samples alternative_group = selector.select_for_genre(genre, key, tempo) if alternative_group: # Recalculate with new samples new_drums = [s.path for s in [alternative_group.drums.kick, alternative_group.drums.snare, alternative_group.drums.clap] if s] new_bass = [s.path for s in alternative_group.bass[:3]] new_synths = [s.path for s in alternative_group.synths[:3]] new_drums_coherence = self._calculate_coherence(new_drums) new_bass_coherence = self._calculate_coherence(new_bass) new_synth_coherence = self._calculate_coherence(new_synths) new_overall = ( new_drums_coherence * 0.5 + new_bass_coherence * 0.3 + new_synth_coherence * 0.2 ) # Use new samples if better if new_overall > coherence_overall: coherence_overall = new_overall coherence_scores = { "drums": new_drums_coherence, "bass": new_bass_coherence, "synths": new_synth_coherence } samples_used["drums"]["coherence"] = new_drums_coherence samples_used["bass"]["coherence"] = new_bass_coherence samples_used["synths"]["coherence"] = new_synth_coherence logger.info(f"Found better samples, new coherence: {coherence_overall:.2f}") # Check final coherence if coherence_overall < coherence_threshold: warning_msg = (f"Could not achieve target coherence {coherence_threshold:.2f} " f"after {iteration_attempts} iterations. Final: {coherence_overall:.2f}") warnings.append(warning_msg) logger.warning(warning_msg) next_steps.append("Try different genre/key or lower coherence threshold") else: logger.info(f"Achieved target coherence: {coherence_overall:.2f}") else: warnings.append("Sample group not returned from selector") else: warnings.append("Sample selector not available") else: warnings.append("Sample selector module not available - using default samples") next_steps.append("Install sample_selector for intelligent selection") # Step 2: Apply variations per section based on variation_level variation_factor = {"low": 0.2, "medium": 0.5, "high": 0.8}.get(variation_level, 0.5) logger.info(f"Applying variation level '{variation_level}' with factor {variation_factor}") # Surprise mode adds randomness if surprise_mode: import random variation_factor = min(1.0, variation_factor + random.uniform(0.1, 0.3)) logger.info(f"Surprise mode active, adjusted variation factor: {variation_factor:.2f}") warnings.append("Surprise mode enabled - variations may be unconventional") # Step 3: Create arrangement in Ableton via LiveBridge arrangement_created = False if self._live_bridge: try: logger.info("Creating arrangement in Ableton...") # Create tracks track_indices = {} track_types = ["drums", "bass", "synths"] for track_type in track_types: result = self._live_bridge.create_audio_track(-1) if result.get("success"): idx = result.get("data", {}).get("track_index", -1) track_indices[track_type] = idx self._live_bridge.set_track_name(idx, f"{track_type.title()} Track") logger.info(f"Created {track_type} track at index {idx}") # Add vocal placeholder if requested if include_vocal_placeholder: vocal_result = self._live_bridge.create_audio_track(-1) if vocal_result.get("success"): vocal_idx = vocal_result.get("data", {}).get("track_index", -1) track_indices["vocal"] = vocal_idx self._live_bridge.set_track_name(vocal_idx, "Vocal Placeholder") logger.info(f"Created vocal placeholder track at index {vocal_idx}") # Place clips for each section current_bar = 0 for section in structure: section_type = section.get("type", "verse") bars = section.get("bars", 8) elements = section.get("elements", ["drums", "bass"]) # Apply variation to elements based on section type varied_elements = self._apply_section_variation( elements, section_type, variation_factor ) for element in varied_elements: if element in track_indices: # In real implementation, this would load actual samples logger.debug(f"Placing {element} clip at bar {current_bar} " f"for {bars} bars ({section_type})") current_bar += bars arrangement_created = True logger.info(f"Arrangement created with {len(track_indices)} tracks, " f"{current_bar} total bars") next_steps.append("Review arrangement in Ableton and adjust as needed") except Exception as e: error_msg = f"Failed to create arrangement: {str(e)}" logger.exception(error_msg) warnings.append(error_msg) next_steps.append("Check LiveBridge connection and retry") else: warnings.append("LiveBridge unavailable - arrangement not created in Ableton") next_steps.append("Ensure Ableton connection is active and retry") # Step 4: Apply automatic mixing if MIXING_ENGINE_AVAILABLE and self._mixing_engine and arrangement_created: try: mix_preset = self._determine_mix_preset(genre, style) logger.info(f"Applying mix preset: {mix_preset}") mix_result = self.apply_professional_mix(mix_preset) if mix_result.success: logger.info("Professional mix applied successfully") next_steps.append("Fine-tune mix levels if needed") else: warnings.append(f"Mix application: {mix_result.message}") next_steps.append("Apply manual mixing") except Exception as e: warnings.append(f"Mix application failed: {str(e)}") next_steps.append("Apply manual mixing in Ableton") else: warnings.append("Automatic mixing skipped (engine unavailable or no arrangement)") next_steps.append("Apply manual mixing in Ableton") # Step 5: Log rationale (simplified - would use proper logging in production) rationale_id = f"track_{int(start_time)}_{track_name.replace(' ', '_').lower()}" logger.info(f"Rationale logged with ID: {rationale_id}") # Step 6: Save preset if requested preset_saved = None if save_as_preset and samples_used: try: preset_name = f"{track_name.replace(' ', '_')}_{int(start_time)}" # In production, this would save to actual preset storage preset_saved = preset_name logger.info(f"Preset saved as: {preset_name}") next_steps.append(f"Preset '{preset_name}' available for future use") except Exception as e: warnings.append(f"Failed to save preset: {str(e)}") duration = time.time() - start_time logger.info(f"Track generation completed in {duration:.2f} seconds") # Calculate overall coherence if not already done coherence_overall = sum(coherence_scores.values()) / len(coherence_scores) if coherence_scores else 0.0 return { "success": True, "track_name": track_name, "structure": structure, "samples_used": samples_used, "coherence_scores": coherence_scores, "coherence_overall": coherence_overall, "rationale_id": rationale_id, "preset_saved": preset_saved, "duration_seconds": duration, "warnings": warnings, "next_steps": next_steps } except Exception as e: error_msg = f"Track generation failed: {str(e)}" logger.exception(error_msg) duration = time.time() - start_time return { "success": False, "track_name": track_name if 'track_name' in locals() else None, "structure": structure if 'structure' in locals() else [], "samples_used": samples_used, "coherence_scores": coherence_scores, "coherence_overall": 0.0, "rationale_id": None, "preset_saved": None, "duration_seconds": duration, "warnings": warnings + [error_msg], "next_steps": ["Check logs for details", "Retry with different parameters"] } def _parse_description(self, description: str) -> Dict[str, Any]: """Parse natural language description into configuration.""" description_lower = description.lower() # Default config config = { "genre": "reggaeton", "tempo": 95, "key": "Am", "style": "classic" } # Detect genre if "dembow" in description_lower: config["genre"] = "reggaeton" config["style"] = "dembow" config["tempo"] = 90 elif "perreo" in description_lower: config["genre"] = "reggaeton" config["style"] = "perreo" config["tempo"] = 95 elif "romantic" in description_lower or "romantico" in description_lower: config["genre"] = "reggaeton" config["style"] = "romantico" config["tempo"] = 88 elif "trap" in description_lower: config["genre"] = "trap" config["style"] = "dark" config["tempo"] = 140 elif "house" in description_lower: config["genre"] = "house" config["style"] = "classic" config["tempo"] = 128 # Detect tempo import re tempo_match = re.search(r'(\d+)\s*bpm', description_lower) if tempo_match: config["tempo"] = int(tempo_match.group(1)) elif "slow" in description_lower: config["tempo"] = max(80, config["tempo"] - 10) elif "fast" in description_lower or "upbeat" in description_lower: config["tempo"] = min(140, config["tempo"] + 15) # Detect key key_match = re.search(r'\b([A-G][#b]?)\s*(major|minor|m)?\b', description, re.IGNORECASE) if key_match: key = key_match.group(1).upper() is_minor = key_match.group(2) if is_minor and ('minor' in is_minor.lower() or is_minor.lower() == 'm'): config["key"] = key + "m" else: config["key"] = key # Detect style keywords if "dark" in description_lower or "heavy" in description_lower: config["style"] = "dark" elif "bright" in description_lower or "happy" in description_lower: config["style"] = "bright" elif "minimal" in description_lower: config["style"] = "minimal" elif "club" in description_lower: config["style"] = "club" return config def _get_structure_template(self, structure_type: str) -> TypingList[Dict[str, Any]]: """Get song structure template based on type.""" templates = { "tiktok": [ {"type": "hook", "bars": 8, "elements": ["drums", "bass"]}, {"type": "drop", "bars": 8, "elements": ["drums", "bass", "synths"]} ], "short": [ {"type": "intro", "bars": 4, "elements": ["drums"]}, {"type": "verse", "bars": 8, "elements": ["drums", "bass"]}, {"type": "chorus", "bars": 8, "elements": ["drums", "bass", "synths"]}, {"type": "outro", "bars": 4, "elements": ["drums"]} ], "standard": [ {"type": "intro", "bars": 8, "elements": ["drums"]}, {"type": "verse", "bars": 16, "elements": ["drums", "bass"]}, {"type": "pre_chorus", "bars": 8, "elements": ["drums", "bass", "synths"]}, {"type": "chorus", "bars": 16, "elements": ["drums", "bass", "synths"]}, {"type": "verse", "bars": 16, "elements": ["drums", "bass"]}, {"type": "chorus", "bars": 16, "elements": ["drums", "bass", "synths"]}, {"type": "bridge", "bars": 8, "elements": ["bass", "synths"]}, {"type": "chorus", "bars": 16, "elements": ["drums", "bass", "synths"]}, {"type": "outro", "bars": 8, "elements": ["drums"]} ], "extended": [ {"type": "intro", "bars": 16, "elements": ["drums"]}, {"type": "build", "bars": 8, "elements": ["drums", "synths"]}, {"type": "drop", "bars": 16, "elements": ["drums", "bass", "synths"]}, {"type": "verse", "bars": 16, "elements": ["drums", "bass"]}, {"type": "build", "bars": 8, "elements": ["drums", "synths"]}, {"type": "drop", "bars": 16, "elements": ["drums", "bass", "synths"]}, {"type": "breakdown", "bars": 16, "elements": ["synths"]}, {"type": "build", "bars": 8, "elements": ["drums", "synths"]}, {"type": "drop", "bars": 16, "elements": ["drums", "bass", "synths"]}, {"type": "outro", "bars": 16, "elements": ["drums"]} ] } return templates.get(structure_type, templates["standard"]) def _calculate_coherence(self, sample_paths: TypingList[str]) -> float: """Calculate coherence score for a set of samples.""" if not sample_paths or len(sample_paths) < 2: return 1.0 # Single sample has perfect coherence # If metadata store available, use spectral features if self._metadata_store: try: features_list = [] for path in sample_paths: sample = self._metadata_store.get_sample_by_path(path) if sample and hasattr(sample, 'spectral_centroid'): features_list.append(sample.spectral_centroid) if len(features_list) >= 2: # Calculate variance of spectral features import statistics mean_val = statistics.mean(features_list) if mean_val == 0: return 1.0 variance = statistics.variance(features_list) if len(features_list) > 1 else 0 # Coherence is inverse of normalized variance coherence = max(0.0, 1.0 - (variance / (mean_val ** 2)) if mean_val else 1.0) return min(1.0, coherence) except Exception as e: logger.warning(f"Coherence calculation failed: {e}") # Fallback: assume high coherence return 0.85 def _apply_section_variation(self, elements: TypingList[str], section_type: str, variation_factor: float) -> TypingList[str]: """Apply variation to elements based on section type and factor.""" import random base_elements = elements.copy() # Adjust elements based on section type if section_type in ["intro", "outro"]: # Sparse arrangement if variation_factor > 0.5 and "synths" in base_elements: base_elements.remove("synths") elif section_type == "chorus": # Full arrangement if "synths" not in base_elements and variation_factor > 0.3: base_elements.append("synths") elif section_type in ["drop", "build"]: # Maximum elements for elem in ["drums", "bass", "synths"]: if elem not in base_elements: base_elements.append(elem) elif section_type == "breakdown": # Minimal drums if "drums" in base_elements and variation_factor > 0.4: base_elements.remove("drums") # Apply random variation if variation_factor > 0.6 and random.random() < variation_factor: # Randomly swap an element all_elements = ["drums", "bass", "synths", "fx"] available = [e for e in all_elements if e not in base_elements] if available and base_elements: # Remove one, add one if random.random() < 0.5: base_elements.pop(random.randint(0, len(base_elements) - 1)) base_elements.append(random.choice(available)) return base_elements def _determine_mix_preset(self, genre: str, style: str) -> str: """Determine appropriate mix preset based on genre and style.""" preset_map = { ("reggaeton", "dembow"): "reggaeton_club", ("reggaeton", "perreo"): "perreo", ("reggaeton", "romantico"): "romantico", ("reggaeton", "classic"): "reggaeton_club", ("trap", "dark"): "trap_dark", ("trap", "bright"): "trap_clean", ("house", "classic"): "house_club", ("house", "minimal"): "minimal" } return preset_map.get((genre, style), "reggaeton_club") # ======================================================================= # RECORDING CALLBACKS # ======================================================================= def _on_recording_state_change(self, old_state, new_state): """Callback when recording state changes.""" logger.info(f"Recording state: {old_state} -> {new_state}") def _on_recording_progress(self, progress: float): """Callback with recording progress (0.0-1.0).""" logger.debug(f"Recording progress: {progress:.1%}") def _on_recording_error(self, error: Exception): """Callback on recording error.""" logger.error(f"Recording error: {error}") def _on_recording_completed(self, clip_ids: List[str]): """Callback when recording completes successfully.""" logger.info(f"Recording completed with {len(clip_ids)} new clips") # ============================================================================= # HELPER FUNCTIONS # ============================================================================= # Singleton storage _coordinator_singleton: Optional[SeniorArchitectureCoordinator] = None def create_coordinator(song, connection, db_path: Optional[str] = None) -> Dict[str, Any]: """ Factory function to create and initialize coordinator. This is the recommended way to create a coordinator instance. It creates the coordinator and immediately initializes all components. Args: song: Ableton Live Song object connection: MCP TCP connection db_path: Optional path to metadata database Returns: Dictionary with: - coordinator: SeniorArchitectureCoordinator instance (or None on failure) - status: Initialization status dict """ try: coord = SeniorArchitectureCoordinator(song, connection, db_path) status = coord.initialize() return { "coordinator": coord, "status": status } except Exception as e: logger.exception("Failed to create coordinator") return { "coordinator": None, "status": { "initialized": False, "error": str(e) } } def get_coordinator_singleton(song=None, connection=None, db_path: Optional[str] = None) -> Optional[SeniorArchitectureCoordinator]: """ Get or create singleton instance. This function returns the existing coordinator if one exists, or creates a new one if needed. If song and connection are provided but no coordinator exists, it will create and initialize one. Args: song: Ableton Live Song object (required for first creation) connection: MCP TCP connection (required for first creation) db_path: Optional path to metadata database Returns: SeniorArchitectureCoordinator instance or None """ global _coordinator_singleton if _coordinator_singleton is not None: return _coordinator_singleton if song is not None and connection is not None: result = create_coordinator(song, connection, db_path) _coordinator_singleton = result.get("coordinator") return _coordinator_singleton return None def reset_coordinator_singleton(): """Reset the singleton instance. Useful for testing.""" global _coordinator_singleton _coordinator_singleton = None logger.info("Coordinator singleton reset") # ============================================================================= # COMPATIBILITY EXPORTS # ============================================================================= __all__ = [ "SeniorArchitectureCoordinator", "CoordinatorResult", "create_coordinator", "get_coordinator_singleton", "reset_coordinator_singleton", ]