897 lines
44 KiB
Python
897 lines
44 KiB
Python
# SPDX-FileCopyrightText: 2026 Carmine Cristallo Scalzi (IAMCCS)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
"""Standalone, disk-bound H3 upscale stages. No Shotboard/backend routing here.
|
|
|
|
Stage 1 and Stage 2 must be queued as separate prompts to release the first
|
|
generation's model, conditioning and decoded IMAGE batch before tiled refine.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gc
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import folder_paths
|
|
import torch
|
|
from safetensors.torch import load_file, save_file
|
|
|
|
|
|
CATEGORY = "IAMCCS/MiniMax H3/Disk Upscale (Standalone)"
|
|
SCHEMA = "iamccs.h3.disk_upscale.v1"
|
|
ROOT_NAME = "IAMCCS/H3_DISK_UPSCALE"
|
|
|
|
|
|
def _safe_name(value: str) -> str:
|
|
result = re.sub(r"[^A-Za-z0-9_-]+", "_", str(value or "").strip()).strip("_-")[:80]
|
|
if not result:
|
|
raise ValueError("Disk Upscale requires a non-empty render_id")
|
|
return result
|
|
|
|
|
|
def _root() -> Path:
|
|
return (Path(folder_paths.get_output_directory()) / ROOT_NAME).resolve()
|
|
|
|
|
|
def _run_dir(render_id: str) -> Path:
|
|
return _root() / _safe_name(render_id)
|
|
|
|
|
|
def _inside_root(path: str | Path) -> Path:
|
|
resolved = Path(path).resolve()
|
|
if not resolved.is_relative_to(_root()):
|
|
raise ValueError("Checkpoint must be inside ComfyUI/output/IAMCCS/H3_DISK_UPSCALE")
|
|
return resolved
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(4 * 1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _atomic_new_bytes(path: Path, content: bytes) -> None:
|
|
"""Create a new manifest without replacing an existing user artifact."""
|
|
if path.exists():
|
|
raise FileExistsError(f"Disk Upscale refuses to overwrite {path}")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
temporary = path.with_name(path.name + "." + uuid.uuid4().hex + ".tmp")
|
|
try:
|
|
with temporary.open("xb") as stream:
|
|
stream.write(content)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
os.rename(temporary, path)
|
|
finally:
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
|
|
|
|
def _atomic_new_tensors(path: Path, tensors: dict[str, torch.Tensor]) -> None:
|
|
if path.exists():
|
|
raise FileExistsError(f"Disk Upscale refuses to overwrite {path}")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
temporary = path.with_name(path.name + "." + uuid.uuid4().hex + ".tmp")
|
|
try:
|
|
save_file(tensors, str(temporary))
|
|
os.rename(temporary, path)
|
|
finally:
|
|
if temporary.exists():
|
|
temporary.unlink()
|
|
|
|
|
|
def _av_streams(latent) -> tuple[torch.Tensor, torch.Tensor]:
|
|
if not isinstance(latent, dict) or "samples" not in latent:
|
|
raise ValueError("Expected a MiniMax H3 AV LATENT")
|
|
samples = latent["samples"]
|
|
if hasattr(samples, "unbind"):
|
|
streams = list(samples.unbind())
|
|
elif isinstance(samples, (tuple, list)):
|
|
streams = list(samples)
|
|
else:
|
|
raise ValueError("Expected nested video+audio H3 latent, not a flat image latent")
|
|
if len(streams) != 2 or not all(torch.is_tensor(item) for item in streams):
|
|
raise ValueError("Expected exactly two H3 AV latent tensors")
|
|
video, audio = streams
|
|
if video.ndim != 5 or audio.ndim != 4 or video.shape[0] != 1 or audio.shape[0] != 1:
|
|
raise ValueError(f"Invalid H3 AV dimensions: video={tuple(video.shape)}, audio={tuple(audio.shape)}")
|
|
if video.shape[1] != 24 or audio.shape[1] != 32:
|
|
raise ValueError("H3 AV latent must have 24 video and 32 audio channels")
|
|
return video, audio
|
|
|
|
|
|
def _waveform(audio) -> tuple[torch.Tensor, int]:
|
|
if not isinstance(audio, dict) or not torch.is_tensor(audio.get("waveform")):
|
|
raise ValueError("Connect the native AUDIO output; it is the soundtrack authority")
|
|
wave = audio["waveform"]
|
|
sample_rate = int(audio.get("sample_rate", 0) or 0)
|
|
if wave.ndim != 3 or wave.shape[0] != 1 or wave.shape[1] not in (1, 2) or sample_rate < 8000:
|
|
raise ValueError("AUDIO waveform must be [1,1|2,samples] with a valid sample_rate")
|
|
return wave, sample_rate
|
|
|
|
|
|
def _read_checkpoint(checkpoint_path: str):
|
|
path = _inside_root(checkpoint_path)
|
|
if path.suffix.lower() != ".safetensors" or not path.is_file():
|
|
raise FileNotFoundError(f"H3 Disk Upscale checkpoint missing: {path}")
|
|
manifest_path = path.with_suffix(".json")
|
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
if manifest.get("schema") != SCHEMA or manifest.get("checkpoint_name") != path.name:
|
|
raise ValueError("Checkpoint manifest has an invalid schema or file identity")
|
|
if _sha256(path) != manifest.get("checkpoint_sha256"):
|
|
raise ValueError("Checkpoint SHA-256 mismatch; refusing a partial or changed latent")
|
|
tensors = load_file(str(path), device="cpu")
|
|
required = {"video", "audio_latent", "waveform"}
|
|
if set(tensors) != required:
|
|
raise ValueError(f"Checkpoint tensor keys must be {sorted(required)}")
|
|
video, audio_latent = tensors["video"], tensors["audio_latent"]
|
|
_av_streams({"samples": (video, audio_latent)})
|
|
wave, rate = _waveform({"waveform": tensors["waveform"], "sample_rate": manifest["audio_sample_rate"]})
|
|
if int(video.shape[2]) != int(manifest["video_tokens"]):
|
|
raise ValueError("Checkpoint temporal shape does not match its manifest")
|
|
if int(video.shape[2]) < 7 or 17 * ((int(video.shape[2]) - 2) // 5) + 5 < int(manifest["source_frames"]):
|
|
raise ValueError("Checkpoint latent cannot decode the declared source frame count")
|
|
if float(wave.shape[-1]) / rate + 1 / float(manifest["fps"]) < float(manifest["source_frames"]) / float(manifest["fps"]):
|
|
raise ValueError("Checkpoint audio is shorter than its declared video span")
|
|
return path, manifest, video, audio_latent, wave, rate
|
|
|
|
|
|
def _nested_latent(video: torch.Tensor, audio: torch.Tensor):
|
|
import comfy.nested_tensor
|
|
|
|
return {"samples": comfy.nested_tensor.NestedTensor((video, audio))}
|
|
|
|
|
|
def _out0(result):
|
|
try:
|
|
return result[0]
|
|
except (TypeError, KeyError, IndexError):
|
|
return result.result[0]
|
|
|
|
|
|
def _video_frames(path: Path) -> tuple[int, int, int, bool]:
|
|
"""Inspect the encoded stream without materializing an IMAGE batch."""
|
|
import av
|
|
|
|
with av.open(str(path)) as container:
|
|
if not container.streams.video:
|
|
raise ValueError(f"No video stream in {path}")
|
|
stream = container.streams.video[0]
|
|
width, height = int(stream.width), int(stream.height)
|
|
has_audio = bool(container.streams.audio)
|
|
count = sum(1 for _ in container.decode(video=0))
|
|
if count < 1:
|
|
raise ValueError(f"No decoded frames in {path}")
|
|
return count, width, height, has_audio
|
|
|
|
|
|
class IAMCCS_H3DiskUpscaleCheckpoint:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"latent": ("LATENT",), "native_audio": ("AUDIO",),
|
|
"render_id": ("STRING", {"default": "h3_upscale_test"}),
|
|
"segment_index": ("INT", {"default": 0, "min": 0, "max": 99999}),
|
|
"source_frames": ("INT", {"default": 0, "min": 0, "max": 100000,
|
|
"tooltip": "0 = derive the exact decodable H3 frame count from the AV latent"}),
|
|
"fps": ("INT", {"default": 24, "min": 1, "max": 120}),
|
|
"technical_prefix_frames": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
|
"join_overlap_frames": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
|
"join_mode": (["cut", "crossfade"], {"default": "cut"}),
|
|
}}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING")
|
|
RETURN_NAMES = ("checkpoint_path", "report")
|
|
FUNCTION = "save"
|
|
CATEGORY = CATEGORY
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return float("nan")
|
|
|
|
def save(self, latent, native_audio, render_id, segment_index, source_frames, fps,
|
|
technical_prefix_frames, join_overlap_frames, join_mode):
|
|
video, audio_latent = _av_streams(latent)
|
|
wave, rate = _waveform(native_audio)
|
|
run = _safe_name(render_id)
|
|
index = int(segment_index)
|
|
decodable_frames = 17 * ((int(video.shape[2]) - 2) // 5) + 5 if int(video.shape[2]) >= 7 else 0
|
|
frames = int(source_frames) or decodable_frames
|
|
fps = int(fps)
|
|
technical = int(technical_prefix_frames)
|
|
overlap = int(join_overlap_frames)
|
|
if fps != 24:
|
|
raise ValueError("MiniMax H3 disk upscale currently requires 24 fps for frame-accurate joins")
|
|
if decodable_frames < frames:
|
|
raise ValueError("H3 latent cannot decode the declared source frame count")
|
|
if technical + overlap >= frames or (index == 0 and overlap):
|
|
raise ValueError("Invalid technical/join prefix for the segment frame count")
|
|
if join_mode == "crossfade" and index > 0 and overlap < 2:
|
|
raise ValueError("Crossfade requires at least two decoded overlap frames")
|
|
if float(wave.shape[-1]) / rate + 1 / fps < frames / fps:
|
|
raise ValueError("Native audio does not cover source_frames")
|
|
folder = _run_dir(run) / "checkpoints"
|
|
path = folder / f"segment_{index:05d}.safetensors"
|
|
if path.exists() or path.with_suffix(".json").exists():
|
|
raise FileExistsError(f"Checkpoint already exists: {path}")
|
|
tensors = {
|
|
"video": video.detach().to(device="cpu").contiguous(),
|
|
"audio_latent": audio_latent.detach().to(device="cpu").contiguous(),
|
|
"waveform": wave.detach().to(device="cpu").contiguous(),
|
|
}
|
|
_atomic_new_tensors(path, tensors)
|
|
manifest = {
|
|
"schema": SCHEMA, "render_id": run, "segment_index": index,
|
|
"checkpoint_name": path.name, "checkpoint_sha256": _sha256(path),
|
|
"video_tokens": int(video.shape[2]), "native_width": int(video.shape[-1]) * 16,
|
|
"native_height": int(video.shape[-2]) * 16, "source_frames": frames,
|
|
"fps": fps, "audio_sample_rate": rate, "technical_prefix_frames": technical,
|
|
"join_overlap_frames": overlap, "join_mode": join_mode,
|
|
}
|
|
manifest_path = path.with_suffix(".json")
|
|
try:
|
|
_atomic_new_bytes(manifest_path, json.dumps(manifest, indent=2).encode("utf-8"))
|
|
except Exception:
|
|
# The checkpoint is usable only as an authenticated pair. Roll back
|
|
# the exact file created by this call so a retry can resume cleanly.
|
|
if path.exists() and not manifest_path.exists():
|
|
path.unlink()
|
|
raise
|
|
return str(path), f"H3 AV + native audio checkpointed on disk: {path} ({frames} source frames)"
|
|
|
|
|
|
class IAMCCS_H3DiskUpscaleLoad:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {"checkpoint_path": ("STRING", {"default": ""})}}
|
|
|
|
RETURN_TYPES = ("LATENT", "AUDIO", "STRING", "INT", "INT", "STRING")
|
|
RETURN_NAMES = ("av_latent", "native_audio", "manifest_json", "segment_index", "source_frames", "report")
|
|
FUNCTION = "load"
|
|
CATEGORY = CATEGORY
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return float("nan")
|
|
|
|
def load(self, checkpoint_path):
|
|
path, manifest, video, audio, wave, rate = _read_checkpoint(checkpoint_path)
|
|
return (_nested_latent(video, audio), {"waveform": wave, "sample_rate": rate},
|
|
json.dumps(manifest), int(manifest["segment_index"]),
|
|
int(manifest["source_frames"]), f"Verified H3 AV checkpoint {path}")
|
|
|
|
|
|
def _tiled_params(model_name, width, height, tile_width, tile_height, spatial_overlap,
|
|
temporal_chunk, temporal_overlap, upscaler_device, precision):
|
|
if width < 64 or height < 64 or width % 32 or height % 32:
|
|
raise ValueError("Internal H3 target dimensions must be 32-aligned")
|
|
if tile_width % 32 or tile_height % 32 or min(tile_width, tile_height) < 128:
|
|
raise ValueError("H3 tiles must be 32-aligned and at least 128 pixels")
|
|
if tile_width > width or tile_height > height:
|
|
raise ValueError("H3 tile cannot exceed the internal target canvas")
|
|
if spatial_overlap % 32 or spatial_overlap >= min(tile_width, tile_height):
|
|
raise ValueError("Spatial overlap must be 32-aligned and smaller than each tile")
|
|
if temporal_chunk % 17 or temporal_overlap % 17 or temporal_chunk <= temporal_overlap:
|
|
raise ValueError("Temporal chunk and overlap must be 17-frame multiples, with chunk > overlap")
|
|
if upscaler_device == "cpu" and precision != "fp32":
|
|
raise ValueError("CPU learned lift requires fp32 precision")
|
|
if not model_name or not folder_paths.get_full_path("latent_upscale_models", model_name):
|
|
raise ValueError("Select an installed H3 3D .safetensors latent-upscaler model")
|
|
latent = {"model_name": model_name, "width": width, "height": height,
|
|
"device": upscaler_device, "precision": precision}
|
|
temporal = {"chunk_length": temporal_chunk, "temporal_overlap": temporal_overlap,
|
|
"anchor_strength": 0.999}
|
|
spatial = {
|
|
"tile_width": tile_width, "tile_height": tile_height,
|
|
"spatial_w_overlap": spatial_overlap, "spatial_h_overlap": spatial_overlap,
|
|
"fade_width": min(32, spatial_overlap), "fade_height": min(32, spatial_overlap),
|
|
"min_tile_size": min(256, tile_width, tile_height),
|
|
"overlap_mode": "earlier", "overlap_blend": "smoothstep",
|
|
"tile_size_mode": "specific_size", "masked_area_noise": 0.0,
|
|
"brightness_match": False, "dynamic_fade": "off", "dynamic_fade_min": 32,
|
|
}
|
|
return latent, temporal, spatial
|
|
|
|
|
|
def _delivery_cover_size(native_width: int, native_height: int,
|
|
delivery_width: int, delivery_height: int,
|
|
align: int = 32) -> tuple[int, int]:
|
|
"""Return an aligned, aspect-preserving canvas that covers delivery.
|
|
|
|
The learned H3 model uses one effective scale for both spatial axes. Asking
|
|
it to map a 5:3 latent directly to a 16:9 canvas introduces anisotropic
|
|
deformation. Upscale to a cover canvas instead and crop only after decode.
|
|
"""
|
|
values = native_width, native_height, delivery_width, delivery_height, align
|
|
if any(int(value) < 1 for value in values):
|
|
raise ValueError("Native, delivery and alignment dimensions must be positive")
|
|
scale = max(delivery_width / native_width, delivery_height / native_height)
|
|
width = math.ceil((native_width * scale) / align) * align
|
|
height = math.ceil((native_height * scale) / align) * align
|
|
return int(width), int(height)
|
|
|
|
|
|
TARGET_PRESETS = {
|
|
"hd_1280x720": (1280, 720),
|
|
"full_hd_1920x1080": (1920, 1080),
|
|
"qhd_2560x1440": (2560, 1440),
|
|
"uhd_3840x2160": (3840, 2160),
|
|
}
|
|
|
|
|
|
def _delivery_dimensions(native_width: int, native_height: int, target_preset: str,
|
|
custom_width: int, custom_height: int) -> tuple[int, int]:
|
|
"""Resolve delivery dimensions from the actual low-resolution source."""
|
|
if target_preset in TARGET_PRESETS:
|
|
return TARGET_PRESETS[target_preset]
|
|
source_scales = {"source_1_5x": 1.5, "source_2x": 2.0, "source_3x": 3.0}
|
|
if target_preset in source_scales:
|
|
scale = source_scales[target_preset]
|
|
return (
|
|
max(8, round(native_width * scale / 8) * 8),
|
|
max(8, round(native_height * scale / 8) * 8),
|
|
)
|
|
if target_preset == "custom":
|
|
return int(custom_width), int(custom_height)
|
|
raise ValueError(f"Unknown H3 upscale target preset: {target_preset}")
|
|
|
|
|
|
def _release_external_upscaler(klass) -> None:
|
|
"""Release models cached by the third-party upscaler, including OOM paths."""
|
|
module = sys.modules.get(getattr(klass, "__module__", ""))
|
|
cache = getattr(module, "MODEL_CACHE", None)
|
|
if isinstance(cache, dict):
|
|
for model in tuple(cache.values()):
|
|
try:
|
|
model.to("cpu")
|
|
except Exception:
|
|
pass
|
|
cache.clear()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
|
|
def _learned_3d_temporal_lift(klass, video: torch.Tensor, model_name: str,
|
|
width: int, height: int, device: str, precision: str,
|
|
core_tokens: int, halo_tokens: int) -> torch.Tensor:
|
|
"""Upscale full spatial frames in small temporal windows and stitch on CPU.
|
|
|
|
The provider's built-in temporal chunk is hardcoded to 32 latent tokens and
|
|
expands each segment with halos; at Full HD that still OOMs on 12 GB. This
|
|
wrapper keeps every spatial operation full-frame (so no texture grid), but
|
|
limits the Conv3D activation peak to ``core + 2*halo`` latent tokens.
|
|
"""
|
|
if video.ndim != 5:
|
|
raise ValueError("H3 learned temporal lift expects a 5D B,C,T,H,W latent")
|
|
core_tokens, halo_tokens = int(core_tokens), int(halo_tokens)
|
|
if core_tokens < 1 or halo_tokens < 0:
|
|
raise ValueError("Temporal core must be positive and halo cannot be negative")
|
|
total = int(video.shape[2])
|
|
output = torch.empty(
|
|
int(video.shape[0]), int(video.shape[1]), total,
|
|
int(height) // 16, int(width) // 16,
|
|
dtype=video.dtype, device="cpu",
|
|
)
|
|
try:
|
|
for core_start in range(0, total, core_tokens):
|
|
core_end = min(total, core_start + core_tokens)
|
|
window_start = max(0, core_start - halo_tokens)
|
|
window_end = min(total, core_end + halo_tokens)
|
|
window = video[:, :, window_start:window_end].contiguous()
|
|
payload = _out0(klass.execute(
|
|
latent={"samples": window},
|
|
model_name=model_name,
|
|
mode={"mode": "target dimensions", "width": width, "height": height},
|
|
align=32,
|
|
enable_temporal_chunking=False,
|
|
force_unload=False,
|
|
device=device,
|
|
precision=precision,
|
|
))
|
|
if not isinstance(payload, dict) or not torch.is_tensor(payload.get("samples")):
|
|
raise TypeError("Minimax H3 3D upscaler returned an invalid temporal window")
|
|
window_out = payload["samples"]
|
|
local_start = core_start - window_start
|
|
local_end = local_start + (core_end - core_start)
|
|
output[:, :, core_start:core_end].copy_(
|
|
window_out[:, :, local_start:local_end].detach().to("cpu")
|
|
)
|
|
del payload, window_out, window
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
except Exception:
|
|
_release_external_upscaler(klass)
|
|
raise
|
|
_release_external_upscaler(klass)
|
|
return output
|
|
|
|
|
|
def _trim_encoded(raw: Path, final: Path, start: int, end: int, width: int, height: int, fps: int):
|
|
from .iamccs_minimax_h3_shotboard import _find_ffmpeg
|
|
|
|
ffmpeg = _find_ffmpeg()
|
|
if not ffmpeg:
|
|
raise RuntimeError("FFmpeg is required for exact H3 segment trim/crop")
|
|
if final.exists():
|
|
raise FileExistsError(f"Disk Upscale refuses to overwrite {final}")
|
|
crop = f"crop={width}:{height}:(iw-{width})/2:(ih-{height})/2"
|
|
video_filter = f"trim=start_frame={start}:end_frame={end},setpts=PTS-STARTPTS,{crop}"
|
|
audio_filter = f"atrim=start={start / fps:.9f}:end={end / fps:.9f},asetpts=PTS-STARTPTS"
|
|
final.parent.mkdir(parents=True, exist_ok=True)
|
|
command = [ffmpeg, "-hide_banner", "-loglevel", "error", "-nostdin", "-n", "-i", str(raw),
|
|
"-map", "0:v:0", "-map", "0:a:0", "-vf", video_filter, "-af", audio_filter,
|
|
"-frames:v", str(end - start), "-r", str(fps),
|
|
"-c:v", "libx264", "-preset", "medium", "-crf", "16", "-pix_fmt", "yuv420p",
|
|
"-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", str(final)]
|
|
completed = subprocess.run(command, capture_output=True, text=True)
|
|
if completed.returncode:
|
|
raise RuntimeError("H3 exact segment trim failed: " + completed.stderr[-2000:])
|
|
|
|
|
|
def _publish_validated_segment(temporary_final: Path, final: Path, result: dict[str, Any]):
|
|
"""Publish one validated MP4 and its authenticated metadata without overwrite.
|
|
|
|
The encoded bytes exist at ``temporary_final`` until the last atomic rename,
|
|
so their checksum must be computed there. Hashing ``final`` before the
|
|
rename caused successful GPU renders to fail after streaming decode.
|
|
"""
|
|
final_manifest = final.with_suffix(".json")
|
|
sidecar = final.with_suffix(final.suffix + ".iamccs.json")
|
|
if final.exists() or final_manifest.exists() or sidecar.exists():
|
|
raise FileExistsError(f"Upscaled segment already exists: {final}")
|
|
if not temporary_final.is_file():
|
|
raise FileNotFoundError(f"Validated temporary segment is missing: {temporary_final}")
|
|
|
|
published = dict(result)
|
|
published["segment_path"] = str(final)
|
|
published["segment_sha256"] = _sha256(temporary_final)
|
|
promoted = False
|
|
try:
|
|
# Prepare both manifests first; the validated MP4 is promoted last.
|
|
# Therefore a visible final segment always has all of its metadata.
|
|
_atomic_new_bytes(sidecar, json.dumps({
|
|
"schema": "iamccs.minimax_h3.segment",
|
|
"frame_count": int(published["frame_count"]),
|
|
"fps": float(published["fps"]),
|
|
"audio_join_policy": "native_audio_locked",
|
|
}, indent=2).encode("utf-8"))
|
|
_atomic_new_bytes(final_manifest, json.dumps(published, indent=2).encode("utf-8"))
|
|
if final.exists():
|
|
raise FileExistsError(f"Upscaled segment already exists: {final}")
|
|
os.rename(temporary_final, final)
|
|
promoted = True
|
|
except Exception:
|
|
if not promoted:
|
|
if final_manifest.exists():
|
|
final_manifest.unlink()
|
|
if sidecar.exists():
|
|
sidecar.unlink()
|
|
raise
|
|
return final_manifest, published
|
|
|
|
|
|
class IAMCCS_H3DiskUpscaleTiledRefine:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
try:
|
|
installed = folder_paths.get_filename_list("latent_upscale_models")
|
|
except KeyError:
|
|
installed = []
|
|
models = sorted(
|
|
name for name in installed
|
|
if all(key in name.lower() for key in ("minimax", "h3", "3d"))
|
|
and name.lower().endswith(".safetensors")
|
|
)
|
|
return {"required": {
|
|
"checkpoint_path": ("STRING", {"default": ""}),
|
|
"model": ("MODEL",), "conditioning": ("CONDITIONING",),
|
|
"noise": ("NOISE",), "sampler": ("SAMPLER",), "sigmas": ("SIGMAS",),
|
|
"video_vae": ("VAE",),
|
|
"upscaler_model": ([""] + models, {"default": models[0] if models else ""}),
|
|
"target_width": ("INT", {"default": 1920, "min": 256, "max": 3840, "step": 8}),
|
|
"target_height": ("INT", {"default": 1080, "min": 256, "max": 2160, "step": 8}),
|
|
"tile_width": ("INT", {"default": 512, "min": 128, "max": 2048, "step": 32}),
|
|
"tile_height": ("INT", {"default": 384, "min": 128, "max": 2048, "step": 32}),
|
|
"spatial_overlap": ("INT", {"default": 96, "min": 0, "max": 512, "step": 32}),
|
|
"temporal_chunk_frames": ("INT", {"default": 68, "min": 17, "max": 340, "step": 17}),
|
|
"temporal_overlap_frames": ("INT", {"default": 17, "min": 0, "max": 170, "step": 17}),
|
|
"upscaler_device": (["cuda", "cpu"], {"default": "cuda"}),
|
|
"upscaler_precision": (["fp16", "bf16", "fp32"], {"default": "fp16"}),
|
|
"decode_groups_per_chunk": ("INT", {"default": 1, "min": 1, "max": 8}),
|
|
}, "optional": {
|
|
"fun_control_param": ("H3_FUN_CONTROL_PARAM",),
|
|
"inpaint_param": ("H3_INPAINT_PARAM",),
|
|
}}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("segment_path", "segment_manifest_path", "report")
|
|
FUNCTION = "refine"
|
|
CATEGORY = CATEGORY
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return float("nan")
|
|
|
|
def refine(self, checkpoint_path, model, conditioning, noise, sampler, sigmas, video_vae,
|
|
upscaler_model, target_width, target_height, tile_width, tile_height,
|
|
spatial_overlap, temporal_chunk_frames, temporal_overlap_frames,
|
|
upscaler_device, upscaler_precision, decode_groups_per_chunk,
|
|
fun_control_param=None, inpaint_param=None):
|
|
import comfy.model_management as mm
|
|
import nodes
|
|
|
|
path, manifest, video, audio_latent, wave, rate = _read_checkpoint(checkpoint_path)
|
|
run = str(manifest["render_id"])
|
|
index = int(manifest["segment_index"])
|
|
fps = int(manifest["fps"])
|
|
width, height = int(target_width), int(target_height)
|
|
internal_width = math.ceil(width / 32) * 32
|
|
internal_height = math.ceil(height / 32) * 32
|
|
latent_param, temporal_param, spatial_param = _tiled_params(
|
|
upscaler_model, internal_width, internal_height, int(tile_width), int(tile_height),
|
|
int(spatial_overlap), int(temporal_chunk_frames), int(temporal_overlap_frames),
|
|
upscaler_device, upscaler_precision,
|
|
)
|
|
if fun_control_param is not None:
|
|
if str(fun_control_param.get("control_upscale_mode")) != "per_tile":
|
|
raise ValueError("Disk-safe H3 Fun ControlNet requires per_tile control upscale mode")
|
|
if (int(fun_control_param.get("upscale_width", -1)),
|
|
int(fun_control_param.get("upscale_height", -1))) != (internal_width, internal_height):
|
|
raise ValueError("Fun ControlNet canvas must match the 32-aligned H3 upscale canvas")
|
|
required_guide_frames = 17 * ((int(video.shape[2]) - 2) // 5) + 5
|
|
if len(fun_control_param.get("control_video", ())) < required_guide_frames:
|
|
raise ValueError("Fun ControlNet guide must cover the full decoded source segment")
|
|
if width < int(manifest["native_width"]) or height < int(manifest["native_height"]):
|
|
raise ValueError("Tiled refine must not downscale the native H3 canvas")
|
|
klass = nodes.NODE_CLASS_MAPPINGS.get("MMH3UltimateUpscale")
|
|
if klass is None:
|
|
raise RuntimeError("Install/enable Comfyui-MMH3-UltimateUpscale and restart ComfyUI")
|
|
segment_dir = _run_dir(run) / "segments"
|
|
final = segment_dir / f"segment_{index:05d}.mp4"
|
|
final_manifest = final.with_suffix(".json")
|
|
sidecar = final.with_suffix(final.suffix + ".iamccs.json")
|
|
if final.exists() or final_manifest.exists() or sidecar.exists():
|
|
raise FileExistsError(f"Upscaled segment already exists: {final}")
|
|
temporary_final = final.with_name(f"{final.stem}.{uuid.uuid4().hex}.tmp.mp4")
|
|
|
|
try:
|
|
refined = _out0(klass.execute(
|
|
latent=_nested_latent(video, audio_latent), conditioning=conditioning,
|
|
model=model, noise=noise, sampler=sampler, sigmas=sigmas,
|
|
negative=None, cfg=1.0, latent_upscale_param=latent_param,
|
|
temporal_split_param=temporal_param, spatial_split_param=spatial_param,
|
|
fun_control_param=fun_control_param, inpaint_param=inpaint_param,
|
|
))
|
|
refined_video, refined_audio = _av_streams(refined)
|
|
# Only the compressed AV latent remains in CPU RAM during VAE decode.
|
|
refined_cpu = _nested_latent(refined_video.detach().to("cpu"), refined_audio.detach().to("cpu"))
|
|
del refined, refined_video, refined_audio, video, audio_latent
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
|
|
from .iamccs_minimax_h3_pixel_refine_variant import _provider
|
|
|
|
raw = Path(_out0(_provider("nodes_save").MMH3StreamingSave.execute(
|
|
latent=refined_cpu, vae=video_vae, groups_per_chunk=int(decode_groups_per_chunk),
|
|
fps=float(fps), filename_prefix=f"{ROOT_NAME}/{run}/raw/segment_{index:05d}",
|
|
crf=16, audio={"waveform": wave, "sample_rate": rate}, save_metadata=False,
|
|
)))
|
|
del refined_cpu
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
raw_count, raw_width, raw_height, raw_audio = _video_frames(raw)
|
|
if raw_count < int(manifest["source_frames"]) or not raw_audio:
|
|
raise RuntimeError("Streaming decode has missing frames or native audio; segment not accepted")
|
|
if raw_width < width or raw_height < height:
|
|
raise RuntimeError("Streaming decode is smaller than the requested delivery canvas")
|
|
technical = int(manifest["technical_prefix_frames"])
|
|
overlap = int(manifest["join_overlap_frames"])
|
|
join_mode = str(manifest["join_mode"])
|
|
head = technical + (overlap if join_mode == "cut" else 0)
|
|
end = int(manifest["source_frames"])
|
|
_trim_encoded(raw, temporary_final, head, end, width, height, fps)
|
|
frame_count, out_width, out_height, out_audio = _video_frames(temporary_final)
|
|
if (frame_count, out_width, out_height, out_audio) != (end - head, width, height, True):
|
|
raise RuntimeError("Final segment failed exact frame/canvas/audio validation")
|
|
result = {
|
|
"schema": SCHEMA, "render_id": run, "segment_index": index,
|
|
"checkpoint_sha256": manifest["checkpoint_sha256"],
|
|
"frame_count": frame_count, "width": width, "height": height, "fps": fps,
|
|
"has_audio": True, "join_mode": join_mode,
|
|
"join_overlap_frames": overlap if join_mode == "crossfade" else 0,
|
|
"technical_prefix_trimmed": technical,
|
|
}
|
|
final_manifest, result = _publish_validated_segment(temporary_final, final, result)
|
|
return str(final), str(final_manifest), (
|
|
f"H3 tiled upscale saved {frame_count} frames at {width}x{height}; "
|
|
f"tile={tile_width}x{tile_height}, temporal={temporal_chunk_frames}/{temporal_overlap_frames}; "
|
|
f"native audio preserved: {final}"
|
|
)
|
|
finally:
|
|
if temporary_final.exists():
|
|
temporary_final.unlink()
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
|
|
|
|
class IAMCCS_H3DiskUpscaleLearned3D:
|
|
"""Grid-free H3 learned latent lift followed by disk-bound streaming decode.
|
|
|
|
Unlike ``MMH3UltimateUpscale``, this path does not run a new diffusion
|
|
sample independently inside spatial tiles. Temporal chunking remains
|
|
enabled inside the learned 3D upscaler, while the original audio latent and
|
|
waveform are carried through unchanged.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
try:
|
|
installed = folder_paths.get_filename_list("latent_upscale_models")
|
|
except KeyError:
|
|
installed = []
|
|
models = sorted(
|
|
name for name in installed
|
|
if all(key in name.lower() for key in ("minimax", "h3", "3d"))
|
|
and name.lower().endswith(".safetensors")
|
|
)
|
|
return {"required": {
|
|
"checkpoint_path": ("STRING", {"default": ""}),
|
|
"output_render_id": ("STRING", {"default": ""}),
|
|
"video_vae": ("VAE",),
|
|
"upscaler_model": ([""] + models, {"default": models[0] if models else ""}),
|
|
"target_preset": ([
|
|
"full_hd_1920x1080", "hd_1280x720", "qhd_2560x1440",
|
|
"uhd_3840x2160", "source_1_5x", "source_2x", "source_3x", "custom",
|
|
], {"default": "full_hd_1920x1080"}),
|
|
"target_width": ("INT", {"default": 1920, "min": 256, "max": 3840, "step": 8}),
|
|
"target_height": ("INT", {"default": 1080, "min": 256, "max": 2160, "step": 8}),
|
|
"upscaler_device": (["cuda", "cpu"], {"default": "cuda"}),
|
|
"upscaler_precision": (["fp16", "bf16", "fp32"], {"default": "fp16"}),
|
|
"temporal_core_tokens": ("INT", {"default": 4, "min": 1, "max": 32}),
|
|
"temporal_halo_tokens": ("INT", {"default": 4, "min": 0, "max": 16}),
|
|
"decode_groups_per_chunk": ("INT", {"default": 1, "min": 1, "max": 8}),
|
|
}}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("segment_path", "segment_manifest_path", "report")
|
|
FUNCTION = "refine"
|
|
CATEGORY = CATEGORY
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return float("nan")
|
|
|
|
def refine(self, checkpoint_path, output_render_id, video_vae, upscaler_model, target_preset,
|
|
target_width, target_height, upscaler_device, upscaler_precision,
|
|
temporal_core_tokens, temporal_halo_tokens, decode_groups_per_chunk):
|
|
import comfy.model_management as mm
|
|
import nodes
|
|
|
|
path, manifest, video, audio_latent, wave, rate = _read_checkpoint(checkpoint_path)
|
|
source_run = str(manifest["render_id"])
|
|
run = _safe_name(output_render_id) if str(output_render_id).strip() else source_run
|
|
index = int(manifest["segment_index"])
|
|
fps = int(manifest["fps"])
|
|
native_width = int(manifest["native_width"])
|
|
native_height = int(manifest["native_height"])
|
|
width, height = _delivery_dimensions(
|
|
native_width, native_height, str(target_preset),
|
|
int(target_width), int(target_height),
|
|
)
|
|
if width < native_width or height < native_height:
|
|
raise ValueError("Learned H3 upscale must not downscale the native canvas")
|
|
if upscaler_device == "cpu" and upscaler_precision != "fp32":
|
|
raise ValueError("CPU learned lift requires fp32 precision")
|
|
if not upscaler_model or not folder_paths.get_full_path("latent_upscale_models", upscaler_model):
|
|
raise ValueError("Select an installed H3 3D .safetensors latent-upscaler model")
|
|
|
|
internal_width, internal_height = _delivery_cover_size(
|
|
native_width, native_height, width, height, 32
|
|
)
|
|
klass = nodes.NODE_CLASS_MAPPINGS.get("MinimaxH3LatentUpscaler3D")
|
|
if klass is None:
|
|
raise RuntimeError("Install/enable Comfyui_Minimax_h3_latent_Upscaler and restart ComfyUI")
|
|
|
|
segment_dir = _run_dir(run) / "segments"
|
|
final = segment_dir / f"segment_{index:05d}.mp4"
|
|
final_manifest = final.with_suffix(".json")
|
|
sidecar = final.with_suffix(final.suffix + ".iamccs.json")
|
|
if final.exists() or final_manifest.exists() or sidecar.exists():
|
|
raise FileExistsError(f"Upscaled segment already exists: {final}")
|
|
temporary_final = final.with_name(f"{final.stem}.{uuid.uuid4().hex}.tmp.mp4")
|
|
|
|
try:
|
|
# Queue 2 never needs to reload H3 itself: release any resident model
|
|
# before the learned lift and keep the spatial operation full-frame.
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
refined_video = _learned_3d_temporal_lift(
|
|
klass, video, upscaler_model, internal_width, internal_height,
|
|
upscaler_device, upscaler_precision,
|
|
int(temporal_core_tokens), int(temporal_halo_tokens),
|
|
)
|
|
expected_h, expected_w = internal_height // 16, internal_width // 16
|
|
if tuple(refined_video.shape[-2:]) != (expected_h, expected_w):
|
|
raise RuntimeError(
|
|
"Learned H3 upscale returned an unexpected canvas: "
|
|
f"{refined_video.shape[-1] * 16}x{refined_video.shape[-2] * 16}"
|
|
)
|
|
refined_cpu = _nested_latent(
|
|
refined_video.detach().to("cpu"), audio_latent.detach().to("cpu")
|
|
)
|
|
del refined_video, video, audio_latent
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
|
|
from .iamccs_minimax_h3_pixel_refine_variant import _provider
|
|
|
|
raw = Path(_out0(_provider("nodes_save").MMH3StreamingSave.execute(
|
|
latent=refined_cpu, vae=video_vae, groups_per_chunk=int(decode_groups_per_chunk),
|
|
fps=float(fps), filename_prefix=f"{ROOT_NAME}/{run}/raw/segment_{index:05d}",
|
|
crf=16, audio={"waveform": wave, "sample_rate": rate}, save_metadata=False,
|
|
)))
|
|
del refined_cpu
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
raw_count, raw_width, raw_height, raw_audio = _video_frames(raw)
|
|
if raw_count < int(manifest["source_frames"]) or not raw_audio:
|
|
raise RuntimeError("Streaming decode has missing frames or native audio; segment not accepted")
|
|
if (raw_width, raw_height) != (internal_width, internal_height):
|
|
raise RuntimeError("Streaming decode does not match the learned full-frame canvas")
|
|
technical = int(manifest["technical_prefix_frames"])
|
|
overlap = int(manifest["join_overlap_frames"])
|
|
join_mode = str(manifest["join_mode"])
|
|
head = technical + (overlap if join_mode == "cut" else 0)
|
|
end = int(manifest["source_frames"])
|
|
_trim_encoded(raw, temporary_final, head, end, width, height, fps)
|
|
frame_count, out_width, out_height, out_audio = _video_frames(temporary_final)
|
|
if (frame_count, out_width, out_height, out_audio) != (end - head, width, height, True):
|
|
raise RuntimeError("Final segment failed exact frame/canvas/audio validation")
|
|
result = {
|
|
"schema": SCHEMA, "render_id": run, "segment_index": index,
|
|
"source_render_id": source_run,
|
|
"checkpoint_sha256": manifest["checkpoint_sha256"],
|
|
"frame_count": frame_count, "width": width, "height": height, "fps": fps,
|
|
"has_audio": True, "join_mode": join_mode,
|
|
"join_overlap_frames": overlap if join_mode == "crossfade" else 0,
|
|
"technical_prefix_trimmed": technical,
|
|
"upscale_method": "learned_3d_grid_free",
|
|
"source_canvas": [native_width, native_height],
|
|
"target_preset": str(target_preset),
|
|
"learned_canvas": [internal_width, internal_height],
|
|
"temporal_core_tokens": int(temporal_core_tokens),
|
|
"temporal_halo_tokens": int(temporal_halo_tokens),
|
|
}
|
|
final_manifest, result = _publish_validated_segment(temporary_final, final, result)
|
|
return str(final), str(final_manifest), (
|
|
f"H3 learned 3D upscale {native_width}x{native_height} -> "
|
|
f"{width}x{height} ({target_preset}); saved {frame_count} frames; "
|
|
f"isotropic latent canvas={internal_width}x{internal_height}, "
|
|
f"temporal core/halo={temporal_core_tokens}/{temporal_halo_tokens}; "
|
|
f"no spatial diffusion tiles; native audio preserved: {final}"
|
|
)
|
|
finally:
|
|
if temporary_final.exists():
|
|
temporary_final.unlink()
|
|
mm.unload_all_models()
|
|
mm.soft_empty_cache()
|
|
gc.collect()
|
|
|
|
|
|
class IAMCCS_H3DiskUpscaleAssemble:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {
|
|
"render_id": ("STRING", {"default": "h3_upscale_test"}),
|
|
"segment_count": ("INT", {"default": 1, "min": 1, "max": 1000}),
|
|
"output_name": ("STRING", {"default": "final_full_hd"}),
|
|
}}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING")
|
|
RETURN_NAMES = ("film_path", "report")
|
|
FUNCTION = "assemble"
|
|
CATEGORY = CATEGORY
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return float("nan")
|
|
|
|
def assemble(self, render_id, segment_count, output_name):
|
|
from .iamccs_minimax_h3_shotboard import _concat_videos, _concat_videos_overlap
|
|
|
|
run = _safe_name(render_id)
|
|
directory = _run_dir(run) / "segments"
|
|
paths = [directory / f"segment_{i:05d}.mp4" for i in range(int(segment_count))]
|
|
manifests = []
|
|
for index, path in enumerate(paths):
|
|
if not path.is_file() or not path.with_suffix(".json").is_file():
|
|
raise FileNotFoundError(f"Missing upscaled segment or manifest: {path}")
|
|
data = json.loads(path.with_suffix(".json").read_text(encoding="utf-8"))
|
|
if (data.get("schema") != SCHEMA or data.get("render_id") != run
|
|
or int(data.get("segment_index", -1)) != index
|
|
or Path(data.get("segment_path", "")).resolve() != path.resolve()):
|
|
raise ValueError(f"Segment manifest order/schema mismatch: {path}")
|
|
if _sha256(path) != data.get("segment_sha256"):
|
|
raise ValueError(f"Upscaled segment checksum mismatch: {path}")
|
|
count, width, height, audio = _video_frames(path)
|
|
if (count, width, height, audio) != (int(data["frame_count"]), int(data["width"]), int(data["height"]), True):
|
|
raise ValueError(f"Segment frame/canvas/audio mismatch: {path}")
|
|
manifests.append(data)
|
|
width, height, fps = (int(manifests[0][key]) for key in ("width", "height", "fps"))
|
|
if any((int(m["width"]), int(m["height"]), int(m["fps"])) != (width, height, fps) for m in manifests):
|
|
raise ValueError("All H3 upscale segments must have an identical canvas and frame rate")
|
|
if len(manifests) > 1:
|
|
modes = {str(m["join_mode"]) for m in manifests[1:]}
|
|
if len(modes) != 1:
|
|
raise ValueError("Mixed cut/crossfade joins are not supported in this standalone assembler")
|
|
mode = modes.pop()
|
|
overlaps = {int(m["join_overlap_frames"]) for m in manifests[1:]}
|
|
if mode == "crossfade" and (len(overlaps) != 1 or min(overlaps) < 2):
|
|
raise ValueError("Crossfade segments must share one overlap of at least two frames")
|
|
if mode == "crossfade" and any(
|
|
int(m["frame_count"]) <= next(iter(overlaps)) for m in manifests
|
|
):
|
|
raise ValueError("Crossfade overlap must be shorter than every segment")
|
|
else:
|
|
mode, overlaps = "cut", {0}
|
|
output = _run_dir(run) / f"{_safe_name(output_name)}.mp4"
|
|
if output.exists():
|
|
raise FileExistsError(f"Disk Upscale refuses to overwrite {output}")
|
|
join_frames = next(iter(overlaps)) if mode == "crossfade" else 0
|
|
expected_count = sum(int(m["frame_count"]) for m in manifests) - join_frames * (len(paths) - 1)
|
|
temporary_output = output.with_name(f"{output.stem}.{uuid.uuid4().hex}.tmp.mp4")
|
|
try:
|
|
if mode == "crossfade":
|
|
_concat_videos_overlap(paths, temporary_output, join_frames, fps)
|
|
else:
|
|
_concat_videos(paths, temporary_output)
|
|
count, actual_width, actual_height, has_audio = _video_frames(temporary_output)
|
|
if (count, actual_width, actual_height, has_audio) != (expected_count, width, height, True):
|
|
raise RuntimeError("Assembled film failed exact frame/canvas/audio validation")
|
|
if output.exists():
|
|
raise FileExistsError(f"Disk Upscale refuses to overwrite {output}")
|
|
os.rename(temporary_output, output)
|
|
finally:
|
|
if temporary_output.exists():
|
|
temporary_output.unlink()
|
|
return str(output), f"H3 disk upscale film ready: {count} frames, {width}x{height}, {fps} fps, {len(paths)} segments: {output}"
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"IAMCCS_H3DiskUpscaleCheckpoint": IAMCCS_H3DiskUpscaleCheckpoint,
|
|
"IAMCCS_H3DiskUpscaleLoad": IAMCCS_H3DiskUpscaleLoad,
|
|
"IAMCCS_H3DiskUpscaleTiledRefine": IAMCCS_H3DiskUpscaleTiledRefine,
|
|
"IAMCCS_H3DiskUpscaleLearned3D": IAMCCS_H3DiskUpscaleLearned3D,
|
|
"IAMCCS_H3DiskUpscaleAssemble": IAMCCS_H3DiskUpscaleAssemble,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"IAMCCS_H3DiskUpscaleCheckpoint": "IAMCCS H3 Disk Upscale · 1 Save AV Checkpoint",
|
|
"IAMCCS_H3DiskUpscaleLoad": "IAMCCS H3 Disk Upscale · Load AV Checkpoint",
|
|
"IAMCCS_H3DiskUpscaleTiledRefine": "IAMCCS H3 Disk Upscale · 2 Tiled Refine + Stream",
|
|
"IAMCCS_H3DiskUpscaleLearned3D": "IAMCCS H3 Disk Upscale · 2 Learned 3D (Grid-Free) + Stream",
|
|
"IAMCCS_H3DiskUpscaleAssemble": "IAMCCS H3 Disk Upscale · 3 Assemble Film",
|
|
}
|