File size: 14,614 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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | """Read-only catalogue of saved processing takes, separate from generation lineage.
Listing reads JSON and file sizes, never model tensors or multi-GB checksums.
Execution remains responsible for artifact integrity and latent validation.
"""
import json
from pathlib import Path
import re
STAGES = ("derope", "latent_upscale", "pixel_upscale", "other")
def processing_lineage(segments):
"""Small immutable addresses, not copies of tensors or source manifests."""
return [{"scene": int(item["index"]), "revision": item["revision"],
"metadata_path": item["revision_metadata"],
"checkpoint_sha256": item["checkpoint_sha256"]}
for item in segments]
def validate_processing_lineage(value):
if not isinstance(value, list) or not value:
raise ValueError("Saved processing branch has no lineage.")
for item in value:
if (not isinstance(item, dict) or type(item.get("scene")) is not int or
item["scene"] < 1 or
not re.fullmatch(r"[0-9a-f]{32}", str(item.get("revision") or "")) or
not re.fullmatch(r"[0-9a-f]{64}", str(item.get("checkpoint_sha256") or "")) or
not isinstance(item.get("metadata_path"), str) or not item["metadata_path"]):
raise ValueError("Saved processing branch has an invalid revision address.")
first = value[0]["scene"]
if [item["scene"] for item in value] != list(range(first, first + len(value))):
raise ValueError("Saved processing branch is not contiguous.")
return value
def processing_stage(config):
config = config if isinstance(config, dict) else {}
recipe = config.get("recipe") or {}
recipe = recipe if isinstance(recipe, dict) else {}
explicit = recipe.get("stage")
if explicit in STAGES:
return explicit
# The released combined LBH/DeRoPE example records this recipe field.
# Never infer a processing stage from a user-chosen folder/profile name.
derope = recipe.get("derope")
if derope is True or (isinstance(derope, str) and derope.strip().lower()
not in ("", "false", "off", "none", "disabled")):
return "derope"
return {"h3_latent": "latent_upscale", "pixel": "pixel_upscale"}.get(
config.get("backend"), "other")
def saved_checkpoint_variants(output_root, run_name, originals):
root = Path(output_root).resolve()
run = (root / "h3_chains" / run_name).resolve()
if run.parent != root / "h3_chains":
raise ValueError("Invalid checkpoint variant run directory.")
records, warnings, seen, branches = [], [], set(), []
legacy_profiles = set()
def inside(path, parent):
resolved = path.resolve()
if not resolved.is_relative_to(parent):
raise ValueError("Saved processing artifact escapes its profile directory.")
return resolved
def read(path):
with path.open(encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise ValueError("Saved processing metadata is not an object.")
return value
def media(path):
rel = path.relative_to(root)
return {"filename": rel.name, "subfolder": str(rel.parent), "type": "output"}
def legacy_branches(profile):
# Only legacy DeRoPE takes need these larger, embedded-source files.
# New saves use compact immutable lineage snapshots in their metadata.
if profile in legacy_profiles:
return
legacy_profiles.add(profile)
manifests = [profile / "upscale_manifest.json"]
partial = inside(profile / "partial", profile)
if partial.is_dir():
manifests.extend(partial.glob("through_clip_*.manifest.json"))
for path in manifests:
if not path.is_file():
continue
try:
path = inside(path, profile)
saved = read(path)
if (saved.get("format") not in (
"h3_chain_upscale_manifest_v1", "h3_chain_upscale_partial_manifest_v1") or
saved.get("run_name") != run_name or saved.get("profile") != profile.name):
raise ValueError("Invalid processing branch manifest.")
branches.append({"path": str(path.relative_to(root)), "kind": "manifest",
"lineage": validate_processing_lineage(processing_lineage(saved["segments"]))})
except (OSError, ValueError, TypeError, KeyError) as exc:
warnings.append("%s: %s" % (path.name, exc))
parents = [run / "upscaled"]
chapters = run / "chapters"
if chapters.is_dir():
parents.extend(path / "upscaled" for path in chapters.iterdir() if path.is_dir())
for parent in parents:
try:
inside(parent, run)
if not parent.is_dir():
continue
profiles = sorted(parent.iterdir())
except (OSError, ValueError) as exc:
warnings.append(str(exc))
continue
for profile in profiles:
try:
profile = inside(profile, run)
folder = inside(profile / "checkpoints", profile)
if not folder.is_dir():
continue
files = sorted(folder.iterdir())
except (OSError, ValueError) as exc:
warnings.append(str(exc))
continue
for path in files:
match = re.fullmatch(r"clip_(\d{4})(?:\.([0-9a-f]{32}))?\.json", path.name)
if not match:
continue
try:
path = inside(path, profile)
metadata = read(path)
if metadata.get("format") != "h3_chain_upscale_segment_v1":
continue
if metadata.get("run_name") != run_name or metadata.get("profile") != profile.name:
raise ValueError("Saved processing metadata belongs to another run/profile.")
segment = metadata["segment"]
scene = int(segment["index"])
revision = str(segment["revision"])
if (scene != int(match[1]) or not re.fullmatch(r"[0-9a-f]{32}", revision)
or (match[2] and match[2] != revision)):
raise ValueError("Saved processing scene/revision does not match its file.")
canonical = inside(root / segment["revision_metadata"], profile)
if canonical != folder / ("clip_%04d.%s.json" % (scene, revision)):
raise ValueError("Saved processing revision address is inconsistent.")
# Ignore mutable pointer copies; their immutable revision
# file is the authority and is scanned separately.
if path != canonical:
continue
identity = str(path.relative_to(root))
if identity in seen:
continue
seen.add(identity)
missing, size, outputs = [], 0, {}
for field in ("segment", "checkpoint", "generated_audio"):
address = segment.get(field)
if not address and field == "generated_audio":
continue
if not isinstance(address, str) or not address:
missing.append(field)
continue
artifact = inside(root / address, profile)
if not artifact.is_file():
missing.append(field)
continue
size += artifact.stat().st_size
if field != "checkpoint":
outputs["video" if field == "segment" else "audio"] = media(artifact)
config = metadata.get("profile_config") or {}
stage = metadata.get("processing_stage")
if stage not in STAGES:
stage = processing_stage(config)
record = {
"key": identity, "metadata_path": identity,
"scene": scene, "scene_id": str(segment.get("id") or ""),
"revision": revision, "stage": stage, "profile": profile.name,
"profile_path": str(profile.relative_to(root)),
"source_manifest_hash": str(metadata.get("source_manifest_hash") or ""),
"source_revision": str(segment.get("source_revision") or ""),
"source_checkpoint_sha256": str(segment.get("source_checkpoint_sha256") or ""),
"checkpoint_sha256": str(segment.get("checkpoint_sha256") or ""),
"created_at": str(segment.get("created_at") or ""),
"width": segment.get("width"), "height": segment.get("height"),
"raw_frames": segment.get("raw_frames"),
"delivered_frames": segment.get("delivered_frames"),
"latent_saved": segment.get("latent_saved") is True,
"latent_layout": str(segment.get("latent_layout") or "omitted"),
"context_steps": segment.get("context_steps", 0),
"audio_route": str(segment.get("audio_route") or "unknown"),
"prompt": str(segment.get("prompt") or ""),
"ready": not missing, "missing_files": missing, "size_bytes": size,
**outputs,
}
records.append(record)
if metadata.get("processing_lineage"):
branches.append({"path": identity, "kind": "metadata",
"lineage": validate_processing_lineage(metadata["processing_lineage"])})
elif record["stage"] == "derope":
legacy_branches(profile)
except (OSError, ValueError, TypeError, KeyError) as exc:
warnings.append("%s: %s" % (path.name, exc))
# Match the exact source revision AND saved checkpoint identity. A rerun
# with the same scene number is not the same take. Follow derived sources
# transitively so a future DeRoPE -> upscale take stays beside its original.
sources = {}
for original in originals:
if original.get("take_kind") == "editorial_alternate":
continue
key = (int(original["scene"]), str(original["revision"]),
str(original.get("checkpoint_sha256") or ""))
if key[2]:
sources.setdefault(key, []).append(original)
alias = str(original.get("adopted_from_revision") or "")
if alias and key[2]:
sources.setdefault((key[0], alias, key[2]), []).append(original)
# ALT is not a generation branch, but its processed pictures still belong
# beside the immutable base scene. Match the ALT's own checkpoint hash;
# never attach an arbitrary same-scene processed take to the active base.
bases = {(int(item["scene"]), str(item["revision"])): item
for item in originals if item.get("take_kind") != "editorial_alternate"}
for alternate in originals:
if alternate.get("take_kind") != "editorial_alternate":
continue
base = bases.get((int(alternate["scene"]), str(alternate.get("alternate_of_revision") or "")))
digest = str(alternate.get("checkpoint_sha256") or "")
if base is not None and digest:
sources.setdefault((int(alternate["scene"]), str(alternate["revision"]), digest), []).append(base)
derived = {}
for record in records:
key = (record["scene"], record["revision"], record["checkpoint_sha256"])
if key[2]:
derived.setdefault(key, []).append(record)
def origins(record, visited):
key = (record["scene"], record["source_revision"], record["source_checkpoint_sha256"])
if not key[1] or not key[2] or key in visited:
return []
if key in sources:
return sources[key]
return [origin for parent in derived.get(key, [])
for origin in origins(parent, visited | {key})]
for record in records:
matches = {(int(item["scene"]), str(item["revision"]))
for item in origins(record, set())}
record["originals"] = [{"scene": scene, "revision": revision}
for scene, revision in sorted(matches)]
record["source_status"] = "linked" if matches else "original unavailable or source mismatch"
records.sort(key=lambda item: (item["scene"], item["created_at"], item["key"]), reverse=True)
# Independent pixel cleanup can leave a hole in an immutable historical
# lineage. Keep every surviving take visible, but don't offer that history
# as an executable complete branch or silently substitute a newer take.
available = {record["key"]: (record["scene"], record["revision"], record["checkpoint_sha256"])
for record in records if record["ready"]}
branches = [branch for branch in branches if all(
available.get(item["metadata_path"]) == (item["scene"], item["revision"], item["checkpoint_sha256"])
for item in branch["lineage"])]
for record in records:
matches = [branch for branch in branches if any(
item.get("metadata_path") == record["key"] and
item.get("checkpoint_sha256") == record["checkpoint_sha256"]
for item in branch["lineage"] if isinstance(item, dict))]
# Remove strict prefixes and duplicate snapshots. Two distinct leaves
# are ambiguous: the user must click a take unique to the desired leaf.
leaves = {}
for branch in matches:
lineage = branch["lineage"]
if not any(len(other["lineage"]) > len(lineage) and
other["lineage"][:len(lineage)] == lineage for other in matches):
key = json.dumps(lineage, sort_keys=True)
if key not in leaves or branch["kind"] == "metadata":
leaves[key] = branch
record["processing_branch"] = next(iter(leaves.values())) if len(leaves) == 1 else None
return {"variants": records, "warnings": warnings}
|