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