feat: SDD workflow — test sync, song generation + validation, ReaScript hybrid pipeline
- compose-test-sync: fix 3 failing tests (NOTE_TO_MIDI, DrumLoopAnalyzer mock, section name) - generate-song: CLI wrapper + RPP validator (6 structural checks) + 4 e2e tests - reascript-hybrid: ReaScriptGenerator + command protocol + CLI + 16 unit tests - 110/110 tests passing - Full SDD cycle (propose→spec→design→tasks→apply→verify) for all 3 changes
This commit is contained in:
145
scripts/run_in_reaper.py
Normal file
145
scripts/run_in_reaper.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""CLI to run Phase 2 ReaScript refinement on a .rpp file.
|
||||
|
||||
Usage: python scripts/run_in_reaper.py <rpp_path> [--output <wav_path>] [--timeout <seconds>]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]))
|
||||
|
||||
from src.reaper_scripting import ReaScriptGenerator
|
||||
from src.reaper_scripting.commands import (
|
||||
ReaScriptCommand,
|
||||
ReaScriptResult,
|
||||
read_result,
|
||||
write_command,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run Phase 2 ReaScript refinement on a .rpp file"
|
||||
)
|
||||
parser.add_argument("rpp_path", help="Path to .rpp file")
|
||||
parser.add_argument(
|
||||
"--output", "-o", help="Path for rendered WAV output", default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=int,
|
||||
default=120,
|
||||
help="Timeout in seconds for polling result (default: 120)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
rpp_path = Path(args.rpp_path)
|
||||
if not rpp_path.exists():
|
||||
print(f"ERROR: .rpp file not found: {rpp_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Resolve render_path: default to .rpp folder with _rendered.wav
|
||||
if args.output:
|
||||
render_path = Path(args.output)
|
||||
else:
|
||||
render_path = rpp_path.parent / f"{rpp_path.stem}_rendered.wav"
|
||||
|
||||
# 1. Build command
|
||||
command = ReaScriptCommand(
|
||||
version=1,
|
||||
action="calibrate",
|
||||
rpp_path=str(rpp_path.resolve()),
|
||||
render_path=str(render_path.resolve()),
|
||||
timeout=args.timeout,
|
||||
track_calibration=[],
|
||||
)
|
||||
|
||||
# 2. Generate ReaScript
|
||||
generator = ReaScriptGenerator()
|
||||
# Write to a fixed path - in real usage this would be REAPER ResourcePath()/scripts/
|
||||
reaper_scripts_dir = Path(sys.path[1]) / "scripts" if len(sys.path) > 1 else Path("scripts")
|
||||
script_path = reaper_scripts_dir / "fl_control_phase2.py"
|
||||
generator.generate(script_path, command)
|
||||
print(f"Generated ReaScript: {script_path}")
|
||||
|
||||
# 3. Write command JSON (same directory as script)
|
||||
cmd_json_path = reaper_scripts_dir / "fl_control_command.json"
|
||||
write_command(cmd_json_path, command)
|
||||
print(f"Wrote command JSON: {cmd_json_path}")
|
||||
|
||||
# 4. Poll for result JSON
|
||||
res_json_path = reaper_scripts_dir / "fl_control_result.json"
|
||||
timeout = args.timeout
|
||||
poll_interval = 2.0
|
||||
elapsed = 0.0
|
||||
|
||||
print(f"Polling for result at {res_json_path} (timeout={timeout}s)...")
|
||||
while elapsed < timeout:
|
||||
if res_json_path.exists():
|
||||
break
|
||||
time.sleep(poll_interval)
|
||||
elapsed += poll_interval
|
||||
|
||||
if not res_json_path.exists():
|
||||
print("TIMEOUT: result JSON not found within timeout", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# 5. Read and print results
|
||||
try:
|
||||
result = read_result(res_json_path)
|
||||
except Exception as e:
|
||||
print(f"ERROR: failed to read result: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if result.status == "error":
|
||||
print(f"REAPER error: {result.message}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Print LUFS metrics
|
||||
print("\n=== Phase 2 Result ===")
|
||||
print(f"Status: {result.status}")
|
||||
if result.lufs is not None:
|
||||
print(f"LUFS: {result.lufs}")
|
||||
if result.integrated_lufs is not None:
|
||||
print(f"Integrated LUFS: {result.integrated_lufs}")
|
||||
if result.short_term_lufs is not None:
|
||||
print(f"Short-term LUFS: {result.short_term_lufs}")
|
||||
print(f"Tracks verified: {result.tracks_verified}")
|
||||
if result.fx_errors:
|
||||
print(f"FX errors: {json.dumps(result.fx_errors, indent=2)}")
|
||||
print(f"Message: {result.message}")
|
||||
|
||||
# 6. Write output files
|
||||
output_dir = rpp_path.parent / "output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# LUFS metrics
|
||||
lufs_data = {
|
||||
"lufs": result.lufs,
|
||||
"integrated_lufs": result.integrated_lufs,
|
||||
"short_term_lufs": result.short_term_lufs,
|
||||
"render_path": str(render_path.resolve()),
|
||||
}
|
||||
lufs_path = output_dir / f"{rpp_path.stem}_lufs.json"
|
||||
with open(lufs_path, "w", encoding="utf-8") as f:
|
||||
json.dump(lufs_data, f, indent=2)
|
||||
print(f"Wrote LUFS data: {lufs_path}")
|
||||
|
||||
# FX errors
|
||||
if result.fx_errors:
|
||||
fx_path = output_dir / f"{rpp_path.stem}_fx_errors.json"
|
||||
with open(fx_path, "w", encoding="utf-8") as f:
|
||||
json.dump(result.fx_errors, f, indent=2)
|
||||
print(f"Wrote FX errors: {fx_path}")
|
||||
|
||||
print("\nPhase 2 complete.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user