Files
IAMCCS-IAMCCS-nodes/iamccs_h3_previs.py
T
2026-09-23 18:32:54 +02:00

94 lines
5.7 KiB
Python

"""Previs depth producer for the existing R42/R43 H3 control transport."""
import json
import math
import torch
def blocking_prompt(bindings_json):
data = json.loads(bindings_json)
if not isinstance(data, list) or len(data) > 32:
raise ValueError('Bindings must be a JSON list of at most 32 subjects.')
lines = ['Follow the supplied spatial control for camera perspective, parallax and blocking. '
'Preserve the designed appearance from the image references. '
'Use natural articulation inside the supplied coarse trajectories.']
for item in data:
if not isinstance(item, dict):
raise ValueError('Each binding must contain proxy, subject and trajectory.')
values = [str(item.get(key, '')).strip() for key in ('proxy', 'subject', 'trajectory')]
if not all(values):
raise ValueError('Each binding requires proxy, subject and trajectory descriptions.')
lines.append(f'{values[1]} follows {values[2]}. Proxy label: {values[0]}. '
'The proxy defines blocking only; do not reproduce its primitive shape or material.')
return '\n'.join(lines)
class IAMCCS_H3PrevisControl:
@classmethod
def INPUT_TYPES(cls):
return {'required': {
'cine_linx': ('IAMCCS_SUPERNODE_LINX',),
'enabled': ('BOOLEAN', {'default': False}),
'representation': (['direct_depth', 'rgb_proxy_to_depth'],),
'source_fps': ('FLOAT', {'default': 24., 'min': 1., 'max': 240.}),
'source_offset_seconds': ('FLOAT', {'default': 0., 'min': 0., 'max': 86400.}),
'depth_polarity': (['near_white', 'near_black'],),
'preprocess_resolution': ('INT', {'default': 512, 'min': 128, 'max': 2048, 'step': 64}),
'bindings_json': ('STRING', {'multiline': True, 'default': '[{"proxy":"yellow cylinder","subject":"the protagonist in <Picture 1>","trajectory":"from screen left to centre, then stops on the foreground mark"}]'}),
}, 'optional': {'previs_video': ('IMAGE', {'lazy': True})}}
RETURN_TYPES = ('IAMCCS_SUPERNODE_LINX', 'IMAGE', 'STRING', 'STRING')
RETURN_NAMES = ('cine_linx', 'exact_depth_preview', 'blocking_prompt', 'manifest')
FUNCTION = 'inject'
CATEGORY = 'IAMCCS/MiniMax H3/Previs'
def check_lazy_status(self, cine_linx, enabled=False, previs_video=None, **kwargs):
return ['previs_video'] if enabled and previs_video is None else []
def inject(self, cine_linx, enabled, representation, source_fps,
source_offset_seconds, depth_polarity, preprocess_resolution,
bindings_json, previs_video=None):
if not enabled:
return cine_linx, None, '', json.dumps({'enabled': False})
if not isinstance(cine_linx, dict):
raise ValueError('PREVIS requires the Settings/CineH3Input bus before Shotboard.')
if not torch.is_tensor(previs_video) or previs_video.ndim != 4 or len(previs_video) < 1:
raise ValueError('Connect decoded previs IMAGE frames; a filename is not an IMAGE batch.')
if not math.isfinite(source_fps) or source_fps <= 0 or not math.isfinite(source_offset_seconds) or source_offset_seconds < 0:
raise ValueError('Invalid previs FPS or source offset.')
prompt = blocking_prompt(bindings_json)
from .iamccs_cine_h3_bus import IAMCCS_CineH3FunControlInput
producer = IAMCCS_CineH3FunControlInput
if representation == 'rgb_proxy_to_depth':
depth = producer._preprocess(previs_video, 'depth_anything', preprocess_resolution)
elif representation == 'direct_depth':
if previs_video.shape[-1] != 3:
raise ValueError('Direct depth requires an RGB IMAGE batch containing grayscale depth.')
# Reject ID/color renders: they do not encode geometric distance.
if float((previs_video[..., 0] - previs_video[..., 1]).abs().max()) > .03 or float((previs_video[..., 1] - previs_video[..., 2]).abs().max()) > .03:
raise ValueError('Direct depth is not grayscale. Use RGB proxy to depth for colored primitives.')
depth = previs_video
else:
raise ValueError('Unknown previs representation.')
if not bool(torch.isfinite(depth).all()) or float(depth.min()) < 0 or float(depth.max()) > 1:
raise ValueError('Depth must contain finite normalized values in [0,1].')
if representation == 'direct_depth' and depth_polarity == 'near_black':
depth = 1 - depth
result = producer().inject(cine_linx, source_fps, control_video=depth)
out = result['result'][0] if isinstance(result, dict) else result[0]
manifest = {'schema': 'iamccs.h3.previs', 'version': 1, 'enabled': True,
'representation': representation, 'source_fps': source_fps,
'source_offset_seconds': source_offset_seconds, 'frames': len(depth),
'camera_authority': True, 'geometry_authority': True,
'identity_authority': False, 'proxy_rgb_to_model': False,
'bindings': json.loads(bindings_json),
'binding_contract': 'text direction, not deterministic object tracking',
'depth_polarity': 'near_white', 'prompt': prompt}
out['resources']['iamccs_h3_previs_manifest'] = manifest
out['outputs']['iamccs_h3_previs_manifest'] = manifest
out['resources']['iamccs_minimax_h3_control_video_meta']['previs'] = manifest
return out, depth, prompt, json.dumps(manifest, ensure_ascii=False, indent=2)
NODE_CLASS_MAPPINGS = {'IAMCCS_H3PrevisControl': IAMCCS_H3PrevisControl}
NODE_DISPLAY_NAME_MAPPINGS = {'IAMCCS_H3PrevisControl': 'IAMCCS H3 PREVIS · Camera + Subject Blocking'}