Updated IAMCCS-nodes to version 1.5.1
This commit is contained in:
+1
-1
@@ -43,7 +43,7 @@ RTX_COMMON_RATIOS = [
|
||||
"1:1", "4:5", "5:4", "3:4", "4:3", "2:3", "3:2", "16:9", "9:16",
|
||||
"16:10", "10:16", "21:9", "9:21",
|
||||
]
|
||||
RTX_DEFAULT_DIVISIBLE_BY = "1"
|
||||
RTX_DEFAULT_DIVISIBLE_BY = "8"
|
||||
RTX_INSTALL_GUIDE_URL = "https://deno2026.github.io/comfyui-deno-custom-nodes/rtx-vfx-install/"
|
||||
_RUNTIME_MARKER_NAME = "DENO_RTX_VFX_runtime_path.txt"
|
||||
|
||||
|
||||
@@ -519,10 +519,10 @@ def _as_audio_waveform(audio: Any) -> Tuple[torch.Tensor, int]:
|
||||
return waveform.clamp(-1.0, 1.0).contiguous(), sample_rate
|
||||
|
||||
|
||||
def _frame_payload(images: Any, profile: Dict[str, Any]) -> Tuple[bytes, str, int, int, int]:
|
||||
def _frame_payload_info(images: Any, profile: Dict[str, Any]) -> Tuple[torch.Tensor, str, int, int, int]:
|
||||
if not torch.is_tensor(images):
|
||||
images = torch.as_tensor(images)
|
||||
images = images.detach().to(device="cpu", dtype=torch.float32)
|
||||
images = images.detach()
|
||||
if images.ndim == 5 and images.shape[0] == 1:
|
||||
images = images[0]
|
||||
if images.ndim != 4:
|
||||
@@ -542,22 +542,50 @@ def _frame_payload(images: Any, profile: Dict[str, Any]) -> Tuple[bytes, str, in
|
||||
|
||||
wants_alpha = bool(profile.get("alpha")) and channels == 4
|
||||
keep_channels = 4 if wants_alpha else 3
|
||||
images = images[..., :keep_channels].clamp(0.0, 1.0)
|
||||
use_16bit = int(profile.get("input_depth", 8)) >= 16
|
||||
if use_16bit:
|
||||
data = np.rint(images.numpy() * 65535.0).astype(np.uint16)
|
||||
input_pix_fmt = "rgba64le" if keep_channels == 4 else "rgb48le"
|
||||
else:
|
||||
data = np.rint(images.numpy() * 255.0).astype(np.uint8)
|
||||
input_pix_fmt = "rgba" if keep_channels == 4 else "rgb24"
|
||||
return data.tobytes(order="C"), input_pix_fmt, frames, width, height
|
||||
input_pix_fmt = ("rgba64le" if keep_channels == 4 else "rgb48le") if use_16bit else ("rgba" if keep_channels == 4 else "rgb24")
|
||||
|
||||
# Chroma-subsampled delivery codecs require even dimensions. VHS applies
|
||||
# replication padding for the same reason. A one-pixel edge extension is
|
||||
# preferable to failing after an expensive RTX pass.
|
||||
width = width + (width % 2)
|
||||
height = height + (height % 2)
|
||||
return images, input_pix_fmt, frames, width, height
|
||||
|
||||
|
||||
def _iter_frame_payload(
|
||||
images: torch.Tensor,
|
||||
profile: Dict[str, Any],
|
||||
width: int,
|
||||
height: int,
|
||||
) -> Iterable[bytes]:
|
||||
"""Convert one frame at a time, keeping peak host memory bounded."""
|
||||
channels = int(images.shape[-1])
|
||||
wants_alpha = bool(profile.get("alpha")) and channels == 4
|
||||
keep_channels = 4 if wants_alpha else 3
|
||||
use_16bit = int(profile.get("input_depth", 8)) >= 16
|
||||
target_dtype = np.uint16 if use_16bit else np.uint8
|
||||
target_max = 65535.0 if use_16bit else 255.0
|
||||
|
||||
for source_frame in images:
|
||||
frame = source_frame.detach().to(device="cpu", dtype=torch.float32)
|
||||
if channels == 1:
|
||||
frame = frame.repeat(1, 1, 3)
|
||||
elif channels == 2:
|
||||
frame = torch.cat((frame[..., :1], frame[..., :1], frame[..., 1:2]), dim=-1)
|
||||
frame = frame[..., :keep_channels].clamp(0.0, 1.0).numpy()
|
||||
pad_height = int(height) - int(frame.shape[0])
|
||||
pad_width = int(width) - int(frame.shape[1])
|
||||
if pad_height or pad_width:
|
||||
frame = np.pad(frame, ((0, pad_height), (0, pad_width), (0, 0)), mode="edge")
|
||||
yield np.rint(frame * target_max).astype(target_dtype, copy=False).tobytes(order="C")
|
||||
|
||||
|
||||
def _rtx_frame_tensor(images: Any) -> torch.Tensor:
|
||||
"""Return exporter frames in Comfy IMAGE layout for RTX VFX nodes."""
|
||||
"""Return exporter frames in Comfy IMAGE layout without duplicating the batch."""
|
||||
if not torch.is_tensor(images):
|
||||
images = torch.as_tensor(images)
|
||||
frames = images.detach().to(device="cpu", dtype=torch.float32)
|
||||
frames = images.detach()
|
||||
if frames.ndim == 5 and frames.shape[0] == 1:
|
||||
frames = frames[0]
|
||||
if frames.ndim != 4:
|
||||
@@ -569,7 +597,10 @@ def _rtx_frame_tensor(images: Any) -> torch.Tensor:
|
||||
frames = torch.cat((frames[..., :1], frames[..., :1], frames[..., 1:2]), dim=-1)
|
||||
elif channels > 3:
|
||||
frames = frames[..., :3]
|
||||
return frames.clamp(0.0, 1.0).contiguous()
|
||||
# Normal IMAGE/RTX output is already float RGB in [0,1]. Do not call
|
||||
# .to(cpu), clamp(), clone(), or contiguous() here: each would allocate a
|
||||
# second full-resolution video batch after FFmpeg has already completed.
|
||||
return frames
|
||||
|
||||
|
||||
def _output_path(filename_prefix: str, extension: str, width: int, height: int) -> Path:
|
||||
@@ -658,7 +689,7 @@ class IAMCCS_ShotboarderAudVidExporterPRO:
|
||||
"video_profile": (list(VIDEO_PROFILES.keys()), {"default": "prores_422_hq_mov"}),
|
||||
"audio_profile": (list(AUDIO_PROFILES.keys()), {"default": "pcm_s16le"}),
|
||||
"audio_sync": (["trim_to_video", "shortest"], {"default": "trim_to_video"}),
|
||||
"frame_rate_override": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 240.0, "step": 0.01}),
|
||||
"frame_rate_override": ("FLOAT", {"default": 24.0, "min": 0.0, "max": 240.0, "step": 0.01}),
|
||||
"video_quality": ("INT", {"default": 18, "min": 0, "max": 51, "step": 1}),
|
||||
"embed_metadata": ("BOOLEAN", {"default": True}),
|
||||
"write_sidecar": ("BOOLEAN", {"default": True}),
|
||||
@@ -673,7 +704,7 @@ class IAMCCS_ShotboarderAudVidExporterPRO:
|
||||
"rtx_megapixels": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01}),
|
||||
"rtx_width": ("INT", {"default": 1920, "min": 64, "max": 8192, "step": 8}),
|
||||
"rtx_height": ("INT", {"default": 1080, "min": 64, "max": 8192, "step": 8}),
|
||||
"rtx_divisible_by": ("STRING", {"default": "1"}),
|
||||
"rtx_divisible_by": ("STRING", {"default": "8"}),
|
||||
"rtx_device": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}),
|
||||
"rtx_ratio_preset": ("STRING", {"default": "16:9"}),
|
||||
"rtx_resize_method": ("STRING", {"default": "Center Crop (Fill)"}),
|
||||
@@ -790,7 +821,7 @@ class IAMCCS_ShotboarderAudVidExporterPRO:
|
||||
)
|
||||
else:
|
||||
export_images = source_images
|
||||
frame_bytes, input_pix_fmt, frame_count, width, height = _frame_payload(export_images, profile)
|
||||
export_images, input_pix_fmt, frame_count, width, height = _frame_payload_info(export_images, profile)
|
||||
direct_path = None
|
||||
direct_asset: Dict[str, Any] = {}
|
||||
direct_trim_start = 0.0
|
||||
@@ -877,9 +908,7 @@ class IAMCCS_ShotboarderAudVidExporterPRO:
|
||||
metadata["prompt_present"] = bool(prompt)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="iamccs_shotboarder_export_") as temp_dir:
|
||||
frames_path = Path(temp_dir) / "frames.raw"
|
||||
audio_path = Path(temp_dir) / "audio.f32le"
|
||||
frames_path.write_bytes(frame_bytes)
|
||||
if waveform is not None:
|
||||
audio_interleaved = waveform.transpose(0, 1).numpy().astype(np.float32, copy=False)
|
||||
audio_path.write_bytes(audio_interleaved.tobytes(order="C"))
|
||||
@@ -945,7 +974,7 @@ class IAMCCS_ShotboarderAudVidExporterPRO:
|
||||
command = [
|
||||
ffmpeg, "-hide_banner", "-loglevel", "error", "-y",
|
||||
"-f", "rawvideo", "-pix_fmt", input_pix_fmt,
|
||||
"-s:v", f"{width}x{height}", "-r", f"{fps:.09f}", "-i", str(frames_path),
|
||||
"-s:v", f"{width}x{height}", "-r", f"{fps:.09f}", "-i", "pipe:0",
|
||||
*audio_input_format,
|
||||
*(audio_filter_args if source_mode == "master_file_direct" else []),
|
||||
"-map", "0:v:0", *audio_map,
|
||||
@@ -958,13 +987,40 @@ class IAMCCS_ShotboarderAudVidExporterPRO:
|
||||
if str(audio_sync or "trim_to_video") == "shortest":
|
||||
command += ["-shortest"]
|
||||
command += [str(output_path)]
|
||||
result = subprocess.run(command, capture_output=True, text=True, stdin=subprocess.DEVNULL)
|
||||
if result.returncode != 0:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
write_error = None
|
||||
try:
|
||||
from comfy.utils import ProgressBar # type: ignore
|
||||
progress = ProgressBar(frame_count)
|
||||
except Exception:
|
||||
progress = None
|
||||
try:
|
||||
assert process.stdin is not None
|
||||
for index, frame_data in enumerate(_iter_frame_payload(export_images, profile, width, height), start=1):
|
||||
process.stdin.write(frame_data)
|
||||
if progress is not None:
|
||||
progress.update_absolute(index, frame_count)
|
||||
except (BrokenPipeError, OSError) as exc:
|
||||
write_error = exc
|
||||
finally:
|
||||
if process.stdin is not None:
|
||||
try:
|
||||
process.stdin.close()
|
||||
except OSError:
|
||||
pass
|
||||
detail = process.stderr.read().decode("utf-8", errors="replace") if process.stderr is not None else ""
|
||||
return_code = process.wait()
|
||||
if return_code != 0 or write_error is not None:
|
||||
try:
|
||||
output_path.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
detail = (result.stderr or result.stdout or "FFmpeg failed").strip()
|
||||
detail = (detail or str(write_error or "FFmpeg failed")).strip()
|
||||
raise RuntimeError(f"{NODE_ID}: FFmpeg export failed: {detail[-4000:]}")
|
||||
|
||||
metadata["audio_profile_effective"] = effective_audio_profile
|
||||
|
||||
Reference in New Issue
Block a user