#!/usr/bin/env python3 """ SWF to MP4 Converter - Main Orchestrator Handles loop detection and interactive branch capture. """ import argparse import json import logging import os import sys import time from pathlib import Path from recorder import Recorder from loop_detector import LoopDetector from interaction import InteractionController from config_manager import ConfigManager from swf_inspector import SWFInspector logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler("logs/convert.log"), ], ) log = logging.getLogger("convert") def convert_swf(swf_path: Path, cfg: dict, output_dir: Path, dry_run: bool = False): """Convert a single SWF file, including all interactive branches.""" swf_name = swf_path.stem log.info(f"=== Converting: {swf_path.name} ===") recorder = Recorder(swf_path, cfg) interactions = cfg.get("interactions", []) results = [] # --- Pass 1: Base recording (no clicks) --- base_output = output_dir / f"{swf_name}_base.mp4" log.info(f" Pass 1/{ 1 + len(interactions) }: Base recording → {base_output.name}") if not dry_run: success = recorder.record( output_path=base_output, clicks=[], ) if success: results.append({"label": "base", "file": str(base_output)}) log.info(f" ✓ Base recording saved: {base_output.name}") else: log.error(f" ✗ Base recording failed for {swf_path.name}") else: log.info(f" [DRY RUN] Would record base pass → {base_output.name}") results.append({"label": "base", "file": str(base_output), "dry_run": True}) # --- Pass N: One recording per interaction point --- for i, interaction in enumerate(interactions, 1): label = interaction.get("label", f"interaction_{i}") branch_output = output_dir / f"{swf_name}_{label}.mp4" log.info( f" Pass {i + 1}/{ 1 + len(interactions) }: Branch '{label}' " f"(click at t={interaction['t']}s, x={interaction['x']}, y={interaction['y']}) " f"→ {branch_output.name}" ) if not dry_run: success = recorder.record( output_path=branch_output, clicks=[interaction], ) if success: results.append({"label": label, "file": str(branch_output)}) log.info(f" ✓ Branch '{label}' saved: {branch_output.name}") else: log.error(f" ✗ Branch '{label}' failed for {swf_path.name}") else: log.info(f" [DRY RUN] Would record branch '{label}' → {branch_output.name}") results.append({"label": label, "file": str(branch_output), "dry_run": True}) return results def main(): parser = argparse.ArgumentParser( description="Convert SWF files to MP4 with loop detection and interaction capture." ) parser.add_argument( "input", nargs="+", help="SWF file(s) or directory containing SWF files", ) parser.add_argument( "-o", "--output", default="output", help="Output directory for MP4 files (default: output/)", ) parser.add_argument( "-c", "--config", default="configs/swf_config.json", help="Path to JSON config file (default: configs/swf_config.json)", ) parser.add_argument( "--inspect", action="store_true", help="Inspect SWF files and print metadata without converting", ) parser.add_argument( "--generate-config", action="store_true", help="Generate a starter config file from the given SWF files", ) parser.add_argument( "--dry-run", action="store_true", help="Show what would be done without actually recording", ) parser.add_argument( "--loop-grace", type=float, default=5.0, help="Seconds to continue recording after a loop is detected (default: 5.0)", ) parser.add_argument( "--max-duration", type=float, default=600.0, help="Hard cap on recording duration in seconds (default: 600)", ) args = parser.parse_args() # Collect SWF files swf_files = [] for inp in args.input: p = Path(inp) if p.is_dir(): swf_files.extend(sorted(p.glob("**/*.swf"))) elif p.suffix.lower() == ".swf": swf_files.append(p) else: log.warning(f"Skipping non-SWF file: {p}") if not swf_files: log.error("No SWF files found.") sys.exit(1) log.info(f"Found {len(swf_files)} SWF file(s).") # Inspect mode if args.inspect: inspector = SWFInspector() for swf in swf_files: info = inspector.inspect(swf) print(json.dumps({str(swf): info}, indent=2)) return # Generate config mode if args.generate_config: cfg_mgr = ConfigManager(args.config) inspector = SWFInspector() cfg_mgr.generate_starter(swf_files, inspector) print(f"Starter config written to: {args.config}") print("Edit it to add interaction points, then run without --generate-config.") return # Load config cfg_mgr = ConfigManager(args.config) global_defaults = { "loop_grace": args.loop_grace, "max_duration": args.max_duration, } output_dir = Path(args.output) output_dir.mkdir(parents=True, exist_ok=True) # Convert each file all_results = {} for swf_path in swf_files: cfg = cfg_mgr.get(swf_path.name, global_defaults) try: results = convert_swf(swf_path, cfg, output_dir, dry_run=args.dry_run) all_results[swf_path.name] = results except Exception as e: log.exception(f"Unexpected error converting {swf_path.name}: {e}") all_results[swf_path.name] = {"error": str(e)} # Summary print("\n" + "=" * 60) print("CONVERSION SUMMARY") print("=" * 60) for swf_name, results in all_results.items(): print(f"\n{swf_name}:") if isinstance(results, list): for r in results: status = "[DRY RUN]" if r.get("dry_run") else "✓" print(f" {status} [{r['label']}] → {r['file']}") else: print(f" ✗ ERROR: {results.get('error')}") # Write results JSON results_path = output_dir / "conversion_results.json" with open(results_path, "w") as f: json.dump(all_results, f, indent=2) log.info(f"Results written to {results_path}") if __name__ == "__main__": main()