Files
swf-convertion/config_manager.py
2026-03-24 18:11:39 -06:00

193 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Config Manager - Loads, validates, and merges per-SWF configuration.
"""
import json
import logging
from pathlib import Path
from typing import Optional
log = logging.getLogger("config_manager")
# Schema documentation for config files
CONFIG_SCHEMA = {
# Per-file keys (all optional — omit to use global defaults)
"emulator": "str: 'ruffle' or 'lightspark'",
"loop_detection": "str: 'hash' | 'freeze' | 'none'",
"loop_grace": "float: seconds to continue after loop detected (default 5.0)",
"max_duration": "float: hard cap in seconds (default 600)",
"end_seconds": "float: stop at this timestamp (overrides loop detection)",
"end_frame": "int: stop at this frame number (overrides loop detection)",
"fps": "int: capture FPS (default 30)",
"crf": "int: FFmpeg quality 051, lower=better (default 18)",
"window_size": "str: e.g. '800x600' fallback if geometry detection fails",
"startup_delay": "float: seconds to wait after launching emulator (default 3.0)",
"audio": "bool: capture audio (default true)",
"interactions": [
{
"t": "float: seconds from SWF start to fire click",
"x": "int: X coordinate in window",
"y": "int: Y coordinate in window",
"label": "str: output file suffix (e.g. 'button_yes')",
"button": "int: mouse button 1=left 2=mid 3=right (default 1)",
"double": "bool: double-click (default false)",
}
],
}
DEFAULT_CONFIG = {
"emulator": "ruffle",
"loop_detection": "hash",
"loop_grace": 5.0,
"max_duration": 600.0,
"fps": 30,
"crf": 18,
"window_size": "800x600",
"startup_delay": 3.0,
"audio": True,
"interactions": [],
}
class ConfigManager:
"""
Loads a JSON config file mapping SWF filenames to per-file settings.
Config file format:
{
"_defaults": { ... global defaults ... },
"my_animation.swf": {
"end_seconds": 42,
"interactions": [
{ "t": 12.5, "x": 320, "y": 240, "label": "button_yes" }
]
},
"menu.swf": {
"loop_detection": "none",
"end_seconds": 30
}
}
"""
def __init__(self, config_path: str = "configs/swf_config.json"):
self.config_path = Path(config_path)
self._data: dict = {}
self._load()
def _load(self):
if self.config_path.exists():
try:
with open(self.config_path) as f:
self._data = json.load(f)
log.info(f"Loaded config from {self.config_path}")
except json.JSONDecodeError as e:
log.error(f"Config JSON parse error: {e}. Using defaults.")
self._data = {}
else:
log.info(f"Config file not found ({self.config_path}). Using defaults for all files.")
self._data = {}
def get(self, swf_filename: str, global_overrides: Optional[dict] = None) -> dict:
"""
Return the merged config for a SWF file.
Priority (highest first):
1. Per-file config in JSON
2. _defaults section in JSON
3. global_overrides (from CLI args)
4. DEFAULT_CONFIG (hardcoded fallbacks)
"""
cfg = DEFAULT_CONFIG.copy()
if global_overrides:
cfg.update({k: v for k, v in global_overrides.items() if v is not None})
file_defaults = self._data.get("_defaults", {})
cfg.update(file_defaults)
file_cfg = self._data.get(swf_filename, {})
cfg.update(file_cfg)
self._validate(cfg, swf_filename)
return cfg
def _validate(self, cfg: dict, swf_filename: str):
"""Log warnings for obviously wrong config values."""
if cfg.get("crf") is not None and not (0 <= cfg["crf"] <= 51):
log.warning(f"{swf_filename}: crf={cfg['crf']} is out of range 051.")
if cfg.get("fps") is not None and not (1 <= cfg["fps"] <= 120):
log.warning(f"{swf_filename}: fps={cfg['fps']} is unusual.")
for i, interaction in enumerate(cfg.get("interactions", [])):
for required in ("t", "x", "y"):
if required not in interaction:
log.warning(
f"{swf_filename}: interaction[{i}] missing required key '{required}'."
)
def generate_starter(self, swf_files: list, inspector=None) -> dict:
"""
Generate a starter config file for the given SWF files.
Optionally uses SWFInspector to pre-populate FPS and estimated duration.
"""
config = {
"_defaults": {
"loop_detection": "hash",
"loop_grace": 5.0,
"max_duration": 600.0,
"fps": 30,
"audio": True,
},
"_schema": CONFIG_SCHEMA,
}
for swf in swf_files:
entry = {}
if inspector:
info = inspector.inspect(swf)
if info.get("fps"):
entry["fps"] = info["fps"]
if info.get("frame_count"):
estimated_seconds = round(info["frame_count"] / max(info.get("fps", 24), 1), 1)
entry["_estimated_duration_seconds"] = estimated_seconds
entry["_comment"] = (
f"SWF: {info.get('frame_count')} frames @ {info.get('fps')} fps "
f"{estimated_seconds}s. Set end_seconds to control stop time."
)
entry["interactions"] = [
{
"_comment": "Add click interactions here. Example:",
"t": 10.0,
"x": 400,
"y": 300,
"label": "example_click",
}
]
config[swf.name] = entry
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(self.config_path, "w") as f:
json.dump(config, f, indent=2)
log.info(f"Starter config written to {self.config_path}")
return config
def save(self):
with open(self.config_path, "w") as f:
json.dump(self._data, f, indent=2)
log.info(f"Config saved to {self.config_path}")
def set_file_config(self, swf_filename: str, key: str, value):
"""Programmatically update a single config key for a SWF file."""
if swf_filename not in self._data:
self._data[swf_filename] = {}
self._data[swf_filename][key] = value
def add_interaction(self, swf_filename: str, interaction: dict):
"""Append an interaction entry for a SWF file."""
if swf_filename not in self._data:
self._data[swf_filename] = {}
if "interactions" not in self._data[swf_filename]:
self._data[swf_filename]["interactions"] = []
self._data[swf_filename]["interactions"].append(interaction)