# 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