210 lines
6.6 KiB
Python
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()
|