File size: 4,613 Bytes
bc50ea0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | """Chapter-local geometry; no media mutation or implicit latent resizing."""
def normalize_resolution(value, label="Chapter resolution"):
if value is None:
return None
if not isinstance(value, dict) or set(value) != {"width", "height"}:
raise ValueError(f"{label} must contain width and height, or be null to inherit.")
result = {}
for key in ("width", "height"):
number = value[key]
if (isinstance(number, bool) or not isinstance(number, int)
or number < 32 or number > 16384 or number % 32):
raise ValueError(f"{label} {key} must be a multiple of 32 between 32 and 16384.")
result[key] = number
return result
def scene_resolution(plan, index):
shot = plan["shots"][int(index) - 1]
return dict(shot.get("resolution") or {
key: int(plan["compatibility"][key]) for key in ("width", "height")
})
def saved_resolution(segment, metadata=None):
"""Prefer the immutable scene's recorded geometry, including legacy saves."""
metadata = metadata or {}
dependency = segment.get("scene_dependency") or metadata.get("scene_dependency") or {}
candidates = (
segment.get("resolution"),
dependency.get("scopes", {}).get("global_generation"),
metadata.get("compatibility"),
)
for candidate in candidates:
if isinstance(candidate, dict) and all(key in candidate for key in ("width", "height")):
return normalize_resolution(
{key: candidate[key] for key in ("width", "height")},
"Saved scene resolution")
return None
def apply_chapter_resolutions(plan, chapters, locked_resolutions):
"""Resolve one size per chapter, pinning inherited chapters to saved locks.
Only chapter-sized geometry is authored. Locking a saved scene prevents a
Plan-default edit from changing the interpretation of its whole chapter.
Explicit conflicting chapter edits fail; locks never waive integrity checks.
"""
starts = sorted(chapters, key=lambda row: int(row["start_scene"]))
if not starts or int(starts[0]["start_scene"]) > 1:
implicit = {"start_scene": 1, "title": "Unassigned",
"start_scene_id": plan["shots"][0]["id"], "text": ""}
identifier = "unassigned"
while any(row["id"] == identifier for row in chapters):
identifier += "_"
implicit["id"] = identifier
starts.insert(0, implicit)
for offset, chapter in enumerate(starts):
start = int(chapter["start_scene"])
end = int(starts[offset + 1]["start_scene"]) - 1 if offset + 1 < len(starts) else len(plan["shots"])
shots = plan["shots"][start - 1:end]
explicit = normalize_resolution(chapter.get("resolution"))
locked = [locked_resolutions[shot["id"]] for shot in shots
if shot["id"] in locked_resolutions]
sizes = {(row["width"], row["height"]) for row in locked}
if len(sizes) > 1:
raise ValueError(f"{chapter['title']} has locked saved scenes at different resolutions. "
"Place the resolution boundary between chapters before continuing.")
pinned = locked[0] if locked else None
if pinned and explicit and explicit != pinned:
raise ValueError(f"{chapter['title']} is pinned by locked saved scenes to "
f"{pinned['width']}x{pinned['height']}. Unlock those scenes "
"before changing this chapter's resolution; saved files are unchanged.")
resolution = pinned or explicit
if resolution:
if pinned:
chapter["resolution"] = dict(pinned)
if chapter not in chapters:
# Recovery must retain a pin before the first authored marker.
chapters.insert(0, chapter)
for shot in shots:
shot["resolution"] = dict(resolution)
def common_saved_resolution(segments, label="Video assembly"):
sizes = {tuple(value[key] for key in ("width", "height"))
for segment in segments
if (value := saved_resolution(segment)) is not None}
if len(sizes) > 1:
raise ValueError(f"{label} contains different chapter resolutions. Export each chapter "
"separately, then resize to a common output size before joining. "
"Saved scenes and exports are unchanged.")
if sizes:
width, height = next(iter(sizes))
return {"width": width, "height": height}
return None
|