Files
2026-03-24 18:11:39 -06:00

210 lines
6.6 KiB
Python

#!/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()