Update H3 tools and fix Scout lazy bypass
This commit is contained in:
@@ -3161,3 +3161,10 @@ from .iamccs_moss_frame_output import (
|
||||
)
|
||||
NODE_CLASS_MAPPINGS.update(_MOSS_RESULT_CLASSES)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(_MOSS_RESULT_NAMES)
|
||||
|
||||
from .iamccs_minimax_h3_open_refine import (
|
||||
NODE_CLASS_MAPPINGS as _OPEN_H3_REFINE_CLASSES,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as _OPEN_H3_REFINE_NAMES,
|
||||
)
|
||||
NODE_CLASS_MAPPINGS.update(_OPEN_H3_REFINE_CLASSES)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(_OPEN_H3_REFINE_NAMES)
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# SPDX-FileCopyrightText: 2026 Carmine Cristallo Scalzi (IAMCCS)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Read-only continuation diagnostics. Reports are evidence, not identity scores."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
def settings_schema():
|
||||
# Retired fields keep their positions so existing workflows still load.
|
||||
retired = "Retired compatibility field. Ignored; no repair or reference injection runs."
|
||||
return {
|
||||
"h3_guard_mode": (["off", "monitor", "repair"], {"default": "off", "display_name": "GUARD - MODE", "tooltip": "Read-only Extended AV diagnostics. Legacy repair values resolve to Off with a warning."}),
|
||||
"h3_guard_reference_memory": ("BOOLEAN", {"default": True, "display_name": "RETIRED - REFERENCE MEMORY", "tooltip": retired}),
|
||||
"h3_guard_repair_denoise": ("FLOAT", {"default": 0.15, "min": 0.01, "max": 0.35, "step": 0.01, "display_name": "RETIRED - REPAIR DENOISE", "tooltip": retired}),
|
||||
"h3_guard_repair_steps": ("INT", {"default": 6, "min": 1, "max": 30, "display_name": "RETIRED - REPAIR STEPS", "tooltip": retired}),
|
||||
"h3_guard_detail_ratio": ("FLOAT", {"default": 0.65, "min": 0.1, "max": 1.0, "step": 0.01, "display_name": "GUARD - DETAIL LOSS THRESHOLD", "tooltip": "Diagnostic heuristic only. Camera, focus and motion changes can cause false alarms."}),
|
||||
"h3_guard_motion_tolerance": ("FLOAT", {"default": 0.20, "min": 0.01, "max": 1.0, "step": 0.01, "display_name": "RETIRED - MOTION CHANGE LIMIT", "tooltip": retired}),
|
||||
}
|
||||
|
||||
|
||||
|
||||
def effective_mode(config):
|
||||
mode = config.get("mode", "off")
|
||||
if mode == "repair":
|
||||
logging.getLogger(__name__).warning(
|
||||
"Continuity Guard Repair has been retired after unsuccessful validation; using Off. "
|
||||
"Select Monitor for read-only diagnostics.")
|
||||
return "off"
|
||||
if mode not in {"off", "monitor"}:
|
||||
raise ValueError("Unknown Continuity Guard mode")
|
||||
return mode
|
||||
|
||||
|
||||
def configuration(settings):
|
||||
mode = effective_mode({"mode": settings.get("h3_guard_mode", "off")})
|
||||
threshold = float(settings.get("h3_guard_detail_ratio", 0.65))
|
||||
if not math.isfinite(threshold) or not 0 < threshold <= 1:
|
||||
raise ValueError("Invalid Continuity Guard detail ratio")
|
||||
return {"mode": mode, "detail_ratio": threshold}
|
||||
|
||||
|
||||
def storage(output_directory, render_id):
|
||||
if not render_id:
|
||||
raise ValueError("Continuity Guard requires the R49 render identity")
|
||||
key = hashlib.sha256(str(render_id).encode()).hexdigest()[:24]
|
||||
path = Path(output_directory) / "IAMCCS" / "continuity_guard" / key
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def conditioning_identity(render_id, index):
|
||||
# R49 assigns the definitive, numbered name at the first native checkpoint.
|
||||
if int(index) == 0:
|
||||
return uuid.uuid4().hex
|
||||
if not str(render_id or "").strip():
|
||||
raise ValueError("Guard continuation requires its predecessor's checkpoint render identity")
|
||||
return str(render_id)
|
||||
|
||||
|
||||
def bind_checkpoint(output_directory, report, render_id):
|
||||
"""Bind the first segment's provisional measurements to R49's final name."""
|
||||
key = report["storage_key"]
|
||||
if not isinstance(key, str) or not re.fullmatch(r"[0-9a-f]{24}", key):
|
||||
raise ValueError("Invalid Continuity Guard storage key")
|
||||
source = Path(output_directory) / "IAMCCS" / "continuity_guard" / key
|
||||
destination = storage(output_directory, render_id)
|
||||
if source == destination:
|
||||
return
|
||||
if int(report["segment_index"]) != 0:
|
||||
raise ValueError("Guard continuation measurements do not match the checkpoint render identity")
|
||||
for name in ("baseline.json", "segment_0000.json"):
|
||||
path = source / name
|
||||
if path.is_file():
|
||||
shutil.copy2(path, destination / name)
|
||||
if not (destination / "baseline.json").is_file():
|
||||
raise ValueError("Guard first-segment baseline is missing before checkpoint binding")
|
||||
write_report(destination / "checkpoint_identity.json", {"render_id": str(render_id), "provisional_storage_key": key})
|
||||
|
||||
|
||||
def write_report(path, data):
|
||||
temporary = path.with_suffix(".tmp")
|
||||
temporary.write_text(json.dumps(data, indent=2, allow_nan=False), encoding="utf-8")
|
||||
temporary.replace(path)
|
||||
|
||||
|
||||
def analysis_frames(images):
|
||||
# One analysis size for all segments; at most 48 frames, with uniform timing.
|
||||
ids = torch.linspace(0, images.shape[0] - 1, min(48, images.shape[0]), device=images.device).round().long()
|
||||
x = images[ids, ..., :3].movedim(-1, 1).float()
|
||||
x = F.interpolate(x, size=(144, 256), mode="area")
|
||||
return (x[:, 0] * 0.2126 + x[:, 1] * 0.7152 + x[:, 2] * 0.0722).clamp(0, 1).cpu().numpy()
|
||||
|
||||
|
||||
def measure(images):
|
||||
import cv2
|
||||
gray = analysis_frames(images)
|
||||
detail = [float(np.mean(np.abs(cv2.Laplacian(f, cv2.CV_32F)))) for f in gray]
|
||||
light = gray.mean(axis=(1, 2))
|
||||
intervals = max(1, images.shape[0] - 1) / max(1, len(gray) - 1)
|
||||
flow = []
|
||||
for a, b in zip(gray[:-1], gray[1:]):
|
||||
field = cv2.calcOpticalFlowFarneback((a * 255).astype('uint8'), (b * 255).astype('uint8'), None, .5, 3, 15, 3, 5, 1.2, 0)
|
||||
flow.append(float(np.median(np.linalg.norm(field, axis=-1))) / intervals)
|
||||
# Native-resolution tiles prevent large textured regions from hiding local blur.
|
||||
ids = torch.linspace(0, images.shape[0]-1, min(24, images.shape[0]), device=images.device).long()
|
||||
native = images[ids, ..., :3].float().mean(-1).cpu().numpy()
|
||||
tiles = []
|
||||
for row in range(4):
|
||||
for col in range(4):
|
||||
patch = native[:, row*native.shape[1]//4:(row+1)*native.shape[1]//4,
|
||||
col*native.shape[2]//4:(col+1)*native.shape[2]//4]
|
||||
values = [float(np.mean(abs(cv2.Laplacian(f, cv2.CV_32F)))) for f in patch]
|
||||
tiles.append(float(np.median(values)))
|
||||
return {"local_detail": tiles, "detail": float(np.median(detail)), "luminance": float(np.median(light)),
|
||||
"flicker": float(np.median(np.abs(np.diff(light, n=2)))) if len(light) > 2 else 0.0,
|
||||
"motion": float(np.median(flow)) if flow else 0.0,
|
||||
"frames": int(images.shape[0]), "analysis": "luma_texture_optical_flow_v1",
|
||||
"limitations": "No semantic identity, intent or lip-sync certification."}
|
||||
|
||||
|
||||
def assess(current, baseline, config):
|
||||
ratio = current["detail"] / max(baseline["detail"], 1e-6)
|
||||
exposure = abs(current["luminance"] - baseline["luminance"])
|
||||
moving = current["motion"] / max(baseline["motion"], 0.02)
|
||||
uncertain = exposure > 0.15 or moving < 0.4 or moving > 2.5
|
||||
local_before = baseline.get("local_detail", [])
|
||||
local_now = current.get("local_detail", [])
|
||||
local_ratios = [b/max(a, 1e-6) for a,b in zip(local_before, local_now)]
|
||||
# 10% local texture loss is a candidate trigger, never proof of degradation.
|
||||
affected = [i for i,r in enumerate(local_ratios) if r < .90 and local_before[i] > .005]
|
||||
loss = ratio < config["detail_ratio"] or bool(affected)
|
||||
return {"detail_ratio": ratio, "local_detail_ratios": local_ratios, "affected_tiles": affected, "exposure_change": exposure, "motion_ratio": moving,
|
||||
"texture_loss_detected": bool(loss and not uncertain),
|
||||
"review_recommended": bool(uncertain or loss)}
|
||||
|
||||
|
||||
def process(config, sampled, raw_frames, *, directory, index, head_frames, visible_frames):
|
||||
"""Measure decoded frames without modifying video, audio or conditioning."""
|
||||
if effective_mode(config) != "monitor":
|
||||
return sampled, raw_frames, {"status": "off", "repair_attempts": 0}
|
||||
|
||||
visible = raw_frames[head_frames:head_frames + visible_frames]
|
||||
current = measure(visible)
|
||||
baseline_path = directory / "baseline.json"
|
||||
report = {"schema": "iamccs.guard.report.v1", "segment_index": index,
|
||||
"storage_key": directory.name,
|
||||
"mode": config["mode"], "before": current, "repair_attempts": 0,
|
||||
"audio_policy": "Original sampled audio preserved exactly", "status": "monitored"}
|
||||
if index == 0:
|
||||
write_report(baseline_path, current)
|
||||
report["status"] = "baseline_recorded"
|
||||
elif not baseline_path.is_file():
|
||||
raise ValueError("Guard baseline missing. Start this render from its first segment.")
|
||||
else:
|
||||
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
verdict = assess(current, baseline, config)
|
||||
report["assessment"] = verdict
|
||||
if verdict["review_recommended"]:
|
||||
report["status"] = "review_recommended"
|
||||
write_report(directory / f"segment_{index:04d}.json", report)
|
||||
sampled = {**sampled, "_iamccs_continuity_guard": report}
|
||||
return sampled, raw_frames, report
|
||||
@@ -1426,7 +1426,15 @@ def _secondary_lora_settings(shotplan: dict[str, Any]) -> dict[str, Any]:
|
||||
return settings if isinstance(settings, dict) else {"enabled": False}
|
||||
|
||||
|
||||
def _apply_secondary_lora(model, shotplan: dict[str, Any], include_vfx=False):
|
||||
def _secondary_lora_applies_to_segment(shotplan: dict[str, Any], chunk_index: int = 0) -> bool:
|
||||
"""Scope the optional slot to temporal segments, including external resumes."""
|
||||
settings = _secondary_lora_settings(shotplan)
|
||||
return (not bool(settings.get("continuation_only", False))
|
||||
or int(chunk_index) > 0
|
||||
or _external_continuation_active(shotplan, int(chunk_index)))
|
||||
|
||||
|
||||
def _apply_secondary_lora(model, shotplan: dict[str, Any], include_vfx=False, chunk_index=0):
|
||||
"""Apply an independent model-only H3 LoRA without changing sampling.
|
||||
|
||||
The second slot is deliberately separate from Turbo. It never selects a
|
||||
@@ -1447,6 +1455,8 @@ def _apply_secondary_lora(model, shotplan: dict[str, Any], include_vfx=False):
|
||||
return model, f"off (optional H3 LoRA unavailable: {missing})"
|
||||
return model, "off"
|
||||
|
||||
if not _secondary_lora_applies_to_segment(shotplan, chunk_index):
|
||||
return model, "off (segment 1; second LoRA starts at segment 2)"
|
||||
lora_name = str(settings.get("lora_name", "") or "").strip()
|
||||
strength = float(settings.get("strength", 0.0) or 0.0)
|
||||
if not lora_name or abs(strength) <= 1e-8:
|
||||
@@ -1474,7 +1484,8 @@ def _apply_secondary_lora(model, shotplan: dict[str, Any], include_vfx=False):
|
||||
f"Could not apply secondary MiniMax H3 LoRA '{lora_name}'. "
|
||||
"Verify that it is compatible with the selected FL2VA/REF2VA base model."
|
||||
) from exc
|
||||
return patched, f"native model-only H3 LoRA {lora_name}@{strength:.2f}"
|
||||
scope = "continuation-only" if settings.get("continuation_only", False) else "all-segments"
|
||||
return patched, f"native model-only H3 LoRA {lora_name}@{strength:.2f} ({scope}, segment {int(chunk_index) + 1})"
|
||||
|
||||
|
||||
def _turbo_sampler(shotplan: dict[str, Any]):
|
||||
@@ -3070,6 +3081,11 @@ class IAMCCS_MiniMaxH3AtomicConditioningBackend:
|
||||
prefix_frames=control_prefix_frames,
|
||||
)
|
||||
positive, refmod_report = _apply_h3_refmod(positive, shotplan)
|
||||
guard_config = shotplan.get("continuity_guard", {})
|
||||
if guard_config.get("mode", "off") == "monitor" and shotboard_task == "fl2va_extended_av":
|
||||
from .iamccs_h3_continuity_guard import conditioning_identity
|
||||
guard_id = conditioning_identity(render_id, segment_index)
|
||||
motion_state["continuity_guard_render_id"] = guard_id
|
||||
if legacy_actual_output_bridge:
|
||||
motion_report += "; legacy_actual_output_bridge_ignored"
|
||||
if motion_state["active"] and motion_state.get("strategy") == "reference_motion_carry":
|
||||
@@ -3269,7 +3285,7 @@ class IAMCCS_MiniMaxH3GenerationBackendV2:
|
||||
turbo_model, turbo_report = _apply_turbo_lora(model, shotplan)
|
||||
fasth3_model, fasth3_report = _apply_fasth3_dense_lora(turbo_model, shotplan)
|
||||
pdd_model, pdd_report = _apply_pdd_lora(fasth3_model, shotplan)
|
||||
lora_model, secondary_lora_report = _apply_secondary_lora(pdd_model, shotplan, include_vfx=True)
|
||||
lora_model, secondary_lora_report = _apply_secondary_lora(pdd_model, shotplan, include_vfx=True, chunk_index=chunk_index)
|
||||
if turbo_enabled:
|
||||
# Apply the selected Turbo LoRA without silently replacing any
|
||||
# authored sampler, scheduler or AV shift. Presets populate those
|
||||
@@ -3558,6 +3574,16 @@ class IAMCCS_MiniMaxH3GenerationBackendV2:
|
||||
cleanup_report = _clean_vram_before_decode()
|
||||
|
||||
native_frames = comfy_nodes.VAEDecode().decode(vae=video_vae, samples=sampled)[0]
|
||||
guard_report = {"status": "off"}
|
||||
guard_config = shotplan.get("continuity_guard", {})
|
||||
if guard_config.get("mode", "off") == "monitor" and shotplan.get("task_mode") == "fl2va_extended_av":
|
||||
from .iamccs_h3_continuity_guard import process as process_guard, storage as guard_storage
|
||||
guard_directory = guard_storage(folder_paths.get_output_directory(), motion_state["continuity_guard_render_id"])
|
||||
sampled, native_frames, guard_report = process_guard(
|
||||
guard_config, sampled, native_frames, directory=guard_directory, index=int(chunk_index),
|
||||
head_frames=int(motion_state.get("trim_frames", 0)),
|
||||
visible_frames=int(motion_state["export_frames"]))
|
||||
LOG.info("IAMCCS Continuity Guard | chunk=%d | %s | report=%s", int(chunk_index)+1, guard_report["status"], guard_directory)
|
||||
native_audio = VAEDecodeAudio.execute(vae=audio_vae, samples=sampled)[0]
|
||||
if (
|
||||
isinstance(sampled, dict)
|
||||
@@ -3977,6 +4003,7 @@ class IAMCCS_MiniMaxH3GenerationBackendV2:
|
||||
f"lipsync_lock={lipsync_lock_report} | "
|
||||
f"pre_sample_cleanup={conditioning_cleanup} | "
|
||||
f"terminal_closure={terminal_closure_report} | "
|
||||
f"continuity_guard={guard_report['status']} | "
|
||||
f"pre_decode_cleanup={cleanup_report} | native_last_frame=captured | "
|
||||
f"resolution={resolution_contract} | "
|
||||
f"pianosequenza={pianosequenza_report} | "
|
||||
|
||||
@@ -214,7 +214,7 @@ class IAMCCS_MiniMaxH3LatentUpresSamplingR38:
|
||||
shift_video = float(sampling.get("shift_video", 12.0))
|
||||
shift_audio = float(sampling.get("shift_audio", 3.0))
|
||||
turbo_model, turbo_report = _apply_turbo_lora(model, plan)
|
||||
lora_model, secondary_lora_report = _apply_secondary_lora(turbo_model, plan)
|
||||
lora_model, secondary_lora_report = _apply_secondary_lora(turbo_model, plan, chunk_index=segment_index)
|
||||
shifted = MiniMaxH3SigmaShift.execute(model=lora_model, shift_video=shift_video, shift_audio=shift_audio)[0]
|
||||
if str(plan.get("acceleration", "native")).lower() == "comfy_kitchen":
|
||||
from comfy_extras.nodes_model_advanced import ModelAttentionBackend
|
||||
|
||||
@@ -358,7 +358,7 @@ class IAMCCS_MiniMaxH3MaskedLoopGuidedSampler:
|
||||
turbo_model, turbo_report = _apply_turbo_lora(model, shotplan)
|
||||
fasth3_model, fasth3_report = _apply_fasth3_dense_lora(turbo_model, shotplan)
|
||||
pdd_model, pdd_report = _apply_pdd_lora(fasth3_model, shotplan)
|
||||
lora_model, secondary_report = _apply_secondary_lora(pdd_model, shotplan)
|
||||
lora_model, secondary_report = _apply_secondary_lora(pdd_model, shotplan, chunk_index=chunk_index)
|
||||
turbo = _turbo_settings(shotplan)
|
||||
turbo_name = str(turbo.get("lora_name", "") or "").strip()
|
||||
turbo_enabled = bool(
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Settings bridge for the stock-node Open-H3 delivery branch."""
|
||||
|
||||
import folder_paths
|
||||
|
||||
|
||||
PREFIX = "h3_open_refine_"
|
||||
SIGMAS = {
|
||||
"gentle_3step": "0.8, 0.6857142857, 0.48, 0",
|
||||
"reference_3step": "0.909375, 0.857143, 0.666667, 0",
|
||||
}
|
||||
|
||||
|
||||
def settings_schema():
|
||||
loras = ["", *folder_paths.get_filename_list("loras")]
|
||||
upscalers = ["", *folder_paths.get_filename_list("latent_upscale_models")] if "latent_upscale_models" in folder_paths.folder_names_and_paths else [""]
|
||||
refiner = next((name for name in loras if name.lower().endswith("minimax_h3_open_regenerate_2k_refiner_rank_384_bf16.safetensors")), "")
|
||||
distill = next((name for name in loras if name.lower().endswith("minimax_h3_fl2v_lightx2v_turbo_4step_v0.1_comfy.safetensors")), "")
|
||||
upscaler = next((name for name in upscalers if name.lower().endswith("minimax_h3_latent_upscaler_3d_fp16.safetensors")), "")
|
||||
fields = {
|
||||
"enabled": ("BOOLEAN", {"default": False, "display_name": "OPEN-H3 · ENABLE REFINEMENT", "tooltip": "One native generation followed by Open-H3. Replaces the other upscale routes; requires the Open-H3 workflow branch."}),
|
||||
"scale": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 4.0, "step": 0.05, "display_name": "OPEN-H3 · SPATIAL SCALE", "tooltip": "Learned latent upscale. Output follows the native aspect ratio, rounded to the H3 grid. Full-resolution refinement can exceed low-VRAM capacity."}),
|
||||
"model_name": (upscalers, {"default": upscaler, "display_name": "OPEN-H3 · LATENT UPSCALER"}),
|
||||
"precision": (["fp16", "bf16", "fp32"], {"default": "fp16", "display_name": "OPEN-H3 · UPSCALER PRECISION"}),
|
||||
"device": (["cuda", "rocm", "cpu"], {"default": "cuda", "display_name": "OPEN-H3 · UPSCALER DEVICE"}),
|
||||
"temporal_chunking": ("BOOLEAN", {"default": True, "display_name": "OPEN-H3 · UPSCALER CHUNKING", "tooltip": "Chunks the latent upscaler only. The three-step H3 sampler still processes the complete target latent."}),
|
||||
"lora_name": (loras, {"default": refiner, "display_name": "OPEN-H3 · REFINER LORA"}),
|
||||
"strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display_name": "OPEN-H3 · REFINER STRENGTH"}),
|
||||
"distill_lora_name": (loras, {"default": distill, "display_name": "OPEN-H3 · DISTILLATION LORA", "tooltip": "Kijai's rank-384 file contains only the refiner. The supplied community recipe pairs it with LightX2V v0.1; it is not NVIDIA's complete rank-576 bundle. Leave blank when the selected refiner already includes distillation."}),
|
||||
"distill_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01, "display_name": "OPEN-H3 · DISTILLATION STRENGTH"}),
|
||||
"sigma_preset": (["gentle_3step", "reference_3step", "custom"], {"default": "gentle_3step", "display_name": "OPEN-H3 · NOISE SCHEDULE", "tooltip": "Gentle starts at 0.8. Reference starts at 0.909375 and can change identity or performance. Neither preset guarantees preservation."}),
|
||||
"custom_sigmas": ("STRING", {"default": SIGMAS["gentle_3step"], "multiline": False, "display_name": "OPEN-H3 · CUSTOM SIGMAS", "tooltip": "Used only with Custom. Comma-separated Euler sigmas ending in zero."}),
|
||||
"seed": ("INT", {"default": 777, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": False, "display_name": "OPEN-H3 · REFINE SEED"}),
|
||||
"prompt": ("STRING", {"default": "", "multiline": True, "display_name": "OPEN-H3 · REFINE PROMPT", "tooltip": "Blank uses the current shot prompt. Describe the existing action and appearance; new instructions can redraw the performance."}),
|
||||
}
|
||||
return {PREFIX + name: spec for name, spec in fields.items()}
|
||||
|
||||
|
||||
def configuration(settings):
|
||||
return {name[len(PREFIX):]: settings.get(name, spec[1]["default"]) for name, spec in settings_schema().items()}
|
||||
|
||||
|
||||
class IAMCCS_MiniMaxH3OpenRefineControl:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"cine_linx": ("IAMCCS_SUPERNODE_LINX",), "segment_index": ("INT", {"default": 0, "min": 0, "forceInput": True})}}
|
||||
|
||||
# File names feed both legacy list-combos and V3 COMBO sockets.
|
||||
RETURN_TYPES = ("BOOLEAN", "STRING", "FLOAT", "*", "COMBO", "COMBO", "BOOLEAN", "*", "FLOAT", "*", "FLOAT", "STRING", "INT", "BOOLEAN")
|
||||
RETURN_NAMES = ("enabled", "prompt", "scale", "upscaler_name", "precision", "device", "temporal_chunking", "refiner_lora", "refiner_strength", "distill_lora", "distill_strength", "sigmas", "seed", "use_face_source")
|
||||
FUNCTION = "resolve"
|
||||
CATEGORY = "IAMCCS/MiniMax H3/Delivery"
|
||||
|
||||
def resolve(self, cine_linx, segment_index):
|
||||
# Atomic imports Shotboard, which imports this module's settings schema.
|
||||
from .iamccs_minimax_h3_atomic_backend import _resolve_shotplan
|
||||
|
||||
plan = _resolve_shotplan(cine_linx)
|
||||
cfg = plan.get("open_h3_refine") or configuration({})
|
||||
enabled = bool(cfg["enabled"])
|
||||
if enabled and bool(plan.get("upscale_enabled")) and plan.get("upscale_mode", "off") != "off":
|
||||
raise ValueError("Open-H3 replaces the other upscale routes. Select Open-H3 in Settings PRO Mode Assistant.")
|
||||
if enabled and not cfg["lora_name"]:
|
||||
raise ValueError("Select an Open-H3 refiner LoRA in Settings PRO Output.")
|
||||
if enabled and not cfg["model_name"]:
|
||||
raise ValueError("Select an H3 latent upscaler in Settings PRO Output.")
|
||||
chunks = plan.get("chunks") or []
|
||||
chunk = chunks[segment_index] if 0 <= segment_index < len(chunks) else {}
|
||||
prompt = cfg["prompt"] or chunk.get("prompt") or plan.get("global_prompt", "")
|
||||
sigmas = cfg["custom_sigmas"] if cfg["sigma_preset"] == "custom" else SIGMAS[cfg["sigma_preset"]]
|
||||
seed = (int(cfg["seed"]) + int(segment_index)) % (1 << 64)
|
||||
distill_strength = float(cfg["distill_strength"]) if cfg["distill_lora_name"] else 0.0
|
||||
use_face_source = bool(plan.get("face_detailer_settings", {}).get("enabled", False)) and plan.get("task_mode") != "longvid_keyframe_continuous"
|
||||
return (enabled, prompt, float(cfg["scale"]), cfg["model_name"], cfg["precision"], cfg["device"], bool(cfg["temporal_chunking"]), cfg["lora_name"], float(cfg["strength"]), cfg["distill_lora_name"], distill_strength, sigmas, seed, use_face_source)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"IAMCCS_MiniMaxH3OpenRefineControl": IAMCCS_MiniMaxH3OpenRefineControl}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"IAMCCS_MiniMaxH3OpenRefineControl": "MiniMax H3 · Open-H3 Refinement Settings"}
|
||||
@@ -20,6 +20,7 @@ from typing import Any
|
||||
|
||||
import folder_paths
|
||||
import torch
|
||||
from comfy_execution.graph import ExecutionBlocker
|
||||
|
||||
from .iamccs_minimax_h3_atomic_backend import (
|
||||
H3_FPS,
|
||||
@@ -78,14 +79,14 @@ def _apply_sparse(model, enabled: bool, budget: float, denser_edges: bool):
|
||||
) from exc
|
||||
|
||||
|
||||
def _active_sampling_model(model, plan: dict[str, Any]):
|
||||
def _active_sampling_model(model, plan: dict[str, Any], chunk_index=0):
|
||||
from comfy_extras.nodes_minimax_h3 import MiniMaxH3SigmaShift
|
||||
|
||||
sampling = plan.get("sampling") if isinstance(plan.get("sampling"), dict) else {}
|
||||
shift_video = float(sampling.get("shift_video", 12.0))
|
||||
shift_audio = float(sampling.get("shift_audio", 3.0))
|
||||
turbo_model, turbo_report = _apply_turbo_lora(model, plan)
|
||||
lora_model, secondary_report = _apply_secondary_lora(turbo_model, plan)
|
||||
lora_model, secondary_report = _apply_secondary_lora(turbo_model, plan, chunk_index=chunk_index)
|
||||
turbo = _turbo_settings(plan)
|
||||
turbo_name = str(turbo.get("lora_name", "") or "").strip()
|
||||
compatible, _ = _h3_lora_compatibility(turbo_name)
|
||||
@@ -123,11 +124,11 @@ class IAMCCS_MiniMaxH3SeedCandidateR40:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL",),
|
||||
"positive": ("CONDITIONING",),
|
||||
"latent": ("LATENT",),
|
||||
"model": ("MODEL", {"lazy": True}),
|
||||
"positive": ("CONDITIONING", {"lazy": True}),
|
||||
"latent": ("LATENT", {"lazy": True}),
|
||||
"cine_linx": (SUPERNODE_LINX_TYPE,),
|
||||
"chunk_index": ("INT", {"forceInput": True}),
|
||||
"chunk_index": ("INT", {"forceInput": True, "lazy": True}),
|
||||
"candidate_index": ("INT", {"default": 0, "min": 0, "max": 3, "step": 1}),
|
||||
}
|
||||
}
|
||||
@@ -137,7 +138,22 @@ class IAMCCS_MiniMaxH3SeedCandidateR40:
|
||||
FUNCTION = "sample"
|
||||
CATEGORY = CATEGORY
|
||||
|
||||
def sample(self, model, positive, latent, cine_linx, chunk_index, candidate_index):
|
||||
@staticmethod
|
||||
def _active(cine_linx, candidate_index):
|
||||
prefix, _ = IAMCCS_MiniMaxH3ScoutDeliveryLazyRouterR43._selection(cine_linx)
|
||||
settings = _scout_settings(_resolve_shotplan(cine_linx))
|
||||
count = max(1, min(4, int(settings.get("candidate_count", 1) or 1)))
|
||||
return prefix == "scout" and 0 <= int(candidate_index) < count
|
||||
|
||||
def check_lazy_status(self, cine_linx, candidate_index, **kwargs):
|
||||
if not self._active(cine_linx, candidate_index):
|
||||
return []
|
||||
return [name for name in ("model", "positive", "latent", "chunk_index") if kwargs.get(name) is None]
|
||||
|
||||
def sample(self, model=None, positive=None, latent=None, cine_linx=None, chunk_index=0, candidate_index=0):
|
||||
if not self._active(cine_linx, candidate_index):
|
||||
blocker = ExecutionBlocker(None)
|
||||
return blocker, blocker, 0, False, f"R40 candidate {int(candidate_index) + 1} bypassed"
|
||||
from comfy_extras.nodes_custom_sampler import (
|
||||
BasicGuider,
|
||||
BasicScheduler,
|
||||
@@ -148,26 +164,17 @@ class IAMCCS_MiniMaxH3SeedCandidateR40:
|
||||
|
||||
plan = _resolve_shotplan(cine_linx)
|
||||
scout = _scout_settings(plan)
|
||||
enabled = bool(scout.get("enabled", False))
|
||||
count = max(1, min(4, int(scout.get("candidate_count", 1) or 1))) if enabled else 1
|
||||
count = max(1, min(4, int(scout.get("candidate_count", 1) or 1)))
|
||||
index = max(0, min(3, int(candidate_index)))
|
||||
sampling = plan.get("sampling") if isinstance(plan.get("sampling"), dict) else {}
|
||||
base_seed = int(sampling.get("seed", 0) or 0)
|
||||
chunk_stride = int(sampling.get("seed_stride", 1) or 1)
|
||||
scout_stride = max(1, int(scout.get("seed_stride", 1_000_003) or 1_000_003))
|
||||
actual_seed = (base_seed + int(chunk_index) * chunk_stride + index * scout_stride) & 0xFFFFFFFFFFFFFFFF
|
||||
if index >= count:
|
||||
inactive_sampled = copy.copy(latent)
|
||||
inactive_x0 = copy.copy(latent)
|
||||
inactive_sampled["iamccs_r40_candidate_active"] = False
|
||||
inactive_x0["iamccs_r40_candidate_active"] = False
|
||||
report = f"R40 candidate {index + 1} inactive | configured candidates={count}"
|
||||
return inactive_sampled, inactive_x0, actual_seed, False, report
|
||||
|
||||
_audit_lipsync_audio_lock(plan, latent, int(chunk_index))
|
||||
cleanup_report = _release_conditioning_models(plan)
|
||||
active_model, turbo_enabled, turbo_report, secondary_report, acceleration_report, sparse_report = (
|
||||
_active_sampling_model(model, plan)
|
||||
_active_sampling_model(model, plan, chunk_index=chunk_index)
|
||||
)
|
||||
noise = RandomNoise.execute(noise_seed=actual_seed)[0]
|
||||
guider = BasicGuider.execute(model=active_model, conditioning=positive)[0]
|
||||
@@ -319,8 +326,8 @@ class IAMCCS_MiniMaxH3SeedPreviewR40:
|
||||
return {
|
||||
"required": {
|
||||
"latent": ("LATENT",),
|
||||
"video_vae": ("VAE",),
|
||||
"trim_frames": ("INT", {"forceInput": True}),
|
||||
"video_vae": ("VAE", {"lazy": True}),
|
||||
"trim_frames": ("INT", {"forceInput": True, "lazy": True}),
|
||||
"max_preview_frames": ("INT", {"default": 49, "min": 1, "max": 209, "step": 1}),
|
||||
"max_preview_width": ("INT", {"default": 512, "min": 128, "max": 1024, "step": 64}),
|
||||
}
|
||||
@@ -331,9 +338,14 @@ class IAMCCS_MiniMaxH3SeedPreviewR40:
|
||||
FUNCTION = "decode"
|
||||
CATEGORY = CATEGORY
|
||||
|
||||
def decode(self, latent, video_vae, trim_frames, max_preview_frames, max_preview_width):
|
||||
def check_lazy_status(self, latent, **kwargs):
|
||||
if not bool(latent.get("iamccs_r40_candidate_active", False)):
|
||||
return torch.zeros((1, 144, 256, 3), dtype=torch.float32), "R40 candidate inactive"
|
||||
return []
|
||||
return [name for name in ("video_vae", "trim_frames") if kwargs.get(name) is None]
|
||||
|
||||
def decode(self, latent, video_vae=None, trim_frames=0, max_preview_frames=49, max_preview_width=512):
|
||||
if not bool(latent.get("iamccs_r40_candidate_active", False)):
|
||||
return ExecutionBlocker(None), "R40 candidate inactive"
|
||||
import nodes as comfy_nodes
|
||||
|
||||
frames = comfy_nodes.VAEDecode().decode(vae=video_vae, samples=latent)[0]
|
||||
@@ -563,7 +575,7 @@ class IAMCCS_MiniMaxH3LatentUpresSamplingR40:
|
||||
sampling = plan.get("sampling") if isinstance(plan.get("sampling"), dict) else {}
|
||||
seed = (int(shot_lab.get("stage2_seed", 0)) + int(segment_index) * int(shot_lab.get("stage2_seed_stride", 1))) & 0xFFFFFFFFFFFFFFFF
|
||||
base, turbo_report = _apply_turbo_lora(model, plan)
|
||||
base, lora2_report = _apply_secondary_lora(base, plan)
|
||||
base, lora2_report = _apply_secondary_lora(base, plan, chunk_index=segment_index)
|
||||
base, stage2_lora_report = _stage2_lora(base, shot_lab)
|
||||
shifted = MiniMaxH3SigmaShift.execute(
|
||||
model=base,
|
||||
|
||||
@@ -29,6 +29,8 @@ from .iamccs_h3_face_swap import settings_schema as face_swap_schema, face_swap_
|
||||
from .iamccs_h3_character_swap_lora import settings_schema as character_swap_schema, swap_plan
|
||||
from .iamccs_h3_vfx_edit import settings_schema as vfx_edit_schema
|
||||
from .iamccs_h3_omni import configure as configure_omni
|
||||
from .iamccs_h3_continuity_guard import settings_schema as continuity_guard_schema, configuration as continuity_guard_configuration
|
||||
from .iamccs_minimax_h3_open_refine import settings_schema as open_h3_refine_schema, configuration as open_h3_refine_configuration
|
||||
|
||||
from .iamccs_minimax_h3_shotboard_core import (
|
||||
H3_FPS,
|
||||
@@ -3485,6 +3487,7 @@ class IAMCCS_MiniMaxH3ShotPlanner:
|
||||
extended_av_boundary_polish_strength = EXTEND_BOUNDARY_POLISH_DEFAULT_STRENGTH
|
||||
# Append-only Settings/PRO controls. They are intentionally NOT planner widgets,
|
||||
# so historical Shotboard widget positions remain byte-for-byte compatible.
|
||||
secondary_lora_continuation_only = False
|
||||
upscale_link_to_native = False
|
||||
upscale_link_factor = "640x384 -> 1280x768"
|
||||
import hashlib
|
||||
@@ -3647,6 +3650,7 @@ class IAMCCS_MiniMaxH3ShotPlanner:
|
||||
secondary_lora_enabled = saved_settings.get("secondary_lora_enabled", secondary_lora_enabled)
|
||||
secondary_lora_name = saved_settings.get("secondary_lora_name", secondary_lora_name)
|
||||
secondary_lora_strength = saved_settings.get("secondary_lora_strength", secondary_lora_strength)
|
||||
secondary_lora_continuation_only = bool(saved_settings.get("secondary_lora_continuation_only", False))
|
||||
pdd_lora_name = saved_settings.get("pdd_lora_name", pdd_lora_name)
|
||||
pdd_strength = saved_settings.get("pdd_strength", pdd_strength)
|
||||
h3_controlnet_enabled = saved_settings.get("h3_controlnet_enabled", h3_controlnet_enabled)
|
||||
@@ -4545,6 +4549,7 @@ class IAMCCS_MiniMaxH3ShotPlanner:
|
||||
"duplicate_of_turbo": bool(secondary_lora_duplicate),
|
||||
"lora_name": selected_secondary_lora,
|
||||
"strength": float(secondary_strength),
|
||||
"continuation_only": bool(secondary_lora_continuation_only),
|
||||
"loader": "ComfyUI LoraLoaderModelOnly",
|
||||
"source": "shotboard",
|
||||
}
|
||||
@@ -4765,6 +4770,7 @@ class IAMCCS_MiniMaxH3ShotPlanner:
|
||||
"soft_video_curve": str(saved_settings.get("h3_continuation_soft_video_curve", "smoothstep") or "smoothstep"),
|
||||
"soft_audio_ms": max(0.0, min(100.0, _finite_float(saved_settings.get("h3_continuation_soft_audio_ms", 15.0), 15.0, 0.0, 100.0))),
|
||||
}
|
||||
plan["continuity_guard"] = continuity_guard_configuration(saved_settings)
|
||||
plan["refmod_settings"] = {
|
||||
"schema": "iamccs.minimax_h3.refmod.v1",
|
||||
"enabled": bool(saved_settings.get("h3_refmod_enabled", False)),
|
||||
@@ -4802,6 +4808,7 @@ class IAMCCS_MiniMaxH3ShotPlanner:
|
||||
"route": "face_detailer_then_optional_upscale" if bool(face_detailer_enabled) else "native_then_optional_upscale",
|
||||
"source": "IAMCCS_ShotboardH3Settings" if _h3_settings_from_cine_linx(cine_linx) else "shotboard_defaults",
|
||||
}
|
||||
plan["open_h3_refine"] = open_h3_refine_configuration(saved_settings)
|
||||
configure_omni(plan, saved_settings)
|
||||
chunk_frames = [int(chunk.get("frame_count", 0) or 0) for chunk in plan.get("chunks", [])]
|
||||
max_chunk_frames = max(chunk_frames, default=0)
|
||||
@@ -5571,6 +5578,10 @@ class IAMCCS_MiniMaxH3NativeCheckpointSave:
|
||||
)
|
||||
|
||||
trim_count = max(0, int(trim_head_frames or 0))
|
||||
guard_report = sampled_latent.get("_iamccs_continuity_guard") if isinstance(sampled_latent, dict) else None
|
||||
if isinstance(guard_report, dict):
|
||||
from .iamccs_h3_continuity_guard import bind_checkpoint
|
||||
bind_checkpoint(folder_paths.get_output_directory(), guard_report, active_render_id)
|
||||
join_contract = _checkpoint_join_contract(cine_linx, current_segment, trim_count)
|
||||
motion_context_pretrimmed = bool(join_contract["context_pretrimmed"])
|
||||
overlap_stitch = join_contract["mode"] == "decoded_overlap"
|
||||
@@ -7630,6 +7641,12 @@ class IAMCCS_ShotboardH3Settings:
|
||||
"h3_omni_enabled": ("BOOLEAN", {"default": False,
|
||||
"display_name": "REF2VA · FASTH3 OMNI 8 STEP",
|
||||
"tooltip": "Full reference-conditioned checkpoint, not a LoRA. Select the Omni loader. Native PDD32, Euler/simple, 8 steps, shifts 12/3. Other modes keep their own model route."}),
|
||||
**continuity_guard_schema(),
|
||||
"secondary_lora_continuation_only": ("BOOLEAN", {
|
||||
"default": False, "display_name": "Second LoRA: from segment 2",
|
||||
"tooltip": "Apply the second LoRA only from segment 2 onward. A saved-AV checkpoint resume already counts as continuation. OFF preserves all-segment behavior. This controls temporal segments, not upscale passes.",
|
||||
}),
|
||||
**open_h3_refine_schema(),
|
||||
})}
|
||||
|
||||
RETURN_TYPES = (SUPERNODE_LINX_TYPE,)
|
||||
|
||||
+79
-13
@@ -893,12 +893,42 @@ def _normalise_ai_images(value: Any) -> list[dict[str, str]]:
|
||||
return images
|
||||
|
||||
|
||||
def _authored_h3_dialogue_lines(text: str) -> list[str]:
|
||||
"""Protect exact utterances for referenced characters and standalone voices."""
|
||||
return list(dict.fromkeys(re.findall(
|
||||
r"(?:<Subject\s+\d+>\s*)?\(S\d+(?:\s*,\s*S\d+)*\)[^<>\r\n]{0,160}<d>\[[^\]]+\][\s\S]*?</d>(?:\s*<(?:scenetrans|cutoff)>)*",
|
||||
str(text or ""), flags=re.IGNORECASE,
|
||||
)))
|
||||
|
||||
|
||||
def _official_h3_tag_rules(task_mode: str) -> str:
|
||||
"""Shared by Global, selected-field rewrite, and compact local fallback."""
|
||||
if task_mode in {"v2va_face_swap_lora", "ref2va_vfx_edit"}:
|
||||
return "Use this mode's native aligned-edit grammar; do not append full-reference labels to its specialized edit prompt."
|
||||
return (
|
||||
"Official H3 tag authoring contract applies even when the rough request has no tags. "
|
||||
"Format every supplied spoken or sung line as (S1) says <d>[Language] exact words</d>, "
|
||||
"assigning stable (S1), (S2) and later speaker IDs by first actual vocal event, independently of subject numbering. "
|
||||
"Keep voice, delivery and acting descriptions outside <d>; preserve the exact words and original language inside it. "
|
||||
"Never invent dialogue. Use <scenetrans> only for speech genuinely continuing across an authored cut, "
|
||||
"and <cutoff> only for speech intentionally interrupted by the clip ending; they are not generic transition decoration. "
|
||||
"For full-reference modes, define reusable people, objects or environments with stable <Subject N> labels. "
|
||||
"Bind their provenance to actual <Picture N> or <Video N> assets and use <Subject N> (Sx) for a referenced speaker. "
|
||||
"A Subject is content, a Picture is an image/frame, a Video is a source or temporal reference, and an Audio is an active audio reference or reuse source. "
|
||||
"Do not equate Subject and speaker numbers. Reference labels are category-specific and must resolve to available inputs or explicit existing context. "
|
||||
"Do not populate unused slots, invent four subjects, or create Audio labels merely because a video contains sound. "
|
||||
"Base T2VA/I2VA/FL2VA/L2VA keep their three-section grammar and applicable image alignment; "
|
||||
"full-reference modes keep their six sections. Global describes output scope, not a plain-prose exemption from these rules."
|
||||
)
|
||||
|
||||
|
||||
def _assistant_instruction(
|
||||
task_mode: str,
|
||||
sections: dict[str, str],
|
||||
user_direction: str = "",
|
||||
target_keys: Any = None,
|
||||
images: Any = None,
|
||||
request_text: str | None = None,
|
||||
) -> tuple[str, str]:
|
||||
mode = _normalise_task_mode(task_mode or "t2va")
|
||||
if mode not in MODE_SECTIONS:
|
||||
@@ -962,6 +992,27 @@ def _assistant_instruction(
|
||||
"When images are attached, analyze only the contribution named by each image role. An opening image governs the first frame; a closing image governs the last frame; identity, composition and style images govern only those named attributes. "
|
||||
"Never mention unavailable media or claim to have seen a detail that is not visible."
|
||||
)
|
||||
if request_text is not None:
|
||||
system = (
|
||||
"You compile a filmmaker's brief into MiniMax H3 prompt fields. Return one JSON object with plain-string values, "
|
||||
f"using exactly these selected keys: {selected}. "
|
||||
"Write all generated descriptions in English, even when the brief is in another language. "
|
||||
"Preserve supplied dialogue verbatim in its original language and retain exact reference tags and authored times. "
|
||||
"Develop the brief into concrete, filmable staging: chronological visible movement, physical interactions, performance, "
|
||||
"camera and location sound belong in their respective fields. Add only the minimal physical detail needed to stage the requested action. "
|
||||
"Do not merely copy or translate the brief into one field or repeat it across multiple fields. "
|
||||
"Do not invent characters, dialogue, extra actions, cuts, wardrobe, locations or a different story. "
|
||||
"A scene field establishes the setting and subjects; shot_list describes the visible action in order; acting describes body mechanics; "
|
||||
"camera describes the requested framing and movement; lighting describes the stated visual treatment; sound fields describe audible events. "
|
||||
"For a single continuous action use one concise action passage, without an extra [Shot 1] heading. "
|
||||
"Never emit timing placeholders such as MM:SS, HH:MM or 'end time', and never invent a timestamp. "
|
||||
"Leave dialogue empty when none was supplied. Do not add music unless requested. "
|
||||
"The user brief is the content authority. Existing context supplies only the unselected fields; previous drafts of selected fields are not instructions. "
|
||||
f"Mode rule: {mode_rules} "
|
||||
"Images provide visual evidence only for their assigned roles; do not claim unavailable references. "
|
||||
"Return field contents only, without headings, markdown, commentary or reasoning."
|
||||
)
|
||||
system += "\n" + _official_h3_tag_rules(mode)
|
||||
if len(system) > 24000:
|
||||
raise RuntimeError("MiniMax assistant system prompt exceeds the 7000-token safety envelope")
|
||||
user = json.dumps({
|
||||
@@ -974,6 +1025,15 @@ def _assistant_instruction(
|
||||
for item in visuals
|
||||
],
|
||||
}, ensure_ascii=False, indent=2)
|
||||
if request_text is not None:
|
||||
user = json.dumps({
|
||||
"task_mode": mode,
|
||||
"selected_sections": selected,
|
||||
"request": str(request_text).strip(),
|
||||
"direction": str(user_direction or "").strip(),
|
||||
"existing_context": {key: value for key, value in filled.items() if key not in selected},
|
||||
"visual_context": [{"slot": item["slot"], "role": item["role"], "name": item["name"]} for item in visuals],
|
||||
}, ensure_ascii=False)
|
||||
return system, user
|
||||
|
||||
|
||||
@@ -1263,7 +1323,7 @@ def _compact_assistant_instruction(task_mode: str, sections: dict[str, str], use
|
||||
"Return one JSON object only, without markdown. Every value must be one plain string. "
|
||||
f"Use only these keys: {selected}. Omit a key when the user supplied no relevant content. "
|
||||
"Preserve requested identity, action, camera direction, timestamps, language and quoted dialogue. "
|
||||
f"{special}"
|
||||
f"{special} " + _official_h3_tag_rules(mode)
|
||||
)
|
||||
user = json.dumps({
|
||||
"task_mode": mode,
|
||||
@@ -1316,6 +1376,7 @@ def rewrite_sections_with_ai(
|
||||
target_keys: Any = None,
|
||||
images: Any = None,
|
||||
vision_model: str = "",
|
||||
request_text: str | None = None,
|
||||
) -> tuple[dict[str, str], dict[str, Any]]:
|
||||
provider = str(provider or "ollama").strip().lower()
|
||||
if provider == "lm_studio":
|
||||
@@ -1343,7 +1404,7 @@ def rewrite_sections_with_ai(
|
||||
visual_inputs[0].update(data=base64.b64encode(buffer.getvalue()).decode("ascii"), mime_type="image/jpeg")
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
system, user = _assistant_instruction(task_mode, sections, user_direction, target_keys, visual_inputs)
|
||||
system, user = _assistant_instruction(task_mode, sections, user_direction, target_keys, visual_inputs, request_text)
|
||||
vision_observations = {}
|
||||
selected_vision_model = str(vision_model or "").strip()
|
||||
if _normalise_task_mode(task_mode) == "v2va_face_swap_lora" and visual_inputs and not selected_vision_model:
|
||||
@@ -1537,6 +1598,10 @@ def rewrite_sections_with_ai(
|
||||
filtered = {key: value for key, value in rewritten.items() if key in requested and value}
|
||||
if not filtered:
|
||||
raise RuntimeError("The AI did not return any valid filled MiniMax section")
|
||||
if request_text and len(requested) > 1:
|
||||
brief = re.sub(r"\W+", " ", request_text).strip().casefold()
|
||||
if all(re.sub(r"\W+", " ", value).strip().casefold() == brief for value in filtered.values()):
|
||||
raise RuntimeError("The AI echoed REQUEST instead of writing H3 fields. Your existing prompt was kept; retry with another writer model.")
|
||||
mode_name = _normalise_task_mode(task_mode)
|
||||
if mode_name == "v2va_face_swap_lora":
|
||||
text = str(filtered.get("swap_prompt") or "").strip()
|
||||
@@ -1589,16 +1654,17 @@ def rewrite_sections_with_ai(
|
||||
filtered["shot_list"] = canonical_timeline
|
||||
# An AI rewrite may paraphrase a Whisper transcript even when told not to.
|
||||
# Restore the exact H3 dialogue line supplied by the user after parsing.
|
||||
authored = "\n".join([str(user_direction or ""), *(str(value or "") for value in sections.values())])
|
||||
dialogue_lines = list(dict.fromkeys(re.findall(
|
||||
r"<Subject\s+\d+>\s*\(S\d+\):\s*<d>\[[^\]]+\][\s\S]*?</d>",
|
||||
authored,
|
||||
flags=re.IGNORECASE,
|
||||
)))
|
||||
authored = "\n".join([str(request_text or ""), str(user_direction or ""), *(str(value or "") for value in sections.values())])
|
||||
dialogue_lines = _authored_h3_dialogue_lines(authored)
|
||||
if mode_name == "fl2va" and requested.isdisjoint({"action", "shot_list", "dialogue"}):
|
||||
# Global keyframe fields remain stable authority; utterances belong to locals.
|
||||
dialogue_lines = []
|
||||
if dialogue_lines:
|
||||
target = "audio_dialogue_map" if "audio_dialogue_map" in requested else next(
|
||||
(key for key in target_keys if key in requested), next(iter(requested))
|
||||
) if isinstance(target_keys, list) else next(iter(requested))
|
||||
if target in {"dialogue", "audio_dialogue_map", "multishot_dialogue_map"}:
|
||||
filtered[target] = "\n".join(dialogue_lines)
|
||||
for line in dialogue_lines:
|
||||
if line not in "\n".join(filtered.values()):
|
||||
filtered[target] = "\n".join(part for part in (filtered.get(target, ""), line) if part)
|
||||
@@ -1720,7 +1786,7 @@ def build_visual_story_plan_with_ai(provider: str, base_url: str, model: str, ap
|
||||
raise ValueError("Describe the relationship between the images")
|
||||
visual_map = "\n".join(f"Picture {item['slot']}: {item['name']} · role={item['role']}" for item in visuals)
|
||||
payload = _multimodal_json_chat(provider, base_url, model, api_key,
|
||||
VISUAL_STORY_SYSTEM_PROMPT + ("" if visuals else
|
||||
VISUAL_STORY_SYSTEM_PROMPT + "\n" + _official_h3_tag_rules(_normalise_task_mode(task_mode)) + ("" if visuals else
|
||||
"\nNo images supplied. Do not invent Picture references. Create one local shot per explicitly numbered prompt in the brief. Preserve those slot numbers and their individual instructions; do not collapse them into the global prompt. If no slots are numbered, choose a short consecutive shot sequence. Global defines shared identity, location and atmosphere; local prompts carry the chronological development."),
|
||||
f"CURRENT IAMCCS MODE: {task_mode}\nVISUAL MAP:\n{visual_map}\n\nRELATIONSHIP / STORY INTENT:\n{relation}",
|
||||
visuals, temperature, timeout,
|
||||
@@ -1766,11 +1832,10 @@ def build_visual_story_plan_with_ai(provider: str, base_url: str, model: str, ap
|
||||
) if part)
|
||||
# REQUEST → GLOBAL + LOCALS has a separate AI endpoint. Keep verbatim
|
||||
# Whisper dialogue here too, even when the model paraphrases its output.
|
||||
for line in dict.fromkeys(re.findall(
|
||||
r"<Subject\s+\d+>\s*\(S\d+\):\s*<d>\[[^\]]+\][\s\S]*?</d>",
|
||||
relation, flags=re.IGNORECASE,
|
||||
)):
|
||||
for line in _authored_h3_dialogue_lines(relation):
|
||||
if line not in global_prompt and not any(line in shot["local_prompt"] for shot in shots):
|
||||
if _normalise_task_mode(task_mode) == "fl2va":
|
||||
raise RuntimeError("The H3 plan lost an authored dialogue line from its locals. The brief was preserved; retry the local plan.")
|
||||
global_prompt = "\n\n".join(part for part in (global_prompt, line) if part)
|
||||
return {"global_prompt": global_prompt,
|
||||
"global_direction": global_direction,
|
||||
@@ -2754,6 +2819,7 @@ def _register_prompter_routes() -> None:
|
||||
payload.get("target_keys"),
|
||||
payload.get("images"),
|
||||
str(payload.get("vision_model", "")),
|
||||
payload.get("request_text"),
|
||||
)
|
||||
return web.json_response({"ok": True, "sections": rewritten, "report": report})
|
||||
except Exception as exc:
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Retired Repair compatibility and read-only diagnostics; no inference."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import torch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT.parents[1]))
|
||||
spec = importlib.util.spec_from_file_location("guard_test_module", ROOT / "iamccs_h3_continuity_guard.py")
|
||||
guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(guard)
|
||||
|
||||
|
||||
def test_legacy_repair_disables_all_interventions(tmp_path):
|
||||
config = guard.configuration({"h3_guard_mode": "repair", "h3_guard_reference_memory": True})
|
||||
assert config['mode'] == 'off'
|
||||
samples, frames = object(), object()
|
||||
result = guard.process({'mode': 'repair'}, samples, frames, directory=tmp_path,
|
||||
index=1, head_frames=0, visible_frames=1)
|
||||
assert result[0] is samples and result[1] is frames
|
||||
assert result[2]['status'] == 'off'
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
def test_monitor_preserves_video_audio_mask_and_frames(tmp_path):
|
||||
from unittest.mock import patch
|
||||
samples = {'samples': object(), 'noise_mask': object(), 'metadata': {'seed': 42}}
|
||||
frames = torch.rand(8, 12, 12, 3)
|
||||
before = frames.clone()
|
||||
config = guard.configuration({'h3_guard_mode': 'monitor'})
|
||||
with patch.object(guard, 'measure', return_value={'detail': .1, 'luminance': .5, 'motion': .1}):
|
||||
for index in (0, 1):
|
||||
result, returned, report = guard.process(config, samples, frames,
|
||||
directory=tmp_path, index=index, head_frames=2, visible_frames=6)
|
||||
for key, value in samples.items():
|
||||
assert result[key] is value
|
||||
assert returned is frames and torch.equal(frames, before)
|
||||
assert report['repair_attempts'] == 0
|
||||
|
||||
|
||||
def test_darkening_or_motion_change_requires_review_not_automatic_repair():
|
||||
config = guard.configuration({})
|
||||
baseline = {'detail': .1, 'luminance': .5, 'motion': .1}
|
||||
assert guard.assess({'detail': .04, 'luminance': .5, 'motion': .1}, baseline, config)['texture_loss_detected']
|
||||
for changed in ({'detail': .04, 'luminance': .2, 'motion': .1}, {'detail': .04, 'luminance': .5, 'motion': .005}):
|
||||
verdict = guard.assess(changed, baseline, config)
|
||||
assert verdict['review_recommended'] and not verdict['texture_loss_detected']
|
||||
|
||||
|
||||
def test_metrics_detect_synthetic_blur():
|
||||
import torch.nn.functional as F
|
||||
torch.manual_seed(5)
|
||||
texture = torch.rand(1, 3, 144, 256)
|
||||
movie = torch.cat([torch.roll(texture, i, -1) for i in range(8)]).movedim(1, -1)
|
||||
blurred = F.avg_pool2d(movie.movedim(-1, 1), 7, stride=1, padding=3).movedim(1, -1)
|
||||
a, b = guard.measure(movie), guard.measure(blurred)
|
||||
assert b['detail'] < a['detail'] * .65
|
||||
|
||||
|
||||
def test_storage_does_not_use_render_id_as_path(tmp_path):
|
||||
path = guard.storage(tmp_path, '../../outside')
|
||||
assert path.is_relative_to(tmp_path)
|
||||
|
||||
|
||||
def test_invalid_configuration_is_rejected():
|
||||
import unittest
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
guard.configuration({'h3_guard_detail_ratio': float('nan')})
|
||||
|
||||
|
||||
def test_blank_root_identity_binds_to_numbered_checkpoint_and_continuation(tmp_path):
|
||||
import json
|
||||
provisional = guard.conditioning_identity('', 0)
|
||||
source = guard.storage(tmp_path, provisional)
|
||||
baseline = {'detail': .1, 'motion': .1, 'luminance': .5}
|
||||
guard.write_report(source / 'baseline.json', baseline)
|
||||
report = {'storage_key': source.name, 'segment_index': 0}
|
||||
guard.write_report(source / 'segment_0000.json', report)
|
||||
guard.bind_checkpoint(tmp_path, report, 'soldier_0042')
|
||||
continuation_id = guard.conditioning_identity('soldier_0042', 1)
|
||||
destination = guard.storage(tmp_path, continuation_id)
|
||||
assert json.loads((destination / 'baseline.json').read_text()) == baseline
|
||||
guard.bind_checkpoint(tmp_path, {'storage_key': destination.name, 'segment_index': 1}, continuation_id)
|
||||
assert (source / 'baseline.json').is_file()
|
||||
assert guard.conditioning_identity('', 0) != provisional
|
||||
|
||||
|
||||
def test_missing_continuation_identity_and_invalid_binding_key_fail_closed(tmp_path):
|
||||
import unittest
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
guard.conditioning_identity('', 1)
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
guard.bind_checkpoint(tmp_path, {'storage_key': '../../outside', 'segment_index': 0}, 'safe')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import tempfile
|
||||
import inspect
|
||||
tests = [(name, value) for name, value in globals().copy().items() if name.startswith('test_')]
|
||||
for name, test in tests:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
test(Path(directory)) if inspect.signature(test).parameters else test()
|
||||
print('PASS', name)
|
||||
@@ -0,0 +1,57 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const source = fs.readFileSync(process.argv[2] || path.join(__dirname, '..', 'web', 'iamccs_prompter_ui.js'), 'utf8');
|
||||
const start = source.indexOf(' const performAIRewrite = async');
|
||||
const end = source.indexOf(' const renderLocalPrompts = () => {', start);
|
||||
assert(start > 0 && end > start);
|
||||
|
||||
function fixture() {
|
||||
const state = {request: 'Una donna posa una tazza.', task_mode: 't2va', sections: {scene: 'Old scene', camera: 'Old camera', acting: 'Tired hands'}};
|
||||
const context = {
|
||||
project: state, node: {}, MODE_META: {t2va: {sections: [['scene'], ['camera'], ['acting']]}},
|
||||
activePromptKey: null, activePromptArea: null, rewriteBtn: {}, aiScope: {value: 'active_field'},
|
||||
aiDirection: {value: ''}, aiStatus: {}, aiProvider: {value: 'qwen_native'}, aiBaseUrl: {value: ''},
|
||||
aiModel: {value: '__workflow__'}, aiApiKey: {value: ''}, aiVisionModel: {value: ''},
|
||||
aiTemperature: {value: '0.35'}, aiVisualFiles: [], isEditMode: () => false,
|
||||
swapcharSignature: () => '', persistAI() {}, commit() {}, beginAiBusy: () => () => {},
|
||||
buildAIImagePayload: async () => [], nativeClipConfig: () => ({profile: '4b_v3.1'}),
|
||||
snapshotProject() {}, renderControls() {}, renderSections() {}, renderPreview() {},
|
||||
workspace: {showReview() {context.reviewCount = (context.reviewCount || 0) + 1;}},
|
||||
fetchAI: async (_url, options) => {
|
||||
context.sent = JSON.parse(options.body);
|
||||
return {ok: true, json: async () => ({ok: true, sections: {scene: 'A woman lowers a cup onto the table.', camera: 'Fixed eye-level framing.'}})};
|
||||
},
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(source.slice(start, end) + '\nglobalThis.rewrite = performAIRewrite;', context);
|
||||
return context;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const request = fixture();
|
||||
assert.equal(await request.rewrite({narrativeRequest: request.project.request, directTargetKeys: ['scene', 'camera']}), true);
|
||||
assert.equal(request.sent.request_text, 'Una donna posa una tazza.');
|
||||
assert.deepEqual(request.sent.sections, {scene: '', camera: '', acting: 'Tired hands'});
|
||||
assert.equal(request.project.sections.scene, 'A woman lowers a cup onto the table.');
|
||||
assert.equal(request.project.sections.acting, 'Tired hands');
|
||||
assert.equal(request.project.request, 'Una donna posa una tazza.');
|
||||
assert.equal(request.reviewCount, 1, 'REQUEST must reveal the compiled H3 prompt');
|
||||
|
||||
const field = fixture();
|
||||
assert.equal(await field.rewrite({directTargetKeys: ['scene', 'camera']}), true);
|
||||
assert.equal(field.sent.request_text, null);
|
||||
assert.equal(field.sent.sections.scene, 'Old scene');
|
||||
assert.equal(field.reviewCount, undefined, 'a field rewrite keeps the current view');
|
||||
|
||||
const edited = fixture();
|
||||
edited.fetchAI = async () => {
|
||||
edited.project.sections.scene = 'User edit while waiting';
|
||||
return {ok: true, json: async () => ({ok: true, sections: {scene: 'Late AI result'}})};
|
||||
};
|
||||
await edited.rewrite({narrativeRequest: edited.project.request, directTargetKeys: ['scene', 'camera']});
|
||||
assert.equal(edited.project.sections.scene, 'User edit while waiting');
|
||||
assert.match(edited.aiStatus.textContent, /Your edits were kept/);
|
||||
console.log('PASS: REQUEST compiles from its brief, field rewrite retains context, late AI cannot overwrite edits');
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
@@ -37,5 +37,49 @@ class ScoutDeliveryRouterTests(unittest.TestCase):
|
||||
self.assertEqual((prefix, reason), ("original", "Scout disabled"))
|
||||
|
||||
|
||||
class ScoutBypassTests(unittest.TestCase):
|
||||
def plan(self, enabled=False, count=4, mode="t2va"):
|
||||
return {"task_mode":mode,"seed_scout_settings":{"enabled":enabled,"candidate_count":count}}
|
||||
|
||||
def test_disabled_preview_does_not_request_any_candidate_dependencies(self):
|
||||
candidate=MODULE.IAMCCS_MiniMaxH3SeedCandidateR40()
|
||||
with patch.object(MODULE,"_resolve_shotplan",return_value=self.plan()), patch.object(MODULE,"_release_conditioning_models",side_effect=AssertionError("Unexpected model cleanup")), patch.object(MODULE,"_active_sampling_model",side_effect=AssertionError("Unexpected sampling")):
|
||||
for index in range(4):
|
||||
self.assertEqual(candidate.check_lazy_status({},index),[])
|
||||
result=candidate.sample(cine_linx={},candidate_index=index)
|
||||
self.assertIsInstance(result[0],MODULE.ExecutionBlocker)
|
||||
self.assertIsInstance(result[1],MODULE.ExecutionBlocker)
|
||||
self.assertFalse(result[3])
|
||||
|
||||
def test_enabled_candidate_requests_its_heavy_inputs(self):
|
||||
candidate=MODULE.IAMCCS_MiniMaxH3SeedCandidateR40()
|
||||
with patch.object(MODULE,"_resolve_shotplan",return_value=self.plan(True,2)):
|
||||
self.assertEqual(candidate.check_lazy_status({},0),["model","positive","latent","chunk_index"])
|
||||
self.assertEqual(candidate.check_lazy_status({},1,model=object(),chunk_index=0),["positive","latent"])
|
||||
self.assertEqual(candidate.check_lazy_status({},2),[])
|
||||
self.assertIsInstance(candidate.sample(cine_linx={},candidate_index=3)[0],MODULE.ExecutionBlocker)
|
||||
|
||||
def test_temporal_route_bypasses_even_when_scout_requested(self):
|
||||
candidate=MODULE.IAMCCS_MiniMaxH3SeedCandidateR40()
|
||||
with patch.object(MODULE,"_resolve_shotplan",return_value=self.plan(True,4,"latent_go_ahead")):
|
||||
self.assertEqual(candidate.check_lazy_status({},0),[])
|
||||
self.assertFalse(candidate.sample(cine_linx={})[3])
|
||||
|
||||
def test_inactive_preview_never_requests_vae_or_allocates_an_image(self):
|
||||
preview=MODULE.IAMCCS_MiniMaxH3SeedPreviewR40()
|
||||
latent={"iamccs_r40_candidate_active":False}
|
||||
self.assertEqual(preview.check_lazy_status(latent),[])
|
||||
self.assertIsInstance(preview.decode(latent)[0],MODULE.ExecutionBlocker)
|
||||
self.assertEqual(preview.check_lazy_status({"iamccs_r40_candidate_active":True}),["video_vae","trim_frames"])
|
||||
|
||||
def test_existing_socket_names_are_preserved_and_heavy_inputs_are_lazy(self):
|
||||
candidate=MODULE.IAMCCS_MiniMaxH3SeedCandidateR40.INPUT_TYPES()["required"]
|
||||
for name in ("model","positive","latent","chunk_index"):
|
||||
self.assertTrue(candidate[name][1]["lazy"])
|
||||
preview=MODULE.IAMCCS_MiniMaxH3SeedPreviewR40.INPUT_TYPES()["required"]
|
||||
self.assertTrue(preview["video_vae"][1]["lazy"])
|
||||
self.assertTrue(preview["trim_frames"][1]["lazy"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Exercise the real adapter loader's segment gate without GPU inference."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def backend(monkeypatch):
|
||||
package = ModuleType("secondary_scope_test")
|
||||
package.__path__ = []
|
||||
swap = ModuleType(package.__name__ + ".iamccs_h3_character_swap_lora")
|
||||
swap.apply_loras = lambda model, plan: model
|
||||
monkeypatch.setitem(sys.modules, package.__name__, package)
|
||||
monkeypatch.setitem(sys.modules, swap.__name__, swap)
|
||||
loader = Mock(side_effect=lambda **kw: (object(),))
|
||||
monkeypatch.setitem(sys.modules, "nodes", SimpleNamespace(
|
||||
LoraLoaderModelOnly=lambda: SimpleNamespace(load_lora_model_only=loader)))
|
||||
names = {"_external_continuation_active", "_secondary_lora_settings",
|
||||
"_secondary_lora_applies_to_segment", "_apply_secondary_lora"}
|
||||
tree = ast.parse((ROOT / "iamccs_minimax_h3_atomic_backend.py").read_text(encoding="utf-8"))
|
||||
functions = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in names]
|
||||
ns = {"__package__": package.__name__, "Any": Any,
|
||||
"folder_paths": SimpleNamespace(get_full_path=lambda *args: "registered-adapter"),
|
||||
"_h3_lora_compatibility": lambda name: (True, "compatible")}
|
||||
exec(compile(ast.Module(body=functions, type_ignores=[]), "<secondary-scope>", "exec"), ns)
|
||||
return ns["_apply_secondary_lora"], loader
|
||||
|
||||
|
||||
@pytest.mark.parametrize("segment", [0, 1, 3])
|
||||
def test_legacy_all_segments_stays_enabled(monkeypatch, segment):
|
||||
apply, loader = backend(monkeypatch)
|
||||
original = object()
|
||||
plan = {"secondary_lora": {"enabled": True, "lora_name": "test.safetensors", "strength": .5}}
|
||||
result, report = apply(original, plan, chunk_index=segment)
|
||||
assert result is not original
|
||||
assert loader.call_count == 1
|
||||
assert "all-segments" in report
|
||||
|
||||
|
||||
def test_first_segment_skips_then_continuations_apply_without_mutating_plan(monkeypatch):
|
||||
apply, loader = backend(monkeypatch)
|
||||
original = object()
|
||||
settings = {"enabled": True, "continuation_only": True, "lora_name": "test.safetensors", "strength": .5}
|
||||
plan = {"secondary_lora": settings.copy()}
|
||||
result, report = apply(original, plan, chunk_index=0)
|
||||
assert result is original and loader.call_count == 0
|
||||
assert "starts at segment 2" in report
|
||||
for segment in (1, 2):
|
||||
result, report = apply(original, plan, chunk_index=segment)
|
||||
assert result is not original
|
||||
assert "continuation-only" in report
|
||||
assert loader.call_count == 2
|
||||
assert plan["secondary_lora"] == settings
|
||||
# Returning to the first segment must not inherit the patched model.
|
||||
assert apply(original, plan, chunk_index=0)[0] is original
|
||||
assert loader.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpoint,enabled,expected", [("native.pt", True, 1), ("", True, 0), ("native.pt", False, 0)])
|
||||
def test_checkpoint_resume_counts_as_continuation(monkeypatch, checkpoint, enabled, expected):
|
||||
apply, loader = backend(monkeypatch)
|
||||
plan = {"secondary_lora": {"enabled": True, "continuation_only": True,
|
||||
"lora_name": "test.safetensors", "strength": .5},
|
||||
"continuation_settings": {"enabled": enabled, "checkpoint": checkpoint}}
|
||||
apply(object(), plan, chunk_index=0)
|
||||
assert loader.call_count == expected
|
||||
|
||||
|
||||
def test_disabled_slot_never_loads_on_continuation(monkeypatch):
|
||||
apply, loader = backend(monkeypatch)
|
||||
apply(object(), {"secondary_lora": {"enabled": False, "continuation_only": True}}, chunk_index=2)
|
||||
assert loader.call_count == 0
|
||||
|
||||
|
||||
def test_settings_schema_is_append_only():
|
||||
tree = ast.parse((ROOT / "iamccs_minimax_h3_shotboard.py").read_text(encoding="utf-8"))
|
||||
settings = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "IAMCCS_ShotboardH3Settings")
|
||||
method = next(n for n in settings.body if isinstance(n, ast.FunctionDef) and n.name == "INPUT_TYPES")
|
||||
keys = [n.value for n in ast.walk(method) if isinstance(n, ast.Constant) and n.value == "secondary_lora_continuation_only"]
|
||||
assert len(keys) == 1
|
||||
# New widget remains after the final historical extension.
|
||||
source = ast.get_source_segment((ROOT / "iamccs_minimax_h3_shotboard.py").read_text(encoding="utf-8"), method)
|
||||
assert source.index('"secondary_lora_continuation_only"') > source.index("**continuity_guard_schema()")
|
||||
@@ -0,0 +1,60 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT.parents[1]))
|
||||
PACKAGE = "_open_h3_test"
|
||||
package = types.ModuleType(PACKAGE)
|
||||
package.__path__ = [str(ROOT)]
|
||||
sys.modules[PACKAGE] = package
|
||||
spec = importlib.util.spec_from_file_location(PACKAGE + ".iamccs_minimax_h3_open_refine", ROOT / "iamccs_minimax_h3_open_refine.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
atomic = types.ModuleType(PACKAGE + ".iamccs_minimax_h3_atomic_backend")
|
||||
atomic._resolve_shotplan = lambda value: value
|
||||
|
||||
|
||||
class OpenH3SettingsTests(unittest.TestCase):
|
||||
def resolve(self, settings=None, **plan):
|
||||
payload = {"open_h3_refine": module.configuration(settings or {}), **plan}
|
||||
with patch.dict(sys.modules, {atomic.__name__: atomic}):
|
||||
return module.IAMCCS_MiniMaxH3OpenRefineControl().resolve(payload, 1)
|
||||
|
||||
def test_defaults_keep_existing_workflows_disabled(self):
|
||||
self.assertFalse(self.resolve()[0])
|
||||
|
||||
def test_open_refine_does_not_stack_another_upscale(self):
|
||||
with self.assertRaisesRegex(ValueError, "replaces the other upscale"):
|
||||
self.resolve({"h3_open_refine_enabled": True}, upscale_enabled=True, upscale_mode="h3_ultimate_tiled")
|
||||
|
||||
def test_missing_assets_fail_explicitly(self):
|
||||
with self.assertRaisesRegex(ValueError, "refiner LoRA"):
|
||||
self.resolve({"h3_open_refine_enabled": True, "h3_open_refine_lora_name": ""})
|
||||
|
||||
def test_custom_schedule_prompt_and_segment_seed(self):
|
||||
result = self.resolve({"h3_open_refine_sigma_preset": "custom", "h3_open_refine_custom_sigmas": "0.5,0.2,0", "h3_open_refine_seed": 100, "h3_open_refine_prompt": "Preserve the yawn."}, global_prompt="Other prompt")
|
||||
self.assertEqual((result[1], result[11], result[12]), ("Preserve the yawn.", "0.5,0.2,0", 101))
|
||||
|
||||
def test_blank_distill_is_disabled_and_shot_prompt_wins(self):
|
||||
result = self.resolve({"h3_open_refine_distill_lora_name": ""}, global_prompt="Global", chunks=[{}, {"prompt": "Local shot"}])
|
||||
self.assertEqual(result[1], "Local shot")
|
||||
self.assertEqual(result[10], 0.0)
|
||||
|
||||
def test_reference_schedule_and_output_contract(self):
|
||||
result = self.resolve({"h3_open_refine_sigma_preset": "reference_3step"})
|
||||
self.assertEqual(result[11], module.SIGMAS["reference_3step"])
|
||||
self.assertEqual(len(result), len(module.IAMCCS_MiniMaxH3OpenRefineControl.RETURN_TYPES))
|
||||
self.assertEqual(len(result), len(module.IAMCCS_MiniMaxH3OpenRefineControl.RETURN_NAMES))
|
||||
|
||||
def test_optional_face_source_is_retained_for_native_shots(self):
|
||||
self.assertTrue(self.resolve(face_detailer_settings={"enabled": True}, task_mode="ref2va")[13])
|
||||
self.assertFalse(self.resolve(face_detailer_settings={"enabled": True}, task_mode="longvid_keyframe_continuous")[13])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,32 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const assert = require('node:assert/strict');
|
||||
const source = fs.readFileSync(path.join(__dirname, '../web/iamccs_h3_settings_pro_ui.js'), 'utf8');
|
||||
const values = {upscale_enabled:true,upscale_mode:'h3_ultimate_tiled',h3_open_refine_enabled:false,h3_open_refine_sigma_preset:'reference_3step',rife_mode:'off',h3_omni_enabled:true,steps:8,seed:42};
|
||||
const node = {id:811, properties:{}, widgets:Object.entries(values).map(([name,value])=>({name,value}))};
|
||||
const widget = (n,key)=>n.widgets.find(w=>w.name===key);
|
||||
let connected = ['IAMCCS_MiniMaxH3OpenRefineControl'];
|
||||
const context = vm.createContext({node,widget,setValue:(n,key,value)=>{const w=widget(n,key);if(w)w.value=value;},connectedNodes:()=>connected.map(type=>({type})),nodeClass:n=>n.type,shotboardMode:()=> 'ref2va',modeFamily:()=> 'ref2va',document:{dispatchEvent(){}},CustomEvent:class{}});
|
||||
const constants = source.slice(source.indexOf('const NODE_TYPE ='), source.indexOf('const widget ='));
|
||||
const gate = source.slice(source.indexOf('function branchGate('), source.indexOf('function modeAvailability('));
|
||||
const recipes = source.slice(source.indexOf('function applyRecipe('), source.indexOf('const FACE_REFINE_PRESET_HELP'));
|
||||
vm.runInContext(constants + gate + recipes + 'globalThis.apply=applyRecipe;globalThis.layout=FUNCTIONAL_LAYOUT;globalThis.groups=GROUPS;',context);
|
||||
assert.equal(context.apply(node,'open-h3-refine'),true);
|
||||
assert.equal(widget(node,'h3_open_refine_enabled').value,true);
|
||||
assert.equal(widget(node,'upscale_enabled').value,false);
|
||||
assert.equal(widget(node,'upscale_mode').value,'off');
|
||||
assert.equal(widget(node,'h3_open_refine_sigma_preset').value,'gentle_3step');
|
||||
assert.equal(widget(node,'h3_omni_enabled').value,true);
|
||||
assert.equal(widget(node,'steps').value,8);
|
||||
assert.equal(widget(node,'seed').value,42);
|
||||
context.apply(node,'native-delivery');
|
||||
assert.equal(widget(node,'h3_open_refine_enabled').value,false);
|
||||
connected=[];
|
||||
assert.equal(context.apply(node,'open-h3-refine'),false);
|
||||
assert.equal(widget(node,'h3_open_refine_enabled').value,false);
|
||||
const section=context.layout.finish.find(([title])=>title.startsWith('OPEN-H3 REFINEMENT'));
|
||||
assert.equal(section[1].length,14);
|
||||
assert.ok(context.groups.find(g=>g.id==='finish').fields.includes('h3_open_refine_seed'));
|
||||
assert.match(source,/addRecipe\(delivery, "OPEN-H3 REFINEMENT/);
|
||||
console.log('PASS: Mode Assistant selection, separate Output box, lazy-branch gate and one-native-generation settings');
|
||||
@@ -0,0 +1,25 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const assert = require('node:assert/strict');
|
||||
const source = fs.readFileSync(path.join(__dirname, '../web/iamccs_h3_settings_pro_ui.js'), 'utf8');
|
||||
const start = source.indexOf('function openH3ResolutionEstimate(');
|
||||
const end = source.indexOf('function openH3ResolutionSummary(', start);
|
||||
const context = vm.createContext({widget: (node, name) => node.widgets.find(item => item.name === name)});
|
||||
vm.runInContext(source.slice(start, end) + '\nglobalThis.estimate = openH3ResolutionEstimate;', context);
|
||||
const node = {widgets: Object.entries({width: 640, height: 384, h3_open_refine_scale: 2, h3_open_refine_enabled: true}).map(([name, value]) => ({name, value}))};
|
||||
const set = (name, value) => {node.widgets.find(item => item.name === name).value = value;};
|
||||
let result = context.estimate(node);
|
||||
assert.deepEqual([result.targetWidth, result.targetHeight, result.pixelGain], [1280, 768, 4]);
|
||||
set('width', 1024); set('height', 576); set('h3_open_refine_scale', 2.5);
|
||||
result = context.estimate(node);
|
||||
assert.deepEqual([result.targetWidth, result.targetHeight, result.pixelGain], [2560, 1440, 6.25]);
|
||||
set('width', 736); set('height', 416); set('h3_open_refine_scale', 1.5);
|
||||
result = context.estimate(node);
|
||||
assert.deepEqual([result.targetWidth, result.targetHeight], [1088, 640], 'match Python half-to-even, not Math.round');
|
||||
set('h3_open_refine_enabled', false);
|
||||
assert.equal(context.estimate(node).enabled, false);
|
||||
set('width', 0);
|
||||
assert.equal(context.estimate(node), null);
|
||||
assert.match(source, /section\.append\(openH3ResolutionSummary\(node\)\)/);
|
||||
console.log('PASS: native/target sizes, pixel gain, QHD, Python grid rounding and bypass state');
|
||||
@@ -0,0 +1,52 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from test_prompter_native_qwen import prompter
|
||||
|
||||
|
||||
class RequestCompilationTests(unittest.TestCase):
|
||||
def test_request_is_authority_and_selected_drafts_are_not_context(self):
|
||||
sections = {"scene": "Previous railway scene", "camera": "Old camera", "acting": "Keep her tired performance"}
|
||||
system, user = prompter._assistant_instruction(
|
||||
"t2va", sections, "Develop the requested shot", ["scene", "camera"],
|
||||
request_text="Una donna posa una tazza sul tavolo.",
|
||||
)
|
||||
message = json.loads(user)
|
||||
self.assertEqual(message["request"], "Una donna posa una tazza sul tavolo.")
|
||||
self.assertEqual(message["existing_context"], {"acting": "Keep her tired performance"})
|
||||
self.assertNotIn("Previous railway scene", user)
|
||||
self.assertIn("in English", system)
|
||||
self.assertIn("Never emit timing placeholders", system)
|
||||
self.assertEqual(sections["scene"], "Previous railway scene")
|
||||
|
||||
def test_request_returns_generated_fields(self):
|
||||
generated = {"scene": "A woman approaches a table holding a cup.", "camera": "A fixed eye-level composition holds the table in view."}
|
||||
with patch.object(prompter, "native_json_chat", return_value=generated) as writer:
|
||||
fields, report = prompter.rewrite_sections_with_ai(
|
||||
"qwen_native", "", "__workflow__", "", "t2va", {"scene": "", "camera": ""},
|
||||
target_keys=["scene", "camera"], request_text="Una donna posa una tazza sul tavolo.",
|
||||
)
|
||||
self.assertEqual(fields, generated)
|
||||
self.assertEqual(report["rewritten_sections"], ["camera", "scene"])
|
||||
self.assertIn('"request":', writer.call_args.args[2])
|
||||
|
||||
def test_request_echo_is_not_reported_as_success(self):
|
||||
brief = "Una donna posa una tazza sul tavolo."
|
||||
with patch.object(prompter, "native_json_chat", return_value={"scene": brief, "camera": brief}):
|
||||
with self.assertRaisesRegex(RuntimeError, "echoed REQUEST"):
|
||||
prompter.rewrite_sections_with_ai(
|
||||
"qwen_native", "", "__workflow__", "", "t2va", {"scene": "", "camera": ""},
|
||||
target_keys=["scene", "camera"], request_text=brief,
|
||||
)
|
||||
|
||||
def test_single_field_rewrite_keeps_its_original_contract(self):
|
||||
_, user = prompter._assistant_instruction("t2va", {"scene": "A quiet room"}, target_keys=["scene"])
|
||||
self.assertEqual(json.loads(user)["rough_sections"], {"scene": "A quiet room"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,77 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from test_prompter_native_qwen import prompter
|
||||
|
||||
|
||||
class OfficialTagAuthoringTests(unittest.TestCase):
|
||||
def test_global_request_creates_speech_tags_even_when_input_has_none(self):
|
||||
system, user = prompter._assistant_instruction(
|
||||
't2va', {}, 'Compile Global', ['scene', 'dialogue'],
|
||||
request_text='Una donna dice: Aspettami qui.',
|
||||
)
|
||||
self.assertIn('even when the rough request has no tags', system)
|
||||
self.assertIn('<d>[Language] exact words</d>', system)
|
||||
self.assertIn('independently of subject numbering', system)
|
||||
self.assertIn('<scenetrans>', system)
|
||||
self.assertIn('<cutoff>', system)
|
||||
self.assertIn('Global describes output scope', system)
|
||||
self.assertEqual(json.loads(user)['request'], 'Una donna dice: Aspettami qui.')
|
||||
|
||||
def test_reference_grammar_distinguishes_assets_content_and_speakers(self):
|
||||
keys=[key for key,_ in prompter.MODE_SECTIONS['ref2va']]
|
||||
system,_=prompter._assistant_instruction('ref2va',{},'',keys,request_text='The second character speaks.')
|
||||
for tag in ['<Subject N>', '<Picture N>', '<Video N>', '<Audio N>']:
|
||||
self.assertIn(tag,system)
|
||||
self.assertIn('Do not populate unused slots',system)
|
||||
self.assertIn('full-reference modes keep their six sections',system)
|
||||
|
||||
def test_compact_retry_retains_tag_rules(self):
|
||||
system,_=prompter._compact_assistant_instruction('i2va',{},'She says hello.',['dialogue'])
|
||||
self.assertIn('<d>[Language] exact words</d>',system)
|
||||
|
||||
def test_specialized_edit_grammars_are_not_overwritten(self):
|
||||
for mode in ['v2va_face_swap_lora','ref2va_vfx_edit']:
|
||||
self.assertIn('native aligned-edit grammar',prompter._official_h3_tag_rules(mode))
|
||||
self.assertNotIn('Format every supplied',prompter._official_h3_tag_rules(mode))
|
||||
|
||||
def test_reference_output_preserves_independent_subject_and_speaker_ids(self):
|
||||
line='<Subject 2> (S1) says <d>[Italian] Aspettami qui.</d>'
|
||||
payload={'subject_definitions':'<Subject 2> is the woman in <Picture 1>.','detailed_description':'[Shot 1] '+line}
|
||||
reply={'choices':[{'message':{'content':json.dumps(payload)}}]}
|
||||
with patch.object(prompter,'_http_json',return_value=reply):
|
||||
fields,_=prompter.rewrite_sections_with_ai('lm_studio','','local','','ref2va',{},target_keys=list(payload),request_text='Una donna dice: Aspettami qui.')
|
||||
self.assertIn(line,fields['detailed_description'])
|
||||
self.assertNotIn('<Subject 1>',fields['detailed_description'])
|
||||
|
||||
def test_global_and_locals_endpoint_uses_the_same_tag_contract(self):
|
||||
payload={'global_prompt':'A quiet room.','shots':[{'slot':1,'local_prompt':'(S1) says <d>[Italian] Resta.</d>'}]}
|
||||
with patch.object(prompter,'_multimodal_json_chat',return_value=payload) as chat:
|
||||
plan,_=prompter.build_visual_story_plan_with_ai('lm_studio','','local','','Prompt 1: una donna dice Resta.','t2va',[])
|
||||
self.assertIn('even when the rough request has no tags',chat.call_args.args[4])
|
||||
self.assertIn('<d>[Italian] Resta.</d>',plan['shots'][0]['local_prompt'])
|
||||
|
||||
def test_request_utterance_with_says_and_cutoff_survives_a_bad_rewrite(self):
|
||||
line='<Subject 2> (S1) says <d>[Italian] Non andare!</d><cutoff>'
|
||||
reply={'choices':[{'message':{'content':json.dumps({'dialogue':'The woman says go.'})}}]}
|
||||
with patch.object(prompter,'_http_json',return_value=reply):
|
||||
fields,_=prompter.rewrite_sections_with_ai('lm_studio','','local','','t2va',{},target_keys=['dialogue'],request_text=line)
|
||||
self.assertIn(line,fields['dialogue'])
|
||||
self.assertNotIn('The woman says go.',fields['dialogue'])
|
||||
|
||||
def test_standalone_voice_and_cross_cut_markers_are_preserved(self):
|
||||
line='(S2) speaks off-screen <d>[English] Stay here.</d><scenetrans>'
|
||||
self.assertEqual(prompter._authored_h3_dialogue_lines(line),[line])
|
||||
|
||||
def test_keyframe_global_does_not_absorb_timed_dialogue_from_request(self):
|
||||
line='(S1) says <d>[Italian] Resta.</d>'
|
||||
reply={'choices':[{'message':{'content':json.dumps({'reference_use':'Keep the same opening frame.'})}}]}
|
||||
with patch.object(prompter,'_http_json',return_value=reply):
|
||||
fields,_=prompter.rewrite_sections_with_ai('lm_studio','','local','','fl2va',{},target_keys=['reference_use'],request_text=line)
|
||||
self.assertNotIn('<d>',fields['reference_use'])
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -103,8 +103,6 @@ export function createPrompterWorkspace({ root, layout, left, center, right, mod
|
||||
buttons.set(key, button);
|
||||
}
|
||||
function refresh() {
|
||||
const request = center.querySelector('[data-workspace="direction"]');
|
||||
if (request && center.firstElementChild !== request) center.prepend(request);
|
||||
const hasShots = Boolean(center.querySelector('[data-workspace="shots"]'));
|
||||
buttons.get("shots").disabled = !hasShots;
|
||||
if (selected === "shots" && !hasShots) selected = "direction";
|
||||
@@ -122,5 +120,5 @@ export function createPrompterWorkspace({ root, layout, left, center, right, mod
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
return { refresh };
|
||||
return { refresh, showReview() { selected = "review"; refresh(); } };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const NODE_TYPE = "IAMCCS_ShotboardH3SettingsPro";
|
||||
const SHOTBOARD_TYPE = "IAMCCS_MiniMaxH3ShotPlanner";
|
||||
const BRIDGE_TYPES = new Set(["IAMCCS_CineH3Input", "IAMCCS_CineH3FunControlInput", "IAMCCS_MiniMaxH3FunControlInput"]);
|
||||
const SHOTBOARD_OWNED = new Set(["task_mode"]);
|
||||
const OPEN_H3_REFINE_FIELDS = ["h3_open_refine_enabled", "h3_open_refine_scale", "h3_open_refine_model_name", "h3_open_refine_precision", "h3_open_refine_device", "h3_open_refine_temporal_chunking", "h3_open_refine_lora_name", "h3_open_refine_strength", "h3_open_refine_distill_lora_name", "h3_open_refine_distill_strength", "h3_open_refine_sigma_preset", "h3_open_refine_custom_sigmas", "h3_open_refine_seed", "h3_open_refine_prompt"];
|
||||
const AUTO_IMPORT_BLOCKED = new Set([
|
||||
"global_prompt", "timeline_data", "image_paths", "frame_rate", "audio_mode",
|
||||
"guide_policy", "min_guide_gap_seconds", "max_guides", "default_force", "promptrelay_epsilon",
|
||||
@@ -38,11 +39,13 @@ const INTERNAL = new Set([
|
||||
// compiler intentionally does not publish to CineLinX, so the PRO surface does
|
||||
// not present them as a second source of truth.
|
||||
const COMPATIBILITY_ONLY = new Set([
|
||||
"h3_guard_reference_memory", "h3_guard_repair_denoise", "h3_guard_repair_steps", "h3_guard_motion_tolerance",
|
||||
"frame_rate", "guide_policy", "min_guide_gap_seconds", "max_guides", "default_force",
|
||||
"promptrelay_epsilon", "ltx_round_mode", "image_width", "image_height",
|
||||
"image_resize_method", "image_multiple_of", "img_compression",
|
||||
]);
|
||||
const GROUPS = [
|
||||
{ id: "continuity_guard", label: "CONTINUITY MONITOR", title: "Read-only Extended AV diagnostics. Repair has been retired; this monitor does not prevent degradation.", modeEngine: true, fields: ["h3_guard_mode", "h3_guard_detail_ratio"] },
|
||||
{ id: "ahead", label: "AHEAD CONTROL ROOM", title: "LatentGoAhead seam controls", fields: [] },
|
||||
{ id: "extension", label: "LONG TAKE · EXTEND", title: "Automatic native AV extension for one authored shot beyond 15 seconds", modeEngine: true, fields: ["extended_av_profile", "extended_av_custom_window_frames", "extended_av_custom_overlap_frames"] },
|
||||
{ id: "continuation", label: "SAVED TAKE · CONTINUATION", title: "Start a new generation from a previously saved AV checkpoint", fields: ["h3_continuation_enabled", "h3_continuation_save_enabled", "h3_continuation_checkpoint", "h3_continuation_context_frames", "h3_continuation_handover_mode", "h3_continuation_manual_tail_frames", "h3_continuation_visual_handover", "h3_continuation_run_and_gun_enabled", "h3_continuation_run_and_gun_join", "h3_continuation_soft_video_frames", "h3_continuation_soft_video_curve", "h3_continuation_soft_audio_ms"] },
|
||||
@@ -64,7 +67,7 @@ const GROUPS = [
|
||||
{ id: "audio", label: "2 · AUDIO", title: "Audio authority and EXTEND seam polish", fields: ["audio_mode", "reference_audio_role", "voice_reference_picture_index", "extended_av_soft_audio_handover_ms", "extended_av_boundary_polish_ms", "extended_av_boundary_polish_strength"] },
|
||||
{ id: "memory", label: "3 · MEMORY", title: "VRAM preset", fields: ["performance_profile", "motion_context_window_frames", "text_encoder_device", "h3_exact_profile", "h3_exact_chunk_rows", "h3_exact_precision_mode", "h3_exact_qkv_streaming", "h3_exact_attention_memory", "h3_clipproj_profile", "h3_clipproj_load_mode", "vram_clean_before_decode"] },
|
||||
{ id: "sampling", label: "4 · SAMPLE", title: "Native H3 sampling", fields: ["seed", "seed_policy", "seed_stride", "steps", "sampler_name", "scheduler", "denoise", "shift_video", "shift_audio"] },
|
||||
{ id: "speed", label: "5 · SPEED", title: "Acceleration recipe", fields: ["h3_attention_backend", "h3_veda_enabled", "h3_veda_predictor", "h3_veda_verbose", "acceleration", "turbo_mode", "turbo_lora_name", "turbo_strength", "turbo_sampler_mode", "fused_turbo_model_name", "fused_turbo_sigma_preset", "pdd_lora_name", "pdd_strength", "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "ref_image_size", "sol_conditioning", "spectrum_profile", "h3_sla_sparsity", "h3_sla_dense_last_steps"] },
|
||||
{ id: "speed", label: "5 · SPEED", title: "Acceleration recipe", fields: ["h3_attention_backend", "h3_veda_enabled", "h3_veda_predictor", "h3_veda_verbose", "acceleration", "turbo_mode", "turbo_lora_name", "turbo_strength", "turbo_sampler_mode", "fused_turbo_model_name", "fused_turbo_sigma_preset", "pdd_lora_name", "pdd_strength", "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "secondary_lora_continuation_only", "ref_image_size", "sol_conditioning", "spectrum_profile", "h3_sla_sparsity", "h3_sla_dense_last_steps"] },
|
||||
{ id: "direction", label: "6 · DIRECT · LEGACY", title: "Legacy Direct compatibility", fields: ["reference_role_1", "reference_role_2", "reference_role_3", "reference_role_4", "reference_video_role", "v2v_guide_mode", "v2v_source_range_policy", "v2v_source_offset_seconds", "v2v_source_fit", "v2v_source_end_policy", "v2v_audio_pairing", "flf_join_mode", "flf_overlap_frames", "flf_continuity_mode", "flf_continuity_tail_frames", "flf_continuity_audio", "longvid_guide_window_policy", "keyframe_joint_latent_new", "longvid_terminal_endpoint_mode", "longvid_pianosequenza_2stage_enabled"] }, // IAMCCS_LONGVID_ENDPOINT_STRATEGIES_V1
|
||||
{ id: "control", label: "CONTROLNET", title: "H3 Fun ControlNet", contextual: "control", modeEngine: true, fields: ["h3_controlnet_enabled", "h3_controlnet_name", "h3_controlnet_kind", "h3_controlnet_preprocess_resolution", "h3_controlnet_strength", "h3_controlnet_start_percent", "h3_controlnet_end_percent", "h3_controlnet_frame_scope", "h3_controlnet_end_policy", "h3_controlnet_use_video_audio", "h3_controlnet_dwpose_torch_gpu"] },
|
||||
{ id: "vfx_edit", label: "VFX EDIT", title: "REF2VA · VFX EDIT · aligned video guide", modeEngine: true, fields: ["h3_vfx_lora_name", "h3_vfx_lora_strength", "h3_edit_video_source", "upscale_enabled", "upscale_mode", "upscale_width", "upscale_height"] },
|
||||
@@ -75,6 +78,7 @@ const GROUPS = [
|
||||
{ id: "finish", label: "7 · OUTPUT", title: "Delivery", fields: ["h3_editor_enabled", "upscale_mode", "upscale_enabled", "upscale_width", "upscale_height", "h3_pixel_tiled_method", "h3_pixel_tiled_model_name", "h3_pixel_tiled_tile_size", "h3_pixel_tiled_overlap", "upscale_prompt", "upscale_sage", "upscale_seed_offset", "wan_upscale_denoise", "ltx_seam_safe", "ltx_detailer_enabled", "ltx_detailer_lora_name", "ltx_detailer_strength", "ltx_4k_enabled", "ltx_4k_quality", "ltx_looper_temporal_tile_size", "ltx_looper_temporal_overlap", "ltx_looper_guiding_strength", "ltx_looper_overlap_strength", "ltx_looper_cond_image_strength", "ltx_looper_horizontal_tiles", "ltx_looper_vertical_tiles", "ltx_looper_spatial_overlap", "h3_upres_model_name", "h3_upres_precision", "h3_upres_device", "h3_upres_keep_models_resident", "h3_fast_latent_temporal_chunk", "h3_fast_latent_stage2_window", "h3_fast_latent_stage2_overlap", "h3_fast_latent_stage2_guides", "h3_upres_seed", "h3_upres_steps", "h3_upres_denoise", "h3_upres_sampler", "h3_upres_scheduler", "h3_upres_temporal_chunk", "h3_upres_temporal_overlap", "h3_upres_anchor_strength", "h3_upres_tile_width", "h3_upres_tile_height", "h3_upres_overlap_width", "h3_upres_overlap_height", "h3_upres_fade_width", "h3_upres_fade_height", "h3_upres_min_tile_size", "h3_upres_overlap_mode", "h3_upres_overlap_blend", "h3_upres_rtx_enabled", "h3_upres_rtx_quality", "h3_upres_pixel_groups", "h3_upres_window_frames", "h3_upres_window_overlap", "h3_upres_pixel_method"] },
|
||||
{ id: "advanced", label: "TECHNICAL", title: "Technical controls", dynamic: true, fields: [] },
|
||||
];
|
||||
GROUPS.find(group => group.id === "finish").fields.push(...OPEN_H3_REFINE_FIELDS);
|
||||
const ENGINE_IDS = Object.freeze(GROUPS.filter((group) => group.modeEngine || ["ahead", "continuation", "refmod", "face_refine", "scout"].includes(group.id)).map((group) => group.id));
|
||||
const ENGINE_ID_SET = new Set(ENGINE_IDS);
|
||||
const ASSIGNED = new Set(GROUPS.flatMap((group) => group.fields));
|
||||
@@ -116,6 +120,7 @@ const FUNCTIONAL_LAYOUT = {
|
||||
],
|
||||
finish: [
|
||||
["OUTPUT", ["rife_mode", "upscale_enabled", "upscale_mode", "upscale_width", "upscale_height", "upscale_prompt", "upscale_sage", "upscale_seed_offset", "wan_upscale_denoise"]],
|
||||
["OPEN-H3 REFINEMENT · AFTER ONE NATIVE GENERATION", OPEN_H3_REFINE_FIELDS],
|
||||
["PIXEL SAFE · LOW VRAM", ["h3_pixel_tiled_method", "h3_pixel_tiled_model_name", "h3_pixel_tiled_tile_size", "h3_pixel_tiled_overlap"]],
|
||||
["LTX DELIVERY", ["ltx_seam_safe", "ltx_detailer_enabled", "ltx_detailer_lora_name", "ltx_detailer_strength", "ltx_4k_enabled", "ltx_4k_quality", "ltx_looper_temporal_tile_size", "ltx_looper_temporal_overlap", "ltx_looper_guiding_strength", "ltx_looper_overlap_strength", "ltx_looper_cond_image_strength", "ltx_looper_horizontal_tiles", "ltx_looper_vertical_tiles", "ltx_looper_spatial_overlap"]],
|
||||
["H3 2-PASS MODEL", ["h3_upres_model_name", "h3_upres_precision", "h3_upres_device", "h3_upres_keep_models_resident"]],
|
||||
@@ -314,6 +319,7 @@ function selectDeliveryMode(node, mode) {
|
||||
if (!deliveryModeAvailable(node,mode)) return false;
|
||||
const option = H3_DELIVERY_MODES.find(([key]) => key === mode);
|
||||
if (!option) return false;
|
||||
setValue(node,"h3_open_refine_enabled",false,false);
|
||||
if (["h3_fast_latent_2pass","h3_ultimate_tiled"].includes(mode)) {
|
||||
const selected = String(widget(node,"h3_upres_model_name")?.value || "");
|
||||
const installed = selected && !/^none$/i.test(selected) ? selected : firstChoice(node,"h3_upres_model_name",name => /minimax_h3.*upscaler/i.test(name));
|
||||
@@ -330,6 +336,44 @@ function selectDeliveryMode(node, mode) {
|
||||
}
|
||||
|
||||
const widget = (node, name) => (node?.widgets || []).find((item) => item?.name === name);
|
||||
|
||||
function openH3ResolutionEstimate(node) {
|
||||
const width = Number(widget(node, "width")?.value), height = Number(widget(node, "height")?.value);
|
||||
const scale = Number(widget(node, "h3_open_refine_scale")?.value);
|
||||
if (![width, height, scale].every(value => Number.isFinite(value) && value > 0)) return null;
|
||||
// Match the latent upscaler's Python round-to-even on its 32-pixel grid.
|
||||
const align = value => {
|
||||
const cells = value / 32, lower = Math.floor(cells);
|
||||
return Math.max(16, (cells - lower === 0.5 ? lower + lower % 2 : Math.round(cells)) * 32);
|
||||
};
|
||||
const targetWidth = align(width * scale), targetHeight = align(height * scale);
|
||||
return {width, height, scale, targetWidth, targetHeight,
|
||||
widthGain: targetWidth / width, heightGain: targetHeight / height,
|
||||
pixelGain: targetWidth * targetHeight / (width * height),
|
||||
enabled: Boolean(widget(node, "h3_open_refine_enabled")?.value)};
|
||||
}
|
||||
|
||||
function openH3ResolutionSummary(node) {
|
||||
const box = document.createElement("div");
|
||||
box.className = "h3p-context";
|
||||
box.dataset.openH3Resolution = "true";
|
||||
box.style.cssText = "margin:0 0 10px;line-height:1.6";
|
||||
box.setAttribute("role", "status");
|
||||
box.setAttribute("aria-label", "Open-H3 resolution and pixel gain");
|
||||
const estimate = openH3ResolutionEstimate(node);
|
||||
if (!estimate) { box.textContent = "Set the Native width, height and Open-H3 scale to preview the output size."; return box; }
|
||||
const {width, height, targetWidth, targetHeight, widthGain, heightGain, pixelGain, enabled} = estimate;
|
||||
const format = value => value.toLocaleString("en-US", {maximumFractionDigits: 2});
|
||||
const size = document.createElement("b");
|
||||
size.textContent = `${width} × ${height} → ${targetWidth} × ${targetHeight}`;
|
||||
const gain = document.createElement("div");
|
||||
gain.textContent = `${format(widthGain)}× width · ${format(heightGain)}× height · ${format(pixelGain)}× pixels`;
|
||||
const note = document.createElement("div");
|
||||
note.style.cssText = "color:#a9b7c5;font-size:9px";
|
||||
note.textContent = `${enabled ? "Estimated Open-H3 output" : "Open-H3 OFF: configured target only; refinement disabled"}. Based on the Native canvas, rounded to 32 px. Pixel gain does not measure quality gain.`;
|
||||
box.append(size, gain, note);
|
||||
return box;
|
||||
}
|
||||
const nodeClass = (node) => String(node?.comfyClass || node?.type || "");
|
||||
function hideWidget(item) {
|
||||
if (!item || item._iamccsProHidden) return;
|
||||
@@ -545,6 +589,10 @@ function connectedNodes(node) {
|
||||
function branchGate(node, name) {
|
||||
const classes = node._iamccsSettingsProConnectedClasses || connectedNodes(node).map(nodeClass);
|
||||
const has = (pattern) => classes.some((value) => pattern.test(value));
|
||||
if (name.startsWith("h3_open_refine_")) return {
|
||||
available: has(/^IAMCCS_MiniMaxH3OpenRefineControl$/),
|
||||
reason: "Load the R49 Open-H3 workflow with its connected refinement settings branch.",
|
||||
};
|
||||
if (name === "keyframe_joint_latent_new") return {
|
||||
available: Boolean(widget(node, "extended_av_profile")),
|
||||
reason: "LATENT NEW uses the B1 EXTEND backend.",
|
||||
@@ -1222,6 +1270,7 @@ function selectedMemory(node) {
|
||||
}
|
||||
// IAMCCS FACE/UPSCALE MODE ASSISTANT V2
|
||||
function selectedDelivery(node) {
|
||||
if (widget(node, "h3_open_refine_enabled")?.value) return "open-h3-refine";
|
||||
if (!widget(node, "upscale_enabled")?.value) return "native-delivery";
|
||||
const mode = String(widget(node, "upscale_mode")?.value || "off");
|
||||
if (!deliveryBranchGate(node, mode).available) return "native-delivery";
|
||||
@@ -1274,8 +1323,14 @@ function deliveryModeAvailable(node, mode) {
|
||||
|
||||
function applyRecipe(node, recipe) {
|
||||
const mode = shotboardMode(node), family = modeFamily(mode), set = (name, value) => setValue(node, name, value, false);
|
||||
if (["native-delivery", "ultimate-tiled", "upres", "h3-pixel", "pixel-safe", "ltx-master", "ltx-per-shot", "rtx-final"].includes(recipe)) set("h3_open_refine_enabled", false);
|
||||
if (["native", "pdd", "fasth3", "sla", "fused"].includes(recipe)) set("h3_omni_enabled", false);
|
||||
if (recipe === "omni") {
|
||||
if (recipe === "open-h3-refine") {
|
||||
if (!branchGate(node, "h3_open_refine_enabled").available) return false;
|
||||
set("upscale_enabled", false); set("upscale_mode", "off");
|
||||
set("h3_open_refine_enabled", true); set("rife_mode", "off");
|
||||
set("h3_open_refine_sigma_preset", "gentle_3step");
|
||||
} else if (recipe === "omni") {
|
||||
resetAcceleration(set); set("h3_omni_enabled", true); set("acceleration", "h3_sage"); set("h3_attention_backend", "h3_sage");
|
||||
set("steps", 8); set("sampler_name", "euler"); set("scheduler", "simple");
|
||||
set("denoise", 1); set("shift_video", 12); set("shift_audio", 3);
|
||||
@@ -1834,6 +1889,7 @@ function mount(node) {
|
||||
const targetEngine = modeEngineGroup(node, assistantModeKey(node, mode));
|
||||
return GROUPS.filter((group) => {
|
||||
if (group.id === "engine_reference_image") return false;
|
||||
if (group.id === "continuity_guard") return true;
|
||||
if (group.id === "ahead" && mode !== "latent_go_ahead") return false;
|
||||
if (group.modeEngine && group.id !== targetEngine) return false;
|
||||
return ["continuation", "refmod"].includes(group.id) || group.assistant || group.dynamic || group.fields.some((name) => widget(node, name));
|
||||
@@ -1844,10 +1900,11 @@ function mount(node) {
|
||||
return (node.widgets || []).map((item) => item?.name).filter((name) => name && !ASSIGNED.has(name) && !SHOTBOARD_OWNED.has(name) && !INTERNAL.has(name) && !COMPATIBILITY_ONLY.has(name) && fieldRelevant(name, mode));
|
||||
}
|
||||
function makeControl(name) {
|
||||
const item = widget(node, name); if (!item) return null;
|
||||
const item = widget(node, name); if (!item || (name.startsWith("h3_guard_") && COMPATIBILITY_ONLY.has(name))) return null;
|
||||
const box = document.createElement("div"); box.className = "h3p-field";
|
||||
const label = document.createElement("label"); label.textContent = human(name); box.append(label);
|
||||
let control; const options = choices(item);
|
||||
if (name === "secondary_lora_continuation_only") box.style.gridColumn = "1 / -1";
|
||||
const label = document.createElement("label"); label.textContent = name === "secondary_lora_continuation_only" ? "Second LoRA: from segment 2" : human(name); box.append(label);
|
||||
let control; const options = name === "h3_guard_mode" ? ["off", "monitor"] : choices(item);
|
||||
if (options.length) {
|
||||
control = document.createElement("select"); options.forEach((value) => control.add(new Option(
|
||||
name === "h3_fast_latent_stage2_guides"
|
||||
@@ -1859,6 +1916,19 @@ function mount(node) {
|
||||
control = document.createElement("input"); control.type = typeof item.value === "number" ? "number" : "text"; control.value = item.value ?? "";
|
||||
if (control.type === "number") { if (Number.isFinite(item.options?.min)) control.min = item.options.min; if (Number.isFinite(item.options?.max)) control.max = item.options.max; control.step = item.options?.step ?? "any"; }
|
||||
}
|
||||
if (name.startsWith("h3_guard_")) {
|
||||
const supported = assistantModeKey(node) === "fl2va_extended_av";
|
||||
const mode = String(widget(node, "h3_guard_mode")?.value || "off");
|
||||
if (name === "h3_guard_mode" && mode === "repair") {
|
||||
control.value = "off";
|
||||
control.title = "Saved Repair mode is retired and runs as Off. Select Monitor for diagnostics.";
|
||||
const notice = document.createElement("small");
|
||||
notice.textContent = "Saved Repair mode retired - effective mode: Off.";
|
||||
box.append(notice);
|
||||
}
|
||||
control.disabled = !supported || (name !== "h3_guard_mode" && mode !== "monitor");
|
||||
if (!supported) control.title = "Continuity Guard currently supports Extended AV only.";
|
||||
}
|
||||
const gate = branchGate(node, name);
|
||||
if (!gate.available) {
|
||||
control.disabled = true;
|
||||
@@ -1880,6 +1950,10 @@ function mount(node) {
|
||||
const value = control.type === "checkbox" ? control.checked : control.type === "number" ? Number(control.value) : control.value;
|
||||
if (name === "duration_seconds") setShotboardDuration(node, value);
|
||||
else if (name === "audio_mode") setShotboardAudio(node, value);
|
||||
else if (name === "h3_open_refine_enabled") {
|
||||
if (value) applyRecipe(node, "open-h3-refine");
|
||||
else setValue(node, name, false, false);
|
||||
}
|
||||
else if (name === "upscale_enabled") selectDeliveryMode(node,value ? (String(widget(node,"upscale_mode")?.value || "off") !== "off" ? widget(node,"upscale_mode").value : node.properties?.iamccs_delivery_route || "h3_ultimate_tiled") : "off");
|
||||
else if (name === "upscale_mode") selectDeliveryMode(node,String(value));
|
||||
else if (["upscale_width","upscale_height"].includes(name)) {
|
||||
@@ -2181,6 +2255,7 @@ function mount(node) {
|
||||
const delivery = document.createElement("div"); delivery.className = "h3p-recipes h3p-delivery-recipes";
|
||||
addRecipe(delivery, "FACE REFINE · TOGGLE", "face-refine", true, selectedFace);
|
||||
addRecipe(delivery, "NATIVE DELIVERY", "native-delivery", true, selectedFinish === "native-delivery");
|
||||
addRecipe(delivery, "OPEN-H3 REFINEMENT · 3 STEP", "open-h3-refine", branchGate(node, "h3_open_refine_enabled").available, selectedFinish === "open-h3-refine");
|
||||
addRecipe(delivery, "H3 DETAIL · 1-STEP REFINE", "ultimate-tiled", deliveryModeAvailable(node, "h3_ultimate_tiled"), selectedFinish === "ultimate-tiled");
|
||||
addRecipe(delivery, "H3 2-PASS UPRES", "upres", branchGate(node, "h3_upres_model_name").available && deliveryModeAvailable(node, "h3_fast_latent_2pass"), selectedFinish === "upres");
|
||||
addRecipe(delivery, "PIXEL SAFE · LOW VRAM", "pixel-safe", deliveryModeAvailable(node, "pixel_tiled_low_vram"), selectedFinish === "pixel-safe");
|
||||
@@ -2191,6 +2266,7 @@ function mount(node) {
|
||||
grid.append(delivery);
|
||||
const detailNote = document.createElement("p");
|
||||
detailNote.textContent = mode === "v2va_face_swap" ? "SAM3 crop swap supports Native / RTX delivery. H3 Detail needs a full-frame latent and is unavailable for this legacy crop mode." : "H3 DETAIL uses the proven SwapChar second-pass recipe for full-frame video modes: learned latent upscale, 1 step, SA Solver / simple, denoise 0.2, seed 777. Output exposes the shared seed, dimensions, model, sampling and tile controls. The native pass keeps its own settings.";
|
||||
if (selectedFinish === "open-h3-refine") detailNote.textContent = "One native generation followed by Open-H3 refinement. Dedicated controls are in Output / Open-H3 Refinement. The source soundtrack is retained. Gentle sigma 0.8 still requires visual review of identity and acting; the high-resolution transformer pass is not tiled.";
|
||||
grid.append(detailNote);
|
||||
}
|
||||
async function renderAssetLibrary(kind, grid) {
|
||||
@@ -2667,10 +2743,10 @@ function mount(node) {
|
||||
stack.append(stackText, reset);
|
||||
recipes.append(presetGrid, stack);
|
||||
}
|
||||
const recipeSet = active === "memory" ? [["≤ 8–12 GB · 124F / 2048 ROWS", "vram8"], ["12–16 GB · 209F / 4096 ROWS", "vram12"], ["16–24 GB · 294F / 8192 ROWS", "vram16"], ["24 GB+ · 362F / 16384 ROWS", "vram24"]] : active === "speed" ? [["NATIVE QUALITY · 20 STEP", "native"], ["PDD · 8 STEP", "pdd"], ["FASTH3 · 6 STEP", "fasth3"], ["SLA · 4 STEP", "sla"], ["FUSED FAST · T2VA ONLY", "fused"]] : active === "control" ? [["ENABLE + SELECT INSTALLED MODEL", "control"]] : active === "face" ? [["SAM3 SUBJECT SWAP · SAFE DEFAULTS", "face"]] : active === "face_refine" ? [["FACE REFINE · TOGGLE", "face-refine"]] : active === "finish" ? [["NATIVE DELIVERY", "native-delivery"], ["H3 DETAIL · 1-STEP REFINE", "ultimate-tiled"], ["PIXEL SAFE · LOW VRAM", "pixel-safe"], ["H3 2-PASS UPRES", "upres"], ["H3 PIXEL REFINE", "h3-pixel"], ["LTX 2.3 MASTER", "ltx-master"], ["LTX 2.3 PER-SHOT", "ltx-per-shot"], ["RTX FINAL", "rtx-final"]] : [];
|
||||
const recipeSet = active === "memory" ? [["≤ 8–12 GB · 124F / 2048 ROWS", "vram8"], ["12–16 GB · 209F / 4096 ROWS", "vram12"], ["16–24 GB · 294F / 8192 ROWS", "vram16"], ["24 GB+ · 362F / 16384 ROWS", "vram24"]] : active === "speed" ? [["NATIVE QUALITY · 20 STEP", "native"], ["PDD · 8 STEP", "pdd"], ["FASTH3 · 6 STEP", "fasth3"], ["SLA · 4 STEP", "sla"], ["FUSED FAST · T2VA ONLY", "fused"]] : active === "control" ? [["ENABLE + SELECT INSTALLED MODEL", "control"]] : active === "face" ? [["SAM3 SUBJECT SWAP · SAFE DEFAULTS", "face"]] : active === "face_refine" ? [["FACE REFINE · TOGGLE", "face-refine"]] : active === "finish" ? [["OPEN-H3 REFINEMENT · 3 STEP", "open-h3-refine"], ["NATIVE DELIVERY", "native-delivery"], ["H3 DETAIL · 1-STEP REFINE", "ultimate-tiled"], ["PIXEL SAFE · LOW VRAM", "pixel-safe"], ["H3 2-PASS UPRES", "upres"], ["H3 PIXEL REFINE", "h3-pixel"], ["LTX 2.3 MASTER", "ltx-master"], ["LTX 2.3 PER-SHOT", "ltx-per-shot"], ["RTX FINAL", "rtx-final"]] : [];
|
||||
recipeSet.forEach(([label, id]) => {
|
||||
const deliveryRecipe = ["native-delivery", "ultimate-tiled", "pixel-safe", "upres", "h3-pixel", "ltx-master", "ltx-per-shot", "rtx-final", "face-refine"].includes(id);
|
||||
const deliveryAvailable = id === "upres"
|
||||
const deliveryRecipe = ["open-h3-refine", "native-delivery", "ultimate-tiled", "pixel-safe", "upres", "h3-pixel", "ltx-master", "ltx-per-shot", "rtx-final", "face-refine"].includes(id);
|
||||
const deliveryAvailable = id === "open-h3-refine" ? branchGate(node, "h3_open_refine_enabled").available : id === "upres"
|
||||
? branchGate(node, "h3_upres_model_name").available && deliveryModeAvailable(node, "h3_fast_latent_2pass")
|
||||
: id === "ultimate-tiled" ? deliveryModeAvailable(node, "h3_ultimate_tiled")
|
||||
: id === "h3-pixel" ? deliveryModeAvailable(node, "h3_pixel_refine")
|
||||
@@ -2757,6 +2833,7 @@ function mount(node) {
|
||||
const grid = q("[data-grid]"); grid.className = "h3p-grid"; grid.replaceChildren();
|
||||
if (mode === "v2va_face_swap_lora" && ["swap_lora", "overview"].includes(active)) swapDuration.render(grid);
|
||||
let names = fieldNames(group, mode);
|
||||
if (active === "finish") names = [...new Set([...names, ...OPEN_H3_REFINE_FIELDS.filter(name => widget(node, name))])];
|
||||
if (active === "swap_lora" && !names.includes("duration_seconds")) names.unshift("duration_seconds");
|
||||
if (active === "overview" && (mode !== "longvid_guides" || String(widget(node,"upscale_mode")?.value) === "h3_fast_latent_2pass")) {
|
||||
names = names.filter((name) => !["upscale_link_to_native", "upscale_link_factor"].includes(name));
|
||||
@@ -2774,7 +2851,7 @@ function mount(node) {
|
||||
if (!check.checked) {
|
||||
const selected = selectedAcceleration(node);
|
||||
const fields = {omni: ["h3_omni_enabled", "h3_clipproj_profile", "h3_clipproj_load_mode"], native: [], pdd: ["pdd_lora_name"], fasth3: ["turbo_lora_name"], sla: ["turbo_lora_name", "h3_sla_sparsity"], fused: ["fused_turbo_model_name"]}[selected] || ["turbo_mode", "turbo_lora_name"];
|
||||
names = names.filter(name => ["acceleration", "h3_attention_backend", "h3_veda_enabled", "h3_veda_predictor", "h3_veda_verbose", ...fields].includes(name));
|
||||
names = names.filter(name => ["acceleration", "h3_attention_backend", "h3_veda_enabled", "h3_veda_predictor", "h3_veda_verbose", ...fields, "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "secondary_lora_continuation_only"].includes(name));
|
||||
}
|
||||
}
|
||||
let layout = FUNCTIONAL_LAYOUT[active];
|
||||
@@ -2784,7 +2861,7 @@ function mount(node) {
|
||||
}
|
||||
if (layout) {
|
||||
const used = new Set();
|
||||
layout.forEach(([title, fields]) => { const relevant = fields.filter((name) => names.includes(name)); if (!relevant.length) return; const section = document.createElement("section"); section.className = "h3p-functional"; section.innerHTML = `<div class="h3p-functional-title">${title}</div>`; const inner = document.createElement("div"); inner.className = "h3p-functional-grid"; relevant.forEach((name) => { used.add(name); const control = makeControl(name); if (control) inner.append(control); }); section.append(inner); grid.append(section); });
|
||||
layout.forEach(([title, fields]) => { const relevant = fields.filter((name) => names.includes(name)); if (!relevant.length) return; const section = document.createElement("section"); section.className = "h3p-functional"; section.innerHTML = `<div class="h3p-functional-title">${title}</div>`; if (relevant.includes("h3_open_refine_scale")) section.append(openH3ResolutionSummary(node)); const inner = document.createElement("div"); inner.className = "h3p-functional-grid"; relevant.forEach((name) => { used.add(name); const control = makeControl(name); if (control) inner.append(control); }); section.append(inner); grid.append(section); });
|
||||
const remaining = names.filter((name) => !used.has(name)); if (remaining.length) { const section = document.createElement("section"); section.className = "h3p-functional"; section.innerHTML = '<div class="h3p-functional-title">OTHER</div>'; const inner = document.createElement("div"); inner.className = "h3p-functional-grid"; remaining.forEach((name) => { const control = makeControl(name); if (control) inner.append(control); }); section.append(inner); grid.append(section); }
|
||||
} else names.forEach((name) => { const control = makeControl(name); if (control) grid.append(control); });
|
||||
if (active === "direction" && ["keyframe_joint_native", "latent_go_ahead"].includes(String(mode))) {
|
||||
@@ -2806,6 +2883,7 @@ function mount(node) {
|
||||
const assetIssues = selectedH3AssetCompatibility(node, mode);
|
||||
const values = [["Sampling recipe", samplingRecipeLabel(node)], ["Generation authority", node.properties?.iamccs_settings_master ? "SETTINGS PRO" : "SHOTBOARD / INITIAL"], ["Duration authority", node.properties?.iamccs_settings_master ? `SETTINGS PRO · ${widget(node,"duration_seconds")?.value ?? "—"} s · synced to Shotboard` : `SHOTBOARD · ${widget(node,"duration_seconds")?.value ?? "—"} s`], ["Mode", `${mode} · Settings PRO when connected`], ["Editorial authority", "SHOTBOARD · prompts, guides, media, timeline and audio"], ["Asset compatibility", assetIssues.some(([kind]) => kind === "error") ? "INCOMPATIBLE" : assetIssues.length ? "REVIEW" : "COMPATIBLE"], ["VRAM preset", friendly(widget(node, "performance_profile")?.value)], ["Memory contract", `${widget(node,"motion_context_window_frames")?.value ?? "—"} frames · ${widget(node,"h3_exact_chunk_rows")?.value ?? "—"} rows`], ["Guide window", String(mode) === "longvid_guides" ? selectedGuideWindowStrategy(node) : "N/A"], ["Extension engine", String(mode) === "fl2va_extended_av" ? `${friendly(widget(node,"extended_av_profile")?.value)} · MASKED + EXACT · TRIM PINNED HEAD` : "OFF"], ["Sampling / legacy profile", friendly(widget(node, "acceleration")?.value)], ["Speed adapter (Turbo / FastH3 / SLA)", String(widget(node, "turbo_mode")?.value || "off") === "off" && !["fasth3_dense_6step", "h3_sla"].includes(String(widget(node, "acceleration")?.value || "")) ? "OFF" : (widget(node, "turbo_lora_name")?.value || "MISSING")], ["PDD LoRA", widget(node,"pdd_lora_name")?.value || "OFF"], ["Fused model", widget(node, "fused_turbo_model_name")?.value || "OFF"], ["Effective sampling", `${widget(node, "steps")?.value ?? "—"} steps · ${widget(node, "sampler_name")?.value ?? "—"} · ${widget(node, "scheduler")?.value ?? "—"}`], ["Canvas", `${widget(node, "width")?.value ?? "—"} × ${widget(node, "height")?.value ?? "—"}`], ["ControlNet", widget(node, "h3_controlnet_name")?.value || "OFF"], ["Delivery", widget(node, "upscale_enabled")?.value ? widget(node, "upscale_mode")?.value : "NATIVE"], ["Video Editor", Boolean(widget(node,"h3_editor_enabled")?.value) ? "ON · post-master" : "OFF · direct joined master"]];
|
||||
values.splice(1,0,["Attention & memory",attentionLabel(node)]);
|
||||
if (widget(node,"h3_open_refine_enabled")?.value) values.find(([label]) => label === "Delivery")[1] = `OPEN-H3 REFINEMENT · ${widget(node,"h3_open_refine_scale")?.value ?? 2}× · ${widget(node,"h3_open_refine_sigma_preset")?.value || "gentle_3step"}`;
|
||||
values.push(["VEDA", widget(node,"h3_veda_enabled")?.value ? `ON · ${widget(node,"h3_veda_predictor")?.value || "auto"}` : "OFF"]);
|
||||
const list = q(".h3p-truth-list"); list.replaceChildren(); values.forEach(([label, value]) => { const row = document.createElement("div"); row.className = "h3p-truth-row"; const caption = document.createElement("span"), content = document.createElement("b"); caption.textContent = label; content.textContent = String(value ?? "—"); row.append(caption, content); list.append(row); });
|
||||
const issueList = warnings(mode), health = q(".h3p-health"); health.className = `h3p-health ${issueList.some(([kind]) => kind === "error") ? "error" : issueList.some(([kind]) => kind === "warn") ? "warn" : ""}`; health.innerHTML = issueList.map(([, message]) => `• ${message}`).join("<br>");
|
||||
|
||||
@@ -12773,6 +12773,7 @@ function renderShotboardV3(node) {
|
||||
addWidgetBoolSetting("Enable second H3 LoRA", "secondary_lora_enabled");
|
||||
addSelectSetting("Second LoRA file", "secondary_lora_name", Array.isArray(secondaryLoraValues) && secondaryLoraValues.length ? secondaryLoraValues : [""]);
|
||||
addSetting("Second LoRA strength", "secondary_lora_strength", "0.05", "-2");
|
||||
addWidgetBoolSetting("Second LoRA: from segment 2", "secondary_lora_continuation_only");
|
||||
const pddLoraValues = getWidget(node, "pdd_lora_name")?.options?.values;
|
||||
addSelectSetting("PDD acceleration LoRA", "pdd_lora_name", Array.isArray(pddLoraValues) && pddLoraValues.length ? pddLoraValues : [""]);
|
||||
addSetting("PDD strength / trained 1.0", "pdd_strength", "0.05", "0");
|
||||
@@ -20902,7 +20903,7 @@ const H3_SETTINGS_UI_GROUPS = [
|
||||
"fused_turbo_model_name", "fused_turbo_sigma_preset",
|
||||
"h3_exact_profile", "h3_clipproj_profile", "h3_clipproj_load_mode",
|
||||
"h3_exact_chunk_rows", "h3_exact_precision_mode", "h3_exact_qkv_streaming", "h3_exact_attention_memory",
|
||||
"secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength",
|
||||
"secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "secondary_lora_continuation_only",
|
||||
"seed", "seed_stride", "sampler_name", "scheduler", "denoise", "shift_video", "shift_audio",
|
||||
"ref_image_size", "sol_conditioning", "spectrum_profile", "vram_clean_before_decode", "rife_mode",
|
||||
],
|
||||
@@ -20981,7 +20982,7 @@ const H3_SETTINGS_FULL_UI_GROUPS = [
|
||||
note: "Primary/secondary H3 LoRA and execution acceleration. Every visible box remains editable truth.",
|
||||
names: [
|
||||
"turbo_mode", "turbo_lora_name", "turbo_strength", "turbo_sampler_mode",
|
||||
"secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength",
|
||||
"secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "secondary_lora_continuation_only",
|
||||
"acceleration", "ref_image_size", "sol_conditioning", "spectrum_profile", "vram_clean_before_decode", "rife_mode",
|
||||
"fused_turbo_model_name", "fused_turbo_sigma_preset",
|
||||
],
|
||||
@@ -21087,7 +21088,7 @@ const H3_SETTINGS_PRO_UI_GROUPS = [
|
||||
{
|
||||
id: "speed", label: "4 · H3 SPEED", title: "STAGE A · NATIVE H3 ACCELERATION",
|
||||
note: "Speeds up only the native H3 sample. It does not select an upscale or add a delivery stage. PDD and LightX2V Turbo are separate routes.",
|
||||
names: ["acceleration", "pdd_lora_name", "pdd_strength", "turbo_mode", "turbo_lora_name", "turbo_strength", "turbo_sampler_mode", "fused_turbo_model_name", "fused_turbo_sigma_preset", "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "ref_image_size", "sol_conditioning", "spectrum_profile", "vram_clean_before_decode", "rife_mode", "h3_exact_profile", "h3_clipproj_profile", "h3_clipproj_load_mode", "h3_exact_chunk_rows", "h3_exact_precision_mode", "h3_exact_qkv_streaming", "h3_exact_attention_memory"],
|
||||
names: ["acceleration", "pdd_lora_name", "pdd_strength", "turbo_mode", "turbo_lora_name", "turbo_strength", "turbo_sampler_mode", "fused_turbo_model_name", "fused_turbo_sigma_preset", "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "secondary_lora_continuation_only", "ref_image_size", "sol_conditioning", "spectrum_profile", "vram_clean_before_decode", "rife_mode", "h3_exact_profile", "h3_clipproj_profile", "h3_clipproj_load_mode", "h3_exact_chunk_rows", "h3_exact_precision_mode", "h3_exact_qkv_streaming", "h3_exact_attention_memory"],
|
||||
},
|
||||
{
|
||||
id: "direction", label: "5 · DIRECT", title: "MODE-SPECIFIC CONTRACT",
|
||||
@@ -21118,7 +21119,7 @@ const H3_SETTINGS_TAB_GROUPS = [...H3_SETTINGS_PRO_UI_GROUPS, ...H3_SETTINGS_FUL
|
||||
const H3_SETTINGS_ADVANCED_FIELDS = new Set([
|
||||
"reference_resize_megapixels", "reference_resize_filter", "h3_exact_profile", "h3_clipproj_load_mode",
|
||||
"h3_exact_chunk_rows", "h3_exact_precision_mode", "h3_exact_qkv_streaming", "h3_exact_attention_memory",
|
||||
"turbo_sampler_mode", "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength",
|
||||
"turbo_sampler_mode", "secondary_lora_enabled", "secondary_lora_name", "secondary_lora_strength", "secondary_lora_continuation_only",
|
||||
"seed_stride", "sampler_name", "scheduler", "denoise", "shift_video", "shift_audio", "ref_image_size",
|
||||
"sol_conditioning", "spectrum_profile", "vram_clean_before_decode", "rife_mode",
|
||||
"upscale_prompt", "upscale_sage", "upscale_seed_offset", "wan_upscale_denoise", "ltx_seam_safe",
|
||||
@@ -21206,6 +21207,7 @@ function h3SettingsUiLabel(name) {
|
||||
turbo_strength: "Turbo strength", turbo_sampler_mode: "Turbo sampler", reference_resize_policy: "Fit policy",
|
||||
secondary_lora_enabled: "Enable creative / projection H3 LoRA", secondary_lora_name: "Creative / projection H3 LoRA",
|
||||
secondary_lora_strength: "Creative / projection strength",
|
||||
secondary_lora_continuation_only: "Second LoRA: from segment 2",
|
||||
pdd_lora_name: "PDD acceleration LoRA", pdd_strength: "PDD strength",
|
||||
h3_controlnet_enabled: "Enable H3 Fun ControlNet", h3_controlnet_name: "Fun ControlNet model",
|
||||
h3_controlnet_kind: "Control kind / preprocessor", h3_controlnet_strength: "Control strength",
|
||||
|
||||
@@ -2115,6 +2115,7 @@ function mountPrompter(node) {
|
||||
prompt:[shot.local_prompt,shot.h3_transition_prompt].filter(Boolean).join("\n")}));
|
||||
project.injection_target = "global";
|
||||
aiApiKey.value = ""; commit(); renderSections(); renderPreview();
|
||||
workspace?.showReview();
|
||||
aiStatus.className = "iamccs-pr-ai-status ok";
|
||||
aiStatus.textContent = `Global + ${project.local_prompts.length} local prompts ready. Review, then INJECT → SHOTBOARD.`;
|
||||
} catch (error) { aiStatus.className = "iamccs-pr-ai-status error"; aiStatus.textContent = error.message; }
|
||||
@@ -2503,11 +2504,9 @@ function mountPrompter(node) {
|
||||
? "Qwen CLIP: loading the encoder, reading references and writing the H3 prompt. The first REQUEST includes model loading; keep this window open."
|
||||
: `Sending ${targetKeys.join(", ")} to ${aiProvider.options[aiProvider.selectedIndex]?.text || aiProvider.value}.`;
|
||||
const imagePayload = await buildAIImagePayload();
|
||||
// A new image request is authoritative. Never feed previous generated
|
||||
// image fields back as rough text, otherwise local models tend to
|
||||
// concatenate the old prompt with the new refinement.
|
||||
const rewriteInputSections = isImageRequest || isSwapCharRequest
|
||||
? Object.fromEntries(Object.keys(allSections).map((key) => [key, ""]))
|
||||
// REQUEST replaces selected drafts; unselected fields remain context.
|
||||
const rewriteInputSections = narrativeRequest !== null
|
||||
? Object.fromEntries(Object.entries(allSections).map(([key, value]) => [key, targetKeys.includes(key) ? "" : value]))
|
||||
: allSections;
|
||||
const response = await fetchAI("/iamccs/prompter/rewrite", {
|
||||
method: "POST",
|
||||
@@ -2520,6 +2519,7 @@ function mountPrompter(node) {
|
||||
task_mode: project.task_mode,
|
||||
sections: rewriteInputSections,
|
||||
target_keys: targetKeys,
|
||||
request_text: narrativeRequest,
|
||||
user_direction: project.omni_preset && project.task_mode === "ref2va" ? `${omniRequestDirection(project)}\n${direction}` : project.task_mode === "reference_image"
|
||||
? `${project.image_preset === "reference_sheet" ? "REFERENCE SHEET: one combined canvas containing every requested character and prop." : "SINGLE STATIC IMAGE."}\n${direction}` : direction,
|
||||
vision_model: aiVisionModel.value.trim(),
|
||||
@@ -2589,6 +2589,7 @@ function mountPrompter(node) {
|
||||
renderSections();
|
||||
renderPreview();
|
||||
commit();
|
||||
if (narrativeRequest !== null) workspace?.showReview();
|
||||
aiStatus.className = "iamccs-pr-ai-status ok";
|
||||
const visualCount = Number(data.report?.visual_references?.length || 0);
|
||||
aiStatus.textContent = isExtendedEvolvingRequest
|
||||
|
||||
Reference in New Issue
Block a user