"""Persistence for sprite frame label assignments. Assignments are stored in a JSON sidecar keyed by filename (not frame index), so resuming a session survives frames being added, removed, or reordered. """ import json from pathlib import Path SIDECAR_NAME = '.sprite_tags.json' SCHEMA_VERSION = 1 def sidecar_path(frames_dir: Path) -> Path: return frames_dir / SIDECAR_NAME def save_labels(path: Path, assignments_by_name: dict, labels: list) -> None: data = { 'version': SCHEMA_VERSION, 'labels': list(labels), 'assignments': dict(assignments_by_name), } Path(path).write_text(json.dumps(data, indent=2)) def load_labels(path: Path): """Return (assignments_by_name, labels). Missing or invalid file → ({}, []).""" path = Path(path) if not path.exists(): return {}, [] try: data = json.loads(path.read_text()) except (json.JSONDecodeError, OSError, ValueError): return {}, [] assignments = data.get('assignments', {}) labels = data.get('labels', []) if not isinstance(assignments, dict) or not isinstance(labels, list): return {}, [] return assignments, labels def to_name_keyed(assignments_by_index: dict, frames: list) -> dict: """{index: label} → {filename: label}.""" return {frames[i].name: label for i, label in assignments_by_index.items()} def to_index_keyed(assignments_by_name: dict, frames: list) -> dict: """{filename: label} → {index: label}, dropping files not in `frames`.""" name_to_index = {p.name: i for i, p in enumerate(frames)} return { name_to_index[name]: label for name, label in assignments_by_name.items() if name in name_to_index }