# ruff: noqa: S102 - Use of `exec` detected # ruff: noqa: S307 - Use of possibly insecure function # region imports import ast import base64 import code import io import re import sys import textwrap import time import urllib.parse import uuid from contextlib import redirect_stderr, redirect_stdout from pathlib import Path from typing import TYPE_CHECKING, Any import folder_paths import numpy as np import rich import torch from aiohttp import web from PIL import Image from rich.console import Console from rich.traceback import Traceback if TYPE_CHECKING: from .nodes.audio import AudioTensor from .log import log from .utils import singleton # region safe imports try: import pyflakes.api import pyflakes.reporter _HAS_LINT = True except ImportError: log.error( textwrap.dedent(""" ComfyREPL: pyflakes not found. Linting will be disabled. Install with 'pip install pyflakes'. """) ) _HAS_LINT = False # --- Linting Library --- # try: # import ruff # import ruff.lint # import ruff.lint.linter # import ruff.settings # # _HAS_LINT = True # except ImportError: # print( # """ComfyREPL: ruff not found. # Linting will be disabled. # Install with 'pip install ruff'. # """ # ) # _HAS_LINT = False # --- Audio/Video Libraries --- try: import scipy.io.wavfile _HAS_SCIPY = True except ImportError: log.warning( textwrap.dedent(""" ComfyREPL: SciPy not found. Audio display will be disabled. Install with 'pip install scipy'. """) ) _HAS_SCIPY = False try: import imageio # import imageio.plugins.ffmpeg _HAS_IMAGEIO = True except ImportError: log.warning( textwrap.dedent( """ComfyREPL: Imageio or imageio-ffmpeg not found. Video display will be disabled. Install with 'pip install imageio imageio-ffmpeg' or even better use uv. """ ) ) _HAS_IMAGEIO = False # endregion # endregion # region constants # NOTE: The best way I can think of to make outputs dynamic # is to register a large number from the python side and # control their display dynamically from js (based on the dynamic inputs) # but this is hacky and I would rather wait for V3 to start doing that. SOCKET_COUNT = 5 # endregion # region media_handlers class _AudioDisplay: def __init__(self, samples, sample_rate): if not _HAS_SCIPY: raise ImportError("Audio display requires scipy and numpy.") if not isinstance(samples, np.ndarray | torch.Tensor): raise TypeError( "Audio samples must be a numpy array or torch tensor." ) if isinstance(samples, torch.Tensor): samples = samples.detach().cpu().numpy() # Ensure samples are in a format scipy.io.wavfile # can handle (e.g., int16, float32) if samples.dtype == np.float64: samples = samples.astype(np.float32) elif samples.dtype == np.int64: # Or scale to int32 if range requires samples = samples.astype(np.int16) self.samples = samples self.sample_rate = sample_rate def _to_wav_base64(self): buffer = io.BytesIO() try: scipy.io.wavfile.write(buffer, self.sample_rate, self.samples) audio_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") return audio_base64 except Exception as e: return f"
Error encoding audio: {e}
" def _repr_html_(self): base64_data = self._to_wav_base64() if base64_data.startswith("""" def render_audio(samples: np.ndarray | torch.Tensor, sample_rate: int): """ Render audio samples as an HTML audio player. Args: samples: Audio samples. sample_rate: Sample rate in Hz. Returns ------- AudioDisplay: An object that will render as an HTML audio player. """ return _AudioDisplay(samples, sample_rate) class _VideoDisplay: def __init__( self, frames, fps=24, options=None, audio: "AudioTensor | None" = None ): if not _HAS_IMAGEIO: raise ImportError( textwrap.dedent( """Video display requires imageio, imageio-ffmpeg, and image libraries (numpy, Pillow, torch). """ ) ) self.frames = [] for frame in frames: if isinstance(frame, Image.Image): self.frames.append(np.array(frame)) elif isinstance(frame, np.ndarray): # ensure HWC and uint8 # CHW if frame.ndim == 3 and frame.shape[0] in [1, 3, 4]: frame = np.transpose(frame, (1, 2, 0)) if frame.dtype != np.uint8: frame = ( (frame * 255).astype(np.uint8) if frame.max() <= 1.0 else frame.astype(np.uint8) ) self.frames.append(frame) elif isinstance(frame, torch.Tensor): np_frame = frame.detach().cpu().numpy() # CHW if np_frame.ndim == 3 and np_frame.shape[0] in [ 1, 3, 4, ]: np_frame = np.transpose(np_frame, (1, 2, 0)) if np_frame.dtype != np.uint8: np_frame = ( (np_frame * 255).astype(np.uint8) if np_frame.max() <= 1.0 else np_frame.astype(np.uint8) ) self.frames.append(np_frame) else: raise TypeError( textwrap.dedent(f""" Unsupported frame type: {type(frame)}. Must be PIL.Image, numpy.ndarray, or torch.Tensor. """) ) self.fps = fps self.options = options if options is not None else {} self.audio_array = None self.audio_samplerate = None if audio: if ( isinstance(audio, dict) and "waveform" in audio and "sample_rate" in audio ): waveform_tensor = audio["waveform"] self.audio_samplerate = audio["sample_rate"] audio_np = waveform_tensor.detach().cpu().numpy() if audio_np.ndim == 1: audio_np = audio_np[:, np.newaxis] elif ( audio_np.ndim > 1 and audio_np.shape[0] < audio_np.shape[1] ): audio_np = np.transpose(audio_np) self.audio_array = audio_np else: log.warning("Audio provided in an incorrect format, ignoring.") def _save_to_disk_and_get_url(self): """Saves the video to a temp file and returns its URL.""" output_dir = Path(folder_paths.get_temp_directory(), "mtb_repl_videos") output_dir.mkdir(exist_ok=True) filename = f"vid_{time.strftime('%Y%m%d-%H%M%S')}_{uuid.uuid4()}.mp4" filepath = output_dir / filename try: imageio.mimwrite( uri=filepath, ims=self.frames, format="mp4", fps=self.fps, codec="libx264", quality=8, audio_array=self.audio_array, audio_samplerate=self.audio_samplerate, audio_codec="aac", ) params = urllib.parse.urlencode( { "filename": filename, "subfolder": "mtb_repl_videos", "type": "temp", } ) return f"/view?{params}" except Exception as e: return f"
Error saving video to disk: {e}
" def _to_mp4_base64(self): """[DEPRECATED] this bloats the client""" buffer = io.BytesIO() try: imageio.mimwrite( uri=buffer, ims=self.frames, format="mp4", fps=self.fps, codec="libx264", quality=8, ) video_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8") return video_base64 except Exception as e: return f"
Error encoding video: {e}
" def _repr_html_(self, *, old=False): if old: base64_data = self._to_mp4_base64() if base64_data.startswith("""" video_url = self._save_to_disk_and_get_url() if video_url.startswith(" """ def render_video( frames: torch.Tensor | list[np.ndarray] | list[Image.Image] | Any, fps=24, options=None, *, audio: "AudioTensor | None" = None, ): """ Render video frames as an HTML video player. Args: frames: A list of frames, or a single batch tensor/array (B, H, W, C) or (B, C, H, W). fps (int): Frames per second. options (dict): Dictionary of HTML