initial commit
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user