- 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
549 lines
20 KiB
Python
549 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""Final validation script for Senior Architecture.
|
|
|
|
Validates:
|
|
1. All new modules import successfully
|
|
2. SQLite database is accessible
|
|
3. Metadata store works without numpy
|
|
4. ArrangementRecorder state machine functions
|
|
5. LiveBridge connects to Ableton
|
|
6. Integration coordinator initializes
|
|
7. End-to-end workflow executes
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import traceback
|
|
import argparse
|
|
import tempfile
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
class ValidationRunner:
|
|
"""Runs all validations and generates report."""
|
|
|
|
def __init__(self, verbose=False):
|
|
self.verbose = verbose
|
|
self.results = []
|
|
self.errors = []
|
|
self.warnings = []
|
|
self.fix_suggestions = {}
|
|
|
|
def run_all(self, selective=None):
|
|
"""Execute all validation checks."""
|
|
all_checks = [
|
|
("Module Imports", self._check_imports),
|
|
("SQLite Database", self._check_database),
|
|
("Metadata Store", self._check_metadata_store),
|
|
("Numpy Independence", self._check_numpy_independence),
|
|
("ArrangementRecorder", self._check_arrangement_recorder),
|
|
("LiveBridge", self._check_live_bridge),
|
|
("Integration", self._check_integration),
|
|
("Ableton Connection", self._check_ableton_connection),
|
|
]
|
|
|
|
# Filter if selective checks requested
|
|
if selective:
|
|
all_checks = [
|
|
(name, func) for name, func in all_checks
|
|
if name.lower() in selective
|
|
]
|
|
if not all_checks:
|
|
print(f"Error: No checks match {selective}")
|
|
return False
|
|
|
|
for name, check_func in all_checks:
|
|
if self.verbose:
|
|
print(f"\nRunning: {name}...")
|
|
|
|
try:
|
|
result = check_func()
|
|
self.results.append({
|
|
"name": name,
|
|
"status": "PASS" if result else "FAIL",
|
|
"timestamp": datetime.now().isoformat()
|
|
})
|
|
if not result:
|
|
self.fix_suggestions[name] = self._generate_fix_suggestion(name, Exception("Check returned False"))
|
|
|
|
except Exception as e:
|
|
self.results.append({
|
|
"name": name,
|
|
"status": "ERROR",
|
|
"error": str(e),
|
|
"traceback": traceback.format_exc()
|
|
})
|
|
self.errors.append((name, e))
|
|
self.fix_suggestions[name] = self._generate_fix_suggestion(name, e)
|
|
|
|
return self._generate_report()
|
|
|
|
def _check_imports(self):
|
|
"""Check all new modules import successfully."""
|
|
imports = [
|
|
'mcp_server.engines.metadata_store',
|
|
'mcp_server.engines.abstract_analyzer',
|
|
'mcp_server.engines.arrangement_recorder',
|
|
'mcp_server.engines.live_bridge',
|
|
'mcp_server.integration',
|
|
]
|
|
|
|
for module in imports:
|
|
try:
|
|
__import__(module)
|
|
if self.verbose:
|
|
print(f" [OK] Imported {module}")
|
|
except ImportError as e:
|
|
if self.verbose:
|
|
print(f" [FAIL] Failed to import {module}: {e}")
|
|
raise ImportError(f"Failed to import {module}: {e}")
|
|
|
|
return True
|
|
|
|
def _check_database(self):
|
|
"""Check SQLite database is accessible."""
|
|
try:
|
|
# Try to create in-memory database
|
|
conn = sqlite3.connect(':memory:')
|
|
conn.execute('SELECT 1')
|
|
conn.close()
|
|
if self.verbose:
|
|
print(" [OK] SQLite in-memory database created")
|
|
return True
|
|
except Exception as e:
|
|
if self.verbose:
|
|
print(f" [FAIL] SQLite error: {e}")
|
|
raise
|
|
|
|
def _check_metadata_store(self):
|
|
"""Check metadata store works without numpy."""
|
|
from mcp_server.engines.metadata_store import SampleMetadataStore, SampleFeatures
|
|
|
|
# Create temp database
|
|
fd, path = tempfile.mkstemp(suffix='.db')
|
|
try:
|
|
store = SampleMetadataStore(path)
|
|
store.init_database()
|
|
if self.verbose:
|
|
print(f" [OK] Database initialized at {path}")
|
|
|
|
# Save sample features
|
|
features = SampleFeatures(
|
|
path="/test/sample.wav",
|
|
bpm=95.0,
|
|
key="Am",
|
|
duration=2.0,
|
|
rms=-12.0,
|
|
spectral_centroid=1000.0,
|
|
spectral_rolloff=5000.0,
|
|
zero_crossing_rate=0.1,
|
|
mfcc_1=0.1, mfcc_2=0.1, mfcc_3=0.1, mfcc_4=0.1,
|
|
mfcc_5=0.1, mfcc_6=0.1, mfcc_7=0.1, mfcc_8=0.1,
|
|
mfcc_9=0.1, mfcc_10=0.1, mfcc_11=0.1, mfcc_12=0.1, mfcc_13=0.1
|
|
)
|
|
store.save_sample_features("/test/sample.wav", features)
|
|
if self.verbose:
|
|
print(" [OK] Sample features saved")
|
|
|
|
# Retrieve
|
|
retrieved = store.get_sample_features("/test/sample.wav")
|
|
assert retrieved is not None, "Retrieved features should not be None"
|
|
assert retrieved.bpm == 95.0, f"BPM should be 95.0, got {retrieved.bpm}"
|
|
if self.verbose:
|
|
print(" [OK] Sample features retrieved correctly")
|
|
|
|
return True
|
|
finally:
|
|
try:
|
|
os.close(fd)
|
|
os.unlink(path)
|
|
except:
|
|
pass
|
|
|
|
def _check_numpy_independence(self):
|
|
"""Verify core functionality works without numpy."""
|
|
# Check if numpy is available
|
|
try:
|
|
import numpy
|
|
numpy_available = True
|
|
except ImportError:
|
|
numpy_available = False
|
|
|
|
if not numpy_available:
|
|
if self.verbose:
|
|
print(" [INFO] Numpy not available - skipping independence test")
|
|
return True # Already independent by absence
|
|
|
|
# Temporarily hide numpy
|
|
import sys
|
|
numpy_backup = sys.modules.pop('numpy', None)
|
|
|
|
try:
|
|
# Re-import metadata store (should work without numpy)
|
|
if 'mcp_server.engines.metadata_store' in sys.modules:
|
|
del sys.modules['mcp_server.engines.metadata_store']
|
|
|
|
from mcp_server.engines.metadata_store import SampleMetadataStore
|
|
if self.verbose:
|
|
print(" [OK] Metadata store imports without numpy")
|
|
return True
|
|
finally:
|
|
# Restore numpy
|
|
if numpy_backup:
|
|
sys.modules['numpy'] = numpy_backup
|
|
|
|
def _check_arrangement_recorder(self):
|
|
"""Check ArrangementRecorder state machine."""
|
|
from mcp_server.engines.arrangement_recorder import (
|
|
ArrangementRecorder, RecordingState, RecordingConfig
|
|
)
|
|
|
|
# Create mock objects
|
|
class MockSong:
|
|
def __init__(self):
|
|
self.tempo = 95.0
|
|
self.current_song_time = 0.0
|
|
self.arrangement_overdub = False
|
|
self.is_playing = False
|
|
|
|
class MockConn:
|
|
pass
|
|
|
|
recorder = ArrangementRecorder(MockSong(), MockConn())
|
|
|
|
# Check initial state
|
|
assert recorder.get_state() == RecordingState.IDLE, \
|
|
f"Initial state should be IDLE, got {recorder.get_state()}"
|
|
|
|
if self.verbose:
|
|
print(" [OK] Initial state is IDLE")
|
|
|
|
# Check config can be created
|
|
config = RecordingConfig(
|
|
duration_bars=4.0,
|
|
tempo=95.0,
|
|
start_bar=0.0,
|
|
scene_index=0
|
|
)
|
|
assert config.duration_bars == 4.0, \
|
|
f"Duration bars mismatch: expected 4.0, got {config.duration_bars}"
|
|
assert config.tempo == 95.0, \
|
|
f"Tempo mismatch: expected 95.0, got {config.tempo}"
|
|
|
|
if self.verbose:
|
|
print(" [OK] RecordingConfig created successfully")
|
|
print(" [OK] State transitions available:")
|
|
for state in RecordingState:
|
|
print(f" - {state.name}")
|
|
|
|
return True
|
|
|
|
def _check_live_bridge(self):
|
|
"""Check LiveBridge initializes."""
|
|
from mcp_server.engines.live_bridge import AbletonLiveBridge
|
|
|
|
class MockSong:
|
|
pass
|
|
|
|
class MockConn:
|
|
pass
|
|
|
|
bridge = AbletonLiveBridge(MockSong(), MockConn())
|
|
assert bridge is not None, "LiveBridge should initialize"
|
|
|
|
if self.verbose:
|
|
print(" [OK] LiveBridge initialized")
|
|
print(" [OK] Available methods:")
|
|
methods = [m for m in dir(bridge) if not m.startswith('_')]
|
|
for method in methods[:5]:
|
|
print(f" - {method}")
|
|
if len(methods) > 5:
|
|
print(f" ... and {len(methods) - 5} more")
|
|
|
|
return True
|
|
|
|
def _check_integration(self):
|
|
"""Check integration coordinator."""
|
|
from mcp_server.integration import SeniorArchitectureCoordinator
|
|
|
|
class MockSong:
|
|
pass
|
|
|
|
class MockConn:
|
|
pass
|
|
|
|
coord = SeniorArchitectureCoordinator(MockSong(), MockConn())
|
|
assert coord is not None, "Coordinator should initialize"
|
|
|
|
if self.verbose:
|
|
print(" [OK] SeniorArchitectureCoordinator initialized")
|
|
|
|
return True
|
|
|
|
def _check_ableton_connection(self):
|
|
"""Check Ableton Live is accessible."""
|
|
# Try to ping Ableton via existing wrapper
|
|
try:
|
|
import socket
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(2)
|
|
result = s.connect_ex(('127.0.0.1', 9877))
|
|
s.close()
|
|
|
|
if result == 0:
|
|
if self.verbose:
|
|
print(" [OK] Ableton Live TCP server responding on port 9877")
|
|
return True
|
|
else:
|
|
if self.verbose:
|
|
print(f" [WARN] Ableton Live not available on port 9877 (error code: {result})")
|
|
print(" [WARN] This is OK - Ableton may not be running")
|
|
self.warnings.append("Ableton not running - some checks skipped")
|
|
return False # Not a failure, just not available
|
|
except Exception as e:
|
|
if self.verbose:
|
|
print(f" [WARN] Connection check error: {e}")
|
|
self.warnings.append(f"Connection check: {e}")
|
|
return False
|
|
|
|
def _generate_fix_suggestion(self, check_name, error):
|
|
"""Generate fix suggestion for a failed check."""
|
|
suggestions = {
|
|
"Module Imports": """
|
|
Fix: Ensure all new modules exist in mcp_server/engines/:
|
|
1. metadata_store.py - SQLite-based sample metadata
|
|
2. abstract_analyzer.py - Hybrid feature extraction
|
|
3. arrangement_recorder.py - Recording state machine
|
|
4. live_bridge.py - Direct Ableton API execution
|
|
5. integration.py - Coordinator
|
|
|
|
Run: python -m py_compile on each file to check for syntax errors.
|
|
""",
|
|
"SQLite Database": """
|
|
Fix: SQLite is part of Python standard library. If this fails:
|
|
1. Check Python installation: python --version
|
|
2. Verify sqlite3 module: python -c "import sqlite3; print(sqlite3.version)"
|
|
3. Reinstall Python if necessary
|
|
""",
|
|
"Metadata Store": """
|
|
Fix: If metadata store fails:
|
|
1. Check database schema in metadata_store.py
|
|
2. Verify SampleFeatures dataclass definition
|
|
3. Check for SQL syntax errors in init_database()
|
|
4. Ensure proper error handling in save/get methods
|
|
""",
|
|
"Numpy Independence": """
|
|
Fix: If numpy independence fails:
|
|
1. Ensure metadata_store.py has no 'import numpy' at top level
|
|
2. Move numpy imports inside functions that need them
|
|
3. Use type checking (TYPE_CHECKING) for numpy type hints
|
|
4. Provide fallback implementations for numpy operations
|
|
""",
|
|
"ArrangementRecorder": """
|
|
Fix: If ArrangementRecorder fails:
|
|
1. Check RecordingState enum definition
|
|
2. Verify RecordingConfig dataclass
|
|
3. Ensure proper mock objects for testing
|
|
4. Check state transition logic
|
|
""",
|
|
"LiveBridge": """
|
|
Fix: If LiveBridge fails:
|
|
1. Check Ableton Live is running with Remote Script loaded
|
|
2. Verify TCP connection on port 9877
|
|
3. Check Live API access in __init__.py
|
|
4. Verify song and connection objects are properly passed
|
|
""",
|
|
"Integration": """
|
|
Fix: If Integration coordinator fails:
|
|
1. Check all dependencies are imported correctly
|
|
2. Verify mode detection logic (numpy/librosa availability)
|
|
3. Check for circular imports
|
|
4. Ensure proper initialization of sub-components
|
|
""",
|
|
"Ableton Connection": """
|
|
Fix: If Ableton connection fails:
|
|
1. Start Ableton Live 12 Suite
|
|
2. Verify AbletonMCP_AI is selected in Preferences > MIDI > Control Surface
|
|
3. Check that __init__.py is in correct location
|
|
4. Verify port 9877 is not blocked by firewall
|
|
5. Check Ableton log for errors
|
|
""",
|
|
}
|
|
|
|
return suggestions.get(check_name, f"""
|
|
Fix: General troubleshooting for {check_name}:
|
|
1. Check error traceback above
|
|
2. Verify file exists and has no syntax errors
|
|
3. Check import paths are correct
|
|
4. Run with --verbose for more details
|
|
5. Check AGENTS.md for architecture details
|
|
""")
|
|
|
|
def _generate_report(self):
|
|
"""Generate validation report."""
|
|
total = len(self.results)
|
|
passed = sum(1 for r in self.results if r['status'] == 'PASS')
|
|
failed = sum(1 for r in self.results if r['status'] == 'FAIL')
|
|
errors = sum(1 for r in self.results if r['status'] == 'ERROR')
|
|
|
|
report = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"summary": {
|
|
"total": total,
|
|
"passed": passed,
|
|
"failed": failed,
|
|
"errors": errors,
|
|
"success_rate": passed / total if total > 0 else 0
|
|
},
|
|
"results": self.results,
|
|
"warnings": self.warnings,
|
|
"errors": [{"check": name, "error": str(e)} for name, e in self.errors]
|
|
}
|
|
|
|
# Print to console
|
|
print("\n" + "="*60)
|
|
print("SENIOR ARCHITECTURE VALIDATION REPORT")
|
|
print("="*60)
|
|
print(f"Timestamp: {report['timestamp']}")
|
|
print(f"Passed: {passed}/{total}")
|
|
print(f"Failed: {failed}/{total}")
|
|
print(f"Errors: {errors}/{total}")
|
|
print(f"Success Rate: {report['summary']['success_rate']:.1%}")
|
|
|
|
if self.warnings:
|
|
print(f"\nWarnings: {len(self.warnings)}")
|
|
for warning in self.warnings:
|
|
print(f" [WARN] {warning}")
|
|
|
|
print("-"*60)
|
|
|
|
for result in self.results:
|
|
status_icon = "[PASS]" if result['status'] == 'PASS' else "[FAIL]" if result['status'] == 'FAIL' else "[WARN]"
|
|
print(f"{status_icon} {result['name']}: {result['status']}")
|
|
|
|
if self.verbose and 'error' in result and result['error']:
|
|
print(f" Error: {result['error'][:100]}...")
|
|
|
|
print("="*60)
|
|
|
|
# Print fix suggestions for failed checks
|
|
if self.fix_suggestions:
|
|
print("\n" + "="*60)
|
|
print("FIX SUGGESTIONS")
|
|
print("="*60)
|
|
for check_name, suggestion in self.fix_suggestions.items():
|
|
print(f"\n{check_name}:")
|
|
print(suggestion)
|
|
print("="*60)
|
|
|
|
# Save JSON report
|
|
report_path = "senior_validation_report.json"
|
|
with open(report_path, 'w') as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"\nFull report saved to: {report_path}")
|
|
|
|
# Also save fix suggestions
|
|
if self.fix_suggestions:
|
|
fixes_path = "senior_validation_fixes.txt"
|
|
with open(fixes_path, 'w') as f:
|
|
f.write("SENIOR ARCHITECTURE - FIX SUGGESTIONS\n")
|
|
f.write("="*60 + "\n\n")
|
|
for check_name, suggestion in self.fix_suggestions.items():
|
|
f.write(f"{check_name}:\n")
|
|
f.write(suggestion + "\n")
|
|
print(f"Fix suggestions saved to: {fixes_path}")
|
|
|
|
return report['summary']['success_rate'] >= 0.8 # 80% pass threshold
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate Senior Architecture implementation",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python validate_senior.py # Run all checks
|
|
python validate_senior.py -v # Run with verbose output
|
|
python validate_senior.py --list # List available checks
|
|
python validate_senior.py -c imports integration # Run specific checks
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
'-v', '--verbose',
|
|
action='store_true',
|
|
help='Enable verbose output with detailed information'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'-c', '--checks',
|
|
nargs='+',
|
|
metavar='CHECK',
|
|
help='Run only specific checks (space-separated names)'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--list',
|
|
action='store_true',
|
|
help='List all available checks and exit'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--threshold',
|
|
type=float,
|
|
default=0.8,
|
|
help='Success rate threshold (default: 0.8 = 80 percent)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.list:
|
|
print("Available validation checks:")
|
|
checks = [
|
|
"Module Imports - Check all new modules import successfully",
|
|
"SQLite Database - Check SQLite database is accessible",
|
|
"Metadata Store - Check metadata store works without numpy",
|
|
"Numpy Independence - Verify core functionality works without numpy",
|
|
"ArrangementRecorder - Check ArrangementRecorder state machine",
|
|
"LiveBridge - Check LiveBridge initializes",
|
|
"Integration - Check integration coordinator",
|
|
"Ableton Connection - Check Ableton Live is accessible",
|
|
]
|
|
for check in checks:
|
|
print(f" - {check}")
|
|
return 0
|
|
|
|
# Normalize check names
|
|
selective = None
|
|
if args.checks:
|
|
selective = [name.lower().replace("_", " ") for name in args.checks]
|
|
print(f"Running selective validation: {selective}")
|
|
|
|
runner = ValidationRunner(verbose=args.verbose)
|
|
success = runner.run_all(selective=selective)
|
|
|
|
# Apply custom threshold
|
|
if selective:
|
|
total = len(runner.results)
|
|
passed = sum(1 for r in runner.results if r['status'] == 'PASS')
|
|
success_rate = passed / total if total > 0 else 0
|
|
success = success_rate >= args.threshold
|
|
|
|
if success:
|
|
print("\n[PASS] Senior Architecture validation PASSED")
|
|
print(f" Success rate meets {args.threshold:.0%} threshold")
|
|
return 0
|
|
else:
|
|
print("\n[FAIL] Senior Architecture validation FAILED")
|
|
print(f" Success rate below {args.threshold:.0%} threshold")
|
|
print("\nTo fix issues:")
|
|
print(" 1. Check senior_validation_fixes.txt for suggestions")
|
|
print(" 2. Run with --verbose to see detailed errors")
|
|
print(" 3. Review AGENTS.md for architecture details")
|
|
return 1
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|