diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 608a0edc4..68f374747 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -83,28 +83,6 @@ jobs: cache-from: type=registry,ref=livepeer/comfyui-base:build-cache cache-to: type=registry,mode=max,ref=livepeer/comfyui-base:build-cache - trigger: - name: Trigger ai-runner workflow - needs: base - if: ${{ github.repository == 'livepeer/comfystream' }} - runs-on: ubuntu-latest - steps: - - name: Send workflow dispatch event to ai-runner - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.CI_GITHUB_TOKEN }} - script: | - await github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: "ai-runner", - workflow_id: "comfyui-trigger.yaml", - ref: "main", - inputs: { - "comfyui-base-digest": "${{ needs.base.outputs.image-digest }}", - "triggering-branch": "${{ github.head_ref || github.ref_name }}", - }, - }); - comfystream: name: comfystream image needs: base diff --git a/.vscode/launch.json b/.vscode/launch.json index 540197b14..4d442c585 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,10 +31,40 @@ "--media-ports=5678", "--host=0.0.0.0", "--port=8889", - "--log-level=DEBUG", + "--log-level=INFO", + "--comfyui-inference-log-level=DEBUG", ], - "python": "/workspace/miniconda3/envs/comfystream/bin/python", - "justMyCode": true + "justMyCode": true, + "python": "${command:python.interpreterPath}" + }, + { + "name": "Run ComfyStream BYOC", + "type": "debugpy", + "request": "launch", + "cwd": "/workspace/ComfyUI", + "program": "/workspace/comfystream/server/byoc.py", + "console": "integratedTerminal", + "args": [ + "--workspace=/workspace/ComfyUI", + "--host=0.0.0.0", + "--port=8000", + "--log-level=INFO", + "--comfyui-inference-log-level=DEBUG", + "--width=512", + "--height=512" + ], + "env": { + "ORCH_URL": "https://172.17.0.1:9995", + "ORCH_SECRET": "orch-secret", + "CAPABILITY_NAME": "comfystream-byoc-processor", + "CAPABILITY_DESCRIPTION": "ComfyUI streaming processor for BYOC mode", + "CAPABILITY_URL": "http://172.17.0.1:8000", + "CAPABILITY_PRICE_PER_UNIT": "0", + "CAPABILITY_PRICE_SCALING": "1", + "CAPABILITY_CAPACITY": "1" + }, + "justMyCode": true, + "python": "${command:python.interpreterPath}" }, { "name": "Run ComfyStream UI (Node.js)", diff --git a/docker/Dockerfile b/docker/Dockerfile index 0e6baecf7..1181edc2a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,9 +2,15 @@ ARG BASE_IMAGE=livepeer/comfyui-base:latest FROM ${BASE_IMAGE} -ENV PATH="/workspace/miniconda3/bin:${PATH}" \ +# Ensure Bash is the default shell (inherited from base) +SHELL ["/bin/bash", "-c"] + +ENV PATH="/workspace/.venv/bin:/root/.local/bin:${PATH}" \ NVM_DIR=/root/.nvm \ - NODE_VERSION=18.18.0 + NODE_VERSION=18.18.0 \ + VIRTUAL_ENV="/workspace/.venv" \ + PYTHONPATH="/workspace/.venv/lib/python3.12/site-packages" \ + SHELL="/bin/bash" RUN echo "Using base image: ${BASE_IMAGE}" && \ apt update && \ diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index c8f6b7ff1..e0a7d713a 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -1,21 +1,25 @@ ARG BASE_IMAGE=nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04 \ - CONDA_VERSION=latest \ PYTHON_VERSION=3.12 FROM "${BASE_IMAGE}" -ARG CONDA_VERSION \ - PYTHON_VERSION +ARG PYTHON_VERSION + +# Set Bash as the default shell +SHELL ["/bin/bash", "-c"] ENV DEBIAN_FRONTEND=noninteractive \ - CONDA_VERSION="${CONDA_VERSION}" \ - PATH="/workspace/miniconda3/bin:${PATH}" \ - PYTHON_VERSION="${PYTHON_VERSION}" + PATH="/workspace/.venv/bin:/root/.local/bin:/usr/local/bin:${PATH}" \ + PYTHON_VERSION="${PYTHON_VERSION}" \ + VIRTUAL_ENV="/workspace/.venv" \ + PYTHONPATH="/workspace/.venv/lib/python3.12/site-packages" \ + SHELL="/bin/bash" # System dependencies RUN apt update && apt install -yqq --no-install-recommends \ git \ wget \ + curl \ nano \ socat \ libsndfile1 \ @@ -27,6 +31,11 @@ RUN apt update && apt install -yqq --no-install-recommends \ swig \ libprotobuf-dev \ protobuf-compiler \ + ffmpeg \ + python3 \ + python3-pip \ + python3-venv \ + curl \ && rm -rf /var/lib/apt/lists/* #enable opengl support with nvidia gpu @@ -38,59 +47,63 @@ RUN printf '%s\n' \ ' }' \ '}' > /usr/share/glvnd/egl_vendor.d/10_nvidia.json -# Conda setup +# Python setup and uv installation RUN mkdir -p /workspace/comfystream && \ - wget "https://repo.anaconda.com/miniconda/Miniconda3-${CONDA_VERSION}-Linux-x86_64.sh" -O /tmp/miniconda.sh && \ - bash /tmp/miniconda.sh -b -p /workspace/miniconda3 && \ - eval "$(/workspace/miniconda3/bin/conda shell.bash hook)" && \ - /workspace/miniconda3/bin/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main && \ - /workspace/miniconda3/bin/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r && \ - conda create -n comfystream python="${PYTHON_VERSION}" ffmpeg=6 -c conda-forge -y && \ - rm /tmp/miniconda.sh && echo 'export LD_LIBRARY_PATH=/workspace/miniconda3/envs/comfystream/lib:$LD_LIBRARY_PATH' >> ~/.bashrc + python3 -m pip install --upgrade pip uv && \ + cd /workspace && \ + uv venv .venv && \ + echo 'activate_venv() { if [ -f ".venv/bin/activate" ] && [ "$VIRTUAL_ENV" != "$(pwd)/.venv" ]; then . ".venv/bin/activate"; fi; };' >> ~/.bashrc && \ + echo 'PROMPT_COMMAND="activate_venv; $PROMPT_COMMAND"' >> ~/.bashrc + +# Ensure pip exists inside the venv for tools that shell out to `python -m pip` +RUN /workspace/.venv/bin/python -m ensurepip --upgrade && \ + /workspace/.venv/bin/python -m pip install -U pip setuptools wheel && \ + /workspace/.venv/bin/python -m pip --version -RUN conda run -n comfystream --no-capture-output pip install --upgrade pip && \ -conda run -n comfystream --no-capture-output pip install wheel +# Install comfy-cli using uv +RUN uv pip install comfy-cli # Copy only files needed for setup COPY ./src/comfystream/scripts /workspace/comfystream/src/comfystream/scripts COPY ./configs /workspace/comfystream/configs -# Clone ComfyUI -RUN git clone --branch v0.3.56 --depth 1 https://github.com/comfyanonymous/ComfyUI.git /workspace/ComfyUI +RUN mkdir -p /workspace + +# Install ComfyUI using comfy-cli +RUN comfy --skip-prompt --workspace /workspace/ComfyUI install --skip-torch-or-directml --nvidia # Copy ComfyStream files into ComfyUI COPY . /workspace/comfystream -RUN conda run -n comfystream --cwd /workspace/comfystream --no-capture-output pip install -r ./src/comfystream/scripts/constraints.txt +RUN cd /workspace/comfystream && uv pip install -r ./src/comfystream/scripts/overrides.txt # Copy comfystream and example workflows to ComfyUI COPY ./workflows/comfyui/* /workspace/ComfyUI/user/default/workflows/ COPY ./test/example-512x512.png /workspace/ComfyUI/input -# Install ComfyUI requirements -RUN conda run -n comfystream --no-capture-output --cwd /workspace/ComfyUI pip install -r requirements.txt --root-user-action=ignore +# Install ComfyUI requirements (probably not needed) +# RUN cd /workspace/ComfyUI && uv pip install -r requirements.txt # Install ComfyStream requirements RUN ln -s /workspace/comfystream /workspace/ComfyUI/custom_nodes/comfystream -RUN conda run -n comfystream --no-capture-output --cwd /workspace/comfystream pip install -e . --root-user-action=ignore -RUN conda run -n comfystream --no-capture-output --cwd /workspace/comfystream python install.py --workspace /workspace/ComfyUI +RUN cd /workspace/comfystream && uv pip install -e . +RUN cd /workspace/comfystream && python install.py --workspace /workspace/ComfyUI # Accept a build-arg that lets CI force-invalidate setup_nodes.py ARG CACHEBUST=static ENV CACHEBUST=${CACHEBUST} -# Run setup_nodes -RUN conda run -n comfystream --no-capture-output --cwd /workspace/comfystream python src/comfystream/scripts/setup_nodes.py --workspace /workspace/ComfyUI - -RUN conda run -n comfystream --no-capture-output pip install "numpy<2.0.0" +# Run setup_nodes (removing to promote user node installation) +#RUN cd /workspace/comfystream && python src/comfystream/scripts/setup_nodes.py --workspace /workspace/ComfyUI -RUN conda run -n comfystream --no-capture-output pip install --no-cache-dir xformers==0.0.30 --no-deps +#RUN uv pip install "numpy<2.0.0" +#RUN uv pip install --no-cache-dir xformers==0.0.30 --no-deps -# Configure no environment activation by default -RUN conda config --set auto_activate_base false && \ - conda init bash +# Set default Python environment with auto-uv-env and ensure .bashrc is sourced +RUN echo 'source ~/.bashrc' >> ~/.bash_profile && \ + echo 'if [ -f ~/.bashrc ]; then source ~/.bashrc; fi' >> ~/.profile -# Set comfystream environment as default -RUN echo "conda activate comfystream" >> ~/.bashrc +# Ensure bash is used by default for interactive shells +ENV BASH_ENV=/root/.bashrc WORKDIR /workspace/comfystream diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index ef55c77e2..cbe96424b 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,7 +1,6 @@ #!/bin/bash set -e -eval "$(conda shell.bash hook)" # Add help command to show usage show_help() { @@ -76,8 +75,7 @@ fi if [ "$1" = "--download-models" ]; then cd /workspace/comfystream - conda activate comfystream - python src/comfystream/scripts/setup_models.py --workspace /workspace/ComfyUI + uv run setup-models --workspace /workspace/ComfyUI shift fi @@ -89,19 +87,18 @@ FASTERLIVEPORTRAIT_DIR="/workspace/ComfyUI/models/liveportrait_onnx" if [ "$1" = "--build-engines" ]; then cd /workspace/comfystream - conda activate comfystream # Build Static Engine for Dreamshaper - Square (512x512) - python src/comfystream/scripts/build_trt.py --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors --out-engine /workspace/ComfyUI/output/tensorrt/static-dreamshaper8_SD15_\$stat-b-1-h-512-w-512_00001_.engine --width 512 --height 512 + uv run python src/comfystream/scripts/build_trt.py --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors --out-engine /workspace/ComfyUI/output/tensorrt/static-dreamshaper8_SD15_\$stat-b-1-h-512-w-512_00001_.engine --width 512 --height 512 # Build Static Engine for Dreamshaper - Portrait (384x704) - python src/comfystream/scripts/build_trt.py --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors --out-engine /workspace/ComfyUI/output/tensorrt/static-dreamshaper8_SD15_\$stat-b-1-h-704-w-384_00001_.engine --width 384 --height 704 + uv run python src/comfystream/scripts/build_trt.py --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors --out-engine /workspace/ComfyUI/output/tensorrt/static-dreamshaper8_SD15_\$stat-b-1-h-704-w-384_00001_.engine --width 384 --height 704 # Build Static Engine for Dreamshaper - Landscape (704x384) - python src/comfystream/scripts/build_trt.py --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors --out-engine /workspace/ComfyUI/output/tensorrt/static-dreamshaper8_SD15_\$stat-b-1-h-384-w-704_00001_.engine --width 704 --height 384 + uv run python src/comfystream/scripts/build_trt.py --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors --out-engine /workspace/ComfyUI/output/tensorrt/static-dreamshaper8_SD15_\$stat-b-1-h-384-w-704_00001_.engine --width 704 --height 384 # Build Dynamic Engine for Dreamshaper - python src/comfystream/scripts/build_trt.py \ + uv run python src/comfystream/scripts/build_trt.py \ --model /workspace/ComfyUI/models/unet/dreamshaper-8-dmd-1kstep.safetensors \ --out-engine /workspace/ComfyUI/output/tensorrt/dynamic-dreamshaper8_SD15_\$dyn-b-1-4-2-h-512-704-w-320-384-448_00001_.engine \ --width 384 \ @@ -117,7 +114,7 @@ if [ "$1" = "--build-engines" ]; then mkdir -p "$DEPTH_ANYTHING_DIR" fi cd "$DEPTH_ANYTHING_DIR" - python /workspace/ComfyUI/custom_nodes/ComfyUI-Depth-Anything-Tensorrt/export_trt.py + uv run python /workspace/ComfyUI/custom_nodes/ComfyUI-Depth-Anything-Tensorrt/export_trt.py else echo "Engine for DepthAnything2 already exists at ${DEPTH_ANYTHING_DIR}/${DEPTH_ANYTHING_ENGINE}, skipping..." fi @@ -125,7 +122,7 @@ if [ "$1" = "--build-engines" ]; then # Build Engine for Depth Anything2 (large) if [ ! -f "$DEPTH_ANYTHING_DIR/$DEPTH_ANYTHING_ENGINE_LARGE" ]; then cd "$DEPTH_ANYTHING_DIR" - python /workspace/ComfyUI/custom_nodes/ComfyUI-Depth-Anything-Tensorrt/export_trt.py --trt-path "${DEPTH_ANYTHING_DIR}/${DEPTH_ANYTHING_ENGINE_LARGE}" --onnx-path "${DEPTH_ANYTHING_DIR}/depth_anything_v2_vitl.onnx" + uv run python /workspace/ComfyUI/custom_nodes/ComfyUI-Depth-Anything-Tensorrt/export_trt.py --trt-path "${DEPTH_ANYTHING_DIR}/${DEPTH_ANYTHING_ENGINE_LARGE}" --onnx-path "${DEPTH_ANYTHING_DIR}/depth_anything_v2_vitl.onnx" else echo "Engine for DepthAnything2 (large) already exists at ${DEPTH_ANYTHING_DIR}/${DEPTH_ANYTHING_ENGINE_LARGE}, skipping..." fi @@ -138,7 +135,7 @@ if [ "$1" = "--build-engines" ]; then for model in $MODELS; do for timestep in $TIMESTEPS; do echo "Building model=$model with timestep=$timestep" - python build_tensorrt.py \ + uv run python build_tensorrt.py \ --model-id "$model" \ --timesteps "$timestep" \ --engine-dir $TENSORRT_DIR/StreamDiffusion-engines @@ -152,8 +149,7 @@ fi if [ "$1" = "--opencv-cuda" ]; then cd /workspace/comfystream - conda activate comfystream - + # Check if OpenCV CUDA build already exists if [ ! -f "/workspace/comfystream/opencv-cuda-release.tar.gz" ]; then # Download and extract OpenCV CUDA build @@ -176,18 +172,18 @@ if [ "$1" = "--opencv-cuda" ]; then libswscale-dev # Remove existing cv2 package - SITE_PACKAGES_DIR="/workspace/miniconda3/envs/comfystream/lib/python3.12/site-packages" + SITE_PACKAGES_DIR="$(uv python dir --bin)/lib/python3.12/site-packages" rm -rf "${SITE_PACKAGES_DIR}/cv2"* # Copy new cv2 package cp -r /workspace/comfystream/cv2 "${SITE_PACKAGES_DIR}/" # Handle library dependencies - CONDA_ENV_LIB="/workspace/miniconda3/envs/comfystream/lib" - + UV_ENV_LIB="$(uv python dir --bin)/lib" + # Remove existing libstdc++ and copy system one - rm -f "${CONDA_ENV_LIB}/libstdc++.so"* - cp /usr/lib/x86_64-linux-gnu/libstdc++.so* "${CONDA_ENV_LIB}/" + rm -f "${UV_ENV_LIB}/libstdc++.so"* + cp /usr/lib/x86_64-linux-gnu/libstdc++.so* "${UV_ENV_LIB}/" # Copy OpenCV libraries cp /workspace/comfystream/opencv/build/lib/libopencv_* /usr/lib/x86_64-linux-gnu/ diff --git a/nodes/audio_utils/load_audio_tensor.py b/nodes/audio_utils/load_audio_tensor.py index 52fa6fa37..eed09eea9 100644 --- a/nodes/audio_utils/load_audio_tensor.py +++ b/nodes/audio_utils/load_audio_tensor.py @@ -1,53 +1,88 @@ import numpy as np - +import torch +import queue from comfystream import tensor_cache +from comfystream.exceptions import ComfyStreamInputTimeoutError, ComfyStreamAudioBufferError + class LoadAudioTensor: - CATEGORY = "audio_utils" - RETURN_TYPES = ("WAVEFORM", "INT") - RETURN_NAMES = ("audio", "sample_rate") + CATEGORY = "ComfyStream/Loaders" + RETURN_TYPES = ("AUDIO",) + RETURN_NAMES = ("audio",) FUNCTION = "execute" + DESCRIPTION = "Load audio tensor from ComfyStream input with timeout." def __init__(self): self.audio_buffer = np.empty(0, dtype=np.int16) self.buffer_samples = None self.sample_rate = None + self.leftover = np.empty(0, dtype=np.int16) @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { "required": { - "buffer_size": ("FLOAT", {"default": 500.0}), + "buffer_size": ("FLOAT", { + "default": 500.0, + "tooltip": "Audio buffer size in milliseconds" + }), + }, + "optional": { + "timeout_seconds": ("FLOAT", { + "default": 1.0, + "min": 0.1, + "max": 30.0, + "step": 0.1, + "tooltip": "Timeout in seconds" + }), } } @classmethod - def IS_CHANGED(): + def IS_CHANGED(cls, **kwargs): return float("nan") - - def execute(self, buffer_size): + + def execute(self, buffer_size: float, timeout_seconds: float = 1.0): + # Initialize if needed if self.sample_rate is None or self.buffer_samples is None: - frame = tensor_cache.audio_inputs.get(block=True) - self.sample_rate = frame.sample_rate - self.buffer_samples = int(self.sample_rate * buffer_size / 1000) - self.leftover = frame.side_data.input + try: + frame = tensor_cache.audio_inputs.get(block=True, timeout=timeout_seconds) + self.sample_rate = frame.sample_rate + self.buffer_samples = int(self.sample_rate * buffer_size / 1000) + self.leftover = frame.side_data.input + except queue.Empty: + raise ComfyStreamInputTimeoutError("audio", timeout_seconds) - if self.leftover.shape[0] < self.buffer_samples: + # Use leftover data if available + if self.leftover.shape[0] >= self.buffer_samples: + buffered_audio = self.leftover[:self.buffer_samples] + self.leftover = self.leftover[self.buffer_samples:] + else: + # Collect more audio chunks chunks = [self.leftover] if self.leftover.size > 0 else [] total_samples = self.leftover.shape[0] while total_samples < self.buffer_samples: - frame = tensor_cache.audio_inputs.get(block=True) - if frame.sample_rate != self.sample_rate: - raise ValueError("Sample rate mismatch") - chunks.append(frame.side_data.input) - total_samples += frame.side_data.input.shape[0] + try: + frame = tensor_cache.audio_inputs.get(block=True, timeout=timeout_seconds) + if frame.sample_rate != self.sample_rate: + raise ValueError(f"Sample rate mismatch: expected {self.sample_rate}Hz, got {frame.sample_rate}Hz") + chunks.append(frame.side_data.input) + total_samples += frame.side_data.input.shape[0] + except queue.Empty: + raise ComfyStreamAudioBufferError(timeout_seconds, self.buffer_samples, total_samples) merged_audio = np.concatenate(chunks, dtype=np.int16) buffered_audio = merged_audio[:self.buffer_samples] - self.leftover = merged_audio[self.buffer_samples:] - else: - buffered_audio = self.leftover[:self.buffer_samples] - self.leftover = self.leftover[self.buffer_samples:] + self.leftover = merged_audio[self.buffer_samples:] if merged_audio.shape[0] > self.buffer_samples else np.empty(0, dtype=np.int16) - return buffered_audio, self.sample_rate + # Convert to ComfyUI AUDIO format + waveform_tensor = torch.from_numpy(buffered_audio.astype(np.float32) / 32768.0) + + # Ensure proper tensor shape: (batch, channels, samples) + if waveform_tensor.dim() == 1: + waveform_tensor = waveform_tensor.unsqueeze(0).unsqueeze(0) + elif waveform_tensor.dim() == 2: + waveform_tensor = waveform_tensor.unsqueeze(0) + + return ({"waveform": waveform_tensor, "sample_rate": self.sample_rate},) \ No newline at end of file diff --git a/nodes/audio_utils/pitch_shift.py b/nodes/audio_utils/pitch_shift.py index ed2b2b383..2fba9ee59 100644 --- a/nodes/audio_utils/pitch_shift.py +++ b/nodes/audio_utils/pitch_shift.py @@ -1,17 +1,17 @@ import numpy as np import librosa +import torch class PitchShifter: CATEGORY = "audio_utils" - RETURN_TYPES = ("WAVEFORM", "INT") + RETURN_TYPES = ("AUDIO",) FUNCTION = "execute" @classmethod def INPUT_TYPES(cls): return { "required": { - "audio": ("WAVEFORM",), - "sample_rate": ("INT",), + "audio": ("AUDIO",), "pitch_shift": ("FLOAT", { "default": 4.0, "min": 0.0, @@ -25,8 +25,41 @@ def INPUT_TYPES(cls): def IS_CHANGED(cls): return float("nan") - def execute(self, audio, sample_rate, pitch_shift): - audio_float = audio.astype(np.float32) / 32768.0 - shifted_audio = librosa.effects.pitch_shift(y=audio_float, sr=sample_rate, n_steps=pitch_shift) - shifted_int16 = np.clip(shifted_audio * 32768.0, -32768, 32767).astype(np.int16) - return shifted_int16, sample_rate + def execute(self, audio, pitch_shift): + # Extract waveform and sample rate from AUDIO format + waveform = audio["waveform"] + sample_rate = audio["sample_rate"] + + # Convert tensor to numpy and ensure proper format for librosa + if isinstance(waveform, torch.Tensor): + audio_numpy = waveform.squeeze().cpu().numpy() + else: + audio_numpy = waveform.squeeze() + + # Ensure float32 format and proper normalization for librosa processing + if audio_numpy.dtype != np.float32: + audio_numpy = audio_numpy.astype(np.float32) + + # Check if data needs normalization (librosa expects [-1, 1] range) + max_abs_val = np.abs(audio_numpy).max() + if max_abs_val > 1.0: + # Data appears to be in int16 range, normalize it + audio_numpy = audio_numpy / 32768.0 + + # Apply pitch shift + shifted_audio = librosa.effects.pitch_shift(y=audio_numpy, sr=sample_rate, n_steps=pitch_shift) + + # Convert back to tensor and restore original shape + shifted_tensor = torch.from_numpy(shifted_audio).float() + if waveform.dim() == 3: # (batch, channels, samples) + shifted_tensor = shifted_tensor.unsqueeze(0).unsqueeze(0) + elif waveform.dim() == 2: # (channels, samples) + shifted_tensor = shifted_tensor.unsqueeze(0) + + # Return AUDIO format + result_audio = { + "waveform": shifted_tensor, + "sample_rate": sample_rate + } + + return (result_audio,) diff --git a/nodes/audio_utils/save_audio_tensor.py b/nodes/audio_utils/save_audio_tensor.py index 6f86b57c3..6b7b0281c 100644 --- a/nodes/audio_utils/save_audio_tensor.py +++ b/nodes/audio_utils/save_audio_tensor.py @@ -1,3 +1,4 @@ +import numpy as np from comfystream import tensor_cache class SaveAudioTensor: @@ -11,7 +12,7 @@ class SaveAudioTensor: def INPUT_TYPES(s): return { "required": { - "audio": ("WAVEFORM",) + "audio": ("AUDIO",) } } @@ -20,5 +21,26 @@ def IS_CHANGED(s): return float("nan") def execute(self, audio): - tensor_cache.audio_outputs.put_nowait(audio) + # Extract waveform tensor from AUDIO format + waveform = audio["waveform"] + + # Convert to numpy and flatten for pipeline compatibility + if hasattr(waveform, 'cpu'): + # PyTorch tensor + waveform_numpy = waveform.squeeze().cpu().numpy() + else: + # Already numpy + waveform_numpy = waveform.squeeze() + + # Ensure 1D array for pipeline buffer concatenation + if waveform_numpy.ndim > 1: + waveform_numpy = waveform_numpy.flatten() + + # Convert to int16 if needed (pipeline expects int16) + if waveform_numpy.dtype == np.float32: + waveform_numpy = (waveform_numpy * 32767).astype(np.int16) + elif waveform_numpy.dtype != np.int16: + waveform_numpy = waveform_numpy.astype(np.int16) + + tensor_cache.audio_outputs.put_nowait(waveform_numpy) return (audio,) diff --git a/nodes/tensor_utils/load_tensor.py b/nodes/tensor_utils/load_tensor.py index c39fe8a1d..9923a996a 100644 --- a/nodes/tensor_utils/load_tensor.py +++ b/nodes/tensor_utils/load_tensor.py @@ -1,20 +1,37 @@ +import torch +import queue from comfystream import tensor_cache +from comfystream.exceptions import ComfyStreamInputTimeoutError class LoadTensor: - CATEGORY = "tensor_utils" + CATEGORY = "ComfyStream/Loaders" RETURN_TYPES = ("IMAGE",) FUNCTION = "execute" + DESCRIPTION = "Load image tensor from ComfyStream input with timeout." @classmethod - def INPUT_TYPES(s): - return {} + def INPUT_TYPES(cls): + return { + "optional": { + "timeout_seconds": ("FLOAT", { + "default": 1.0, + "min": 0.1, + "max": 30.0, + "step": 0.1, + "tooltip": "Timeout in seconds" + }), + } + } @classmethod - def IS_CHANGED(): + def IS_CHANGED(cls, **kwargs): return float("nan") - def execute(self): - frame = tensor_cache.image_inputs.get(block=True) - frame.side_data.skipped = False - return (frame.side_data.input,) + def execute(self, timeout_seconds: float = 1.0): + try: + frame = tensor_cache.image_inputs.get(block=True, timeout=timeout_seconds) + frame.side_data.skipped = False + return (frame.side_data.input,) + except queue.Empty: + raise ComfyStreamInputTimeoutError("video", timeout_seconds) diff --git a/nodes/tensor_utils/save_text_tensor.py b/nodes/tensor_utils/save_text_tensor.py index 525f2a1b9..098887e07 100644 --- a/nodes/tensor_utils/save_text_tensor.py +++ b/nodes/tensor_utils/save_text_tensor.py @@ -18,7 +18,7 @@ def INPUT_TYPES(s): } @classmethod - def IS_CHANGED(s): + def IS_CHANGED(s, **kwargs): return float("nan") def execute(self, data, remove_linebreaks=True): diff --git a/pyproject.toml b/pyproject.toml index 8cc486456..0c09d8cc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,21 +7,15 @@ name = "comfystream" description = "Build Live AI Video with ComfyUI" version = "0.1.5" license = { file = "LICENSE" } -dependencies = [ - "asyncio", - "comfyui @ git+https://github.com/hiddenswitch/ComfyUI.git@58622c7e91cb5cc2bca985d713db55e5681ff316", - "aiortc", - "aiohttp", - "aiohttp_cors", - "toml", - "twilio", - "prometheus_client", - "librosa" -] +dynamic = ["dependencies"] [project.optional-dependencies] dev = ["pytest", "pytest-cov"] +[project.scripts] +setup-nodes = "comfystream.scripts.setup_nodes:setup_nodes" +setup-models = "comfystream.scripts.setup_models:setup_models" + [project.urls] Repository = "https://github.com/yondonfu/comfystream" diff --git a/requirements.txt b/requirements.txt index 7ff3310b6..790900bb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ asyncio +pytrickle @ git+https://github.com/livepeer/pytrickle.git@de37bea74679fa5db46b656a83c9b7240fc597b6 comfyui @ git+https://github.com/hiddenswitch/ComfyUI.git@58622c7e91cb5cc2bca985d713db55e5681ff316 aiortc aiohttp diff --git a/server/app.py b/server/app.py index ecf5751f4..c07065695 100644 --- a/server/app.py +++ b/server/app.py @@ -29,7 +29,7 @@ from aiortc.rtcrtpsender import RTCRtpSender from comfystream.pipeline import Pipeline from twilio.rest import Client -from comfystream.server.utils import patch_loop_datagram, add_prefix_to_app_routes, FPSMeter +from comfystream.server.utils import patch_loop_datagram, add_prefix_to_app_routes, FPSMeter, ComfyStreamTimeoutFilter from comfystream.server.metrics import MetricsManager, StreamStatsManager import time @@ -40,6 +40,7 @@ MAX_BITRATE = 2000000 MIN_BITRATE = 2000000 +TEXT_POLL_INTERVAL = 0.25 # Interval in seconds to poll for text outputs class VideoStreamTrack(MediaStreamTrack): @@ -390,11 +391,11 @@ async def forward_text(): try: while channel.readyState == "open": try: - # Use timeout to prevent indefinite blocking - text = await asyncio.wait_for( - pipeline.get_text_output(), - timeout=1.0 # Check every second if channel is still open - ) + # Non-blocking poll; sleep if no text to avoid tight loop + text = await pipeline.get_text_output() + if text is None or text.strip() == "": + await asyncio.sleep(TEXT_POLL_INTERVAL) + continue if channel.readyState == "open": # Send as JSON string for extensibility try: @@ -402,9 +403,6 @@ async def forward_text(): except Exception as e: logger.debug(f"[TextChannel] Send failed, stopping forwarder: {e}") break - except asyncio.TimeoutError: - # No text available, continue checking - continue except asyncio.CancelledError: logger.debug("[TextChannel] Forward text task cancelled") break @@ -696,6 +694,10 @@ def force_print(*args, **kwargs): if args.comfyui_log_level: log_level = logging._nameToLevel.get(args.comfyui_log_level.upper()) logging.getLogger("comfy").setLevel(log_level) + + # Add ComfyStream timeout filter to suppress verbose execution logging + logging.getLogger("comfy.cmd.execution").addFilter(ComfyStreamTimeoutFilter()) + logging.getLogger("comfy").addFilter(ComfyStreamTimeoutFilter()) if args.comfyui_inference_log_level: app["comfui_inference_log_level"] = args.comfyui_inference_log_level diff --git a/server/byoc.py b/server/byoc.py new file mode 100644 index 000000000..19a166be8 --- /dev/null +++ b/server/byoc.py @@ -0,0 +1,216 @@ +import argparse +import asyncio +import logging +import os +import sys + +import torch +# Initialize CUDA before any other imports to prevent core dump. +if torch.cuda.is_available(): + torch.cuda.init() + +from aiohttp import web +from pytrickle.stream_processor import StreamProcessor +from pytrickle.utils.register import RegisterCapability +from pytrickle.frame_skipper import FrameSkipConfig +from frame_processor import ComfyStreamFrameProcessor +from comfystream.server.utils import ComfyStreamTimeoutFilter + +logger = logging.getLogger(__name__) + + +async def register_orchestrator(orch_url=None, orch_secret=None, capability_name=None, host="127.0.0.1", port=8889): + """Register capability with orchestrator if configured.""" + try: + orch_url = orch_url or os.getenv("ORCH_URL") + orch_secret = orch_secret or os.getenv("ORCH_SECRET") + + if orch_url and orch_secret: + os.environ.update({ + "CAPABILITY_NAME": capability_name or os.getenv("CAPABILITY_NAME") or "comfystream-processor", + "CAPABILITY_DESCRIPTION": "ComfyUI streaming processor", + "CAPABILITY_URL": f"http://{host}:{port}", + "CAPABILITY_CAPACITY": "1", + "ORCH_URL": orch_url, + "ORCH_SECRET": orch_secret + }) + + # Pass through explicit capability_name to ensure CLI/env override takes effect + result = await RegisterCapability.register( + logger=logger, + capability_name=capability_name + ) + if result: + logger.info(f"Registered capability: {result.geturl()}") + except Exception as e: + logger.error(f"Orchestrator registration failed: {e}") + + +def main(): + parser = argparse.ArgumentParser( + description="Run comfystream server in BYOC (Bring Your Own Compute) mode using pytrickle." + ) + parser.add_argument("--port", default=8889, help="Set the server port") + parser.add_argument("--host", default="127.0.0.1", help="Set the host") + parser.add_argument( + "--workspace", default=None, required=True, help="Set Comfy workspace" + ) + parser.add_argument( + "--log-level", + default="INFO", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set the logging level", + ) + parser.add_argument( + "--comfyui-log-level", + default=None, + choices=logging._nameToLevel.keys(), + help="Set the global logging level for ComfyUI", + ) + parser.add_argument( + "--comfyui-inference-log-level", + default=None, + choices=logging._nameToLevel.keys(), + help="Set the logging level for ComfyUI inference", + ) + parser.add_argument( + "--orch-url", + default=None, + help="Orchestrator URL for capability registration", + ) + parser.add_argument( + "--orch-secret", + default=None, + help="Orchestrator secret for capability registration", + ) + parser.add_argument( + "--capability-name", + default=None, + help="Name for this capability (default: comfystream-processor)", + ) + parser.add_argument( + "--disable-frame-skip", + default=False, + action="store_true", + help="Disable adaptive frame skipping based on queue sizes (enabled by default)", + ) + parser.add_argument( + "--width", + default=512, + type=int, + help="Default video width for processing", + ) + parser.add_argument( + "--height", + default=512, + type=int, + help="Default video height for processing", + ) + args = parser.parse_args() + + logging.basicConfig( + level=args.log_level.upper(), + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", + ) + + # Allow overriding of ComfyUI log levels. + if args.comfyui_log_level: + log_level = logging._nameToLevel.get(args.comfyui_log_level.upper()) + logging.getLogger("comfy").setLevel(log_level) + + # Add ComfyStream timeout filter to suppress verbose execution logging + logging.getLogger("comfy.cmd.execution").addFilter(ComfyStreamTimeoutFilter()) + logging.getLogger("comfy").addFilter(ComfyStreamTimeoutFilter()) + + def force_print(*args, **kwargs): + print(*args, **kwargs, flush=True) + sys.stdout.flush() + + logger.info("Starting ComfyStream BYOC server with pytrickle StreamProcessor...") + + # Create frame processor with configuration + frame_processor = ComfyStreamFrameProcessor( + width=args.width, + height=args.height, + workspace=args.workspace, + disable_cuda_malloc=True, + gpu_only=True, + preview_method='none', + comfyui_inference_log_level=args.comfyui_inference_log_level + ) + + # Create frame skip configuration only if enabled + frame_skip_config = None + if args.disable_frame_skip: + logger.info("Frame skipping disabled") + else: + frame_skip_config = FrameSkipConfig() + logger.info("Frame skipping enabled: adaptive skipping based on queue sizes") + + # Create StreamProcessor with frame processor + processor = StreamProcessor( + video_processor=frame_processor.process_video_async, + audio_processor=frame_processor.process_audio_async, + model_loader=frame_processor.load_model, + param_updater=frame_processor.update_params, + on_stream_stop=frame_processor.on_stream_stop, + # Align processor name with capability for consistent logs + name=(args.capability_name or os.getenv("CAPABILITY_NAME") or "comfystream-processor"), + port=int(args.port), + host=args.host, + frame_skip_config=frame_skip_config, + # Ensure server metadata reflects the desired capability name + capability_name=(args.capability_name or os.getenv("CAPABILITY_NAME") or "comfystream-processor") + ) + + # Set the stream processor reference for text data publishing + frame_processor.set_stream_processor(processor) + + # Create async startup function to load model + async def load_model_on_startup(app): + await processor._frame_processor.load_model() + + # Create async startup function for orchestrator registration + async def register_orchestrator_startup(app): + await register_orchestrator( + orch_url=args.orch_url, + orch_secret=args.orch_secret, + capability_name=args.capability_name, + host=args.host, + port=args.port + ) + + # Add model loading and registration to startup hooks + processor.server.app.on_startup.append(load_model_on_startup) + processor.server.app.on_startup.append(register_orchestrator_startup) + + # Add warmup endpoint: accepts same body as prompts update + async def warmup_handler(request): + try: + body = await request.json() + except Exception as e: + logger.error(f"Invalid JSON in warmup request: {e}") + return web.json_response({"error": "Invalid JSON"}, status=400) + try: + # Inject sentinel to trigger warmup inside update_params on the model thread + if isinstance(body, dict): + body["warmup"] = True + else: + body = {"warmup": True} + # Fire-and-forget: do not await warmup; update_params will schedule it + asyncio.get_running_loop().create_task(frame_processor.update_params(body)) + return web.json_response({"status": "accepted"}) + except Exception as e: + logger.error(f"Warmup failed: {e}") + return web.json_response({"error": str(e)}, status=500) + + # Mount at same API namespace as StreamProcessor defaults + processor.server.add_route("POST", "/api/stream/warmup", warmup_handler) + + # Run the processor + processor.run() + + +if __name__ == "__main__": + main() diff --git a/server/frame_processor.py b/server/frame_processor.py new file mode 100644 index 000000000..6cc79a712 --- /dev/null +++ b/server/frame_processor.py @@ -0,0 +1,273 @@ +import asyncio +import json +import logging +import os +from typing import List + +import numpy as np +from pytrickle.frame_processor import FrameProcessor +from pytrickle.frames import VideoFrame, AudioFrame +from comfystream.pipeline import Pipeline +from comfystream.utils import convert_prompt, ComfyStreamParamsUpdateRequest + +logger = logging.getLogger(__name__) + + +class ComfyStreamFrameProcessor(FrameProcessor): + """ + Integrated ComfyStream FrameProcessor for pytrickle. + + This class wraps the ComfyStream Pipeline to work with pytrickle's streaming architecture. + """ + + def __init__(self, text_poll_interval: float = 0.25, **load_params): + """Initialize with load parameters for pipeline creation. + + Args: + text_poll_interval: Interval in seconds to poll for text outputs (default: 0.25) + **load_params: Parameters for pipeline creation + """ + self.pipeline = None + self._load_params = load_params + self._text_poll_interval = text_poll_interval + self._stream_processor = None + self._warmup_task = None + self._text_forward_task = None + self._background_tasks = [] + self._stop_event = asyncio.Event() + super().__init__() + + def set_stream_processor(self, stream_processor): + """Set reference to StreamProcessor for data publishing.""" + self._stream_processor = stream_processor + logger.info("StreamProcessor reference set for text data publishing") + + def _setup_text_monitoring(self): + """Set up background text forwarding from the pipeline.""" + try: + if self.pipeline and self._stream_processor: + # Reset stop event for new stream + self._reset_stop_event() + # Start forwarder only if workflow has text outputs (best-effort) + should_start = True + try: + should_start = bool(self.pipeline.produces_text_output()) + except Exception: + # If capability check fails, default to starting forwarder + should_start = True + + if should_start: + # Start a background task that forwards text outputs via StreamProcessor + if self._text_forward_task and not self._text_forward_task.done(): + logger.debug("Text forwarder already running; not starting another") + return + + async def _forward_text_loop(): + try: + logger.info("Starting background text forwarder task") + while not self._stop_event.is_set(): + try: + # Non-blocking poll; sleep if no text to avoid tight loop + text = await self.pipeline.get_text_output() + if text is None or text.strip() == "": + await asyncio.sleep(self._text_poll_interval) + continue + if self._stream_processor: + success = await self._stream_processor.send_data(text) + if not success: + logger.debug("Text send failed; stopping text forwarder") + break + except asyncio.CancelledError: + logger.debug("Text forwarder task cancelled") + raise + except asyncio.CancelledError: + # Propagate to finally for cleanup + raise + except Exception as e: + logger.error(f"Error in text forwarder: {e}") + finally: + logger.info("Text forwarder task exiting") + + self._text_forward_task = asyncio.create_task(_forward_text_loop()) + self._background_tasks.append(self._text_forward_task) + except Exception: + logger.warning("Failed to set up text monitoring", exc_info=True) + + async def _stop_text_forwarder(self) -> None: + """Stop the background text forwarder task if running.""" + task = self._text_forward_task + if task and not task.done(): + try: + task.cancel() + await task + except asyncio.CancelledError: + pass + except Exception: + logger.debug("Error while awaiting text forwarder cancellation", exc_info=True) + self._text_forward_task = None + + async def on_stream_stop(self): + """Called when stream stops - cleanup background tasks.""" + logger.info("Stream stopped, cleaning up background tasks") + + # Set stop event to signal all background tasks to stop + self._stop_event.set() + + # Stop text forwarder + await self._stop_text_forwarder() + + # Cancel any other background tasks started by this processor + for task in list(self._background_tasks): + try: + if task and not task.done(): + task.cancel() + except Exception: + continue + + # Await task cancellations + for task in list(self._background_tasks): + if task: + try: + await task + except asyncio.CancelledError: + pass + except Exception: + logger.debug("Background task raised during shutdown", exc_info=True) + + self._background_tasks.clear() + logger.info("All background tasks cleaned up") + + def _reset_stop_event(self): + """Reset the stop event for a new stream.""" + self._stop_event.clear() + + async def load_model(self, **kwargs): + """Load model and initialize the pipeline.""" + params = {**self._load_params, **kwargs} + + if self.pipeline is None: + self.pipeline = Pipeline( + width=int(params.get('width', 512)), + height=int(params.get('height', 512)), + cwd=params.get('workspace', os.getcwd()), + disable_cuda_malloc=params.get('disable_cuda_malloc', True), + gpu_only=params.get('gpu_only', True), + preview_method=params.get('preview_method', 'none'), + comfyui_inference_log_level=params.get('comfyui_inference_log_level'), + ) + + async def warmup(self): + """Warm up the pipeline.""" + if not self.pipeline: + logger.warning("Warmup requested before pipeline initialization") + return + + logger.info("Running pipeline warmup...") + try: + capabilities = self.pipeline.get_workflow_io_capabilities() + logger.info(f"Detected I/O capabilities: {capabilities}") + + if capabilities.get("video", {}).get("input") or capabilities.get("video", {}).get("output"): + await self.pipeline.warm_video() + + if capabilities.get("audio", {}).get("input") or capabilities.get("audio", {}).get("output"): + await self.pipeline.warm_audio() + + except Exception as e: + logger.error(f"Warmup failed: {e}") + + def _schedule_warmup(self) -> None: + """Schedule warmup in background if not already running.""" + try: + if self._warmup_task and not self._warmup_task.done(): + logger.info("Warmup already in progress, skipping new warmup request") + return + + self._warmup_task = asyncio.create_task(self.warmup()) + logger.info("Warmup scheduled in background") + except Exception: + logger.warning("Failed to schedule warmup", exc_info=True) + + async def process_video_async(self, frame: VideoFrame) -> VideoFrame: + """Process video frame through ComfyStream Pipeline.""" + try: + + # Convert pytrickle VideoFrame to av.VideoFrame + av_frame = frame.to_av_frame(frame.tensor) + av_frame.pts = frame.timestamp + av_frame.time_base = frame.time_base + + # Process through pipeline + await self.pipeline.put_video_frame(av_frame) + processed_av_frame = await self.pipeline.get_processed_video_frame() + + # Convert back to pytrickle VideoFrame + processed_frame = VideoFrame.from_av_frame_with_timing(processed_av_frame, frame) + return processed_frame + + except Exception as e: + logger.error(f"Video processing failed: {e}") + return frame + + async def process_audio_async(self, frame: AudioFrame) -> List[AudioFrame]: + """Process audio frame through ComfyStream Pipeline or passthrough.""" + try: + if not self.pipeline: + return [frame] + + # Audio processing needed - use pipeline + av_frame = frame.to_av_frame() + await self.pipeline.put_audio_frame(av_frame) + processed_av_frame = await self.pipeline.get_processed_audio_frame() + processed_frame = AudioFrame.from_av_audio(processed_av_frame) + return [processed_frame] + + except Exception as e: + logger.error(f"Audio processing failed: {e}") + return [frame] + + async def update_params(self, params: dict): + """Update processing parameters.""" + if not self.pipeline: + return + + # Handle list input - take first element + if isinstance(params, list) and params: + params = params[0] + + # Validate parameters using the centralized validation + validated = ComfyStreamParamsUpdateRequest(**params).model_dump() + logger.info(f"Parameter validation successful, keys: {list(validated.keys())}") + + # Process prompts if provided + if "prompts" in validated and validated["prompts"]: + await self._process_prompts(validated["prompts"]) + + # Update pipeline dimensions + if "width" in validated: + self.pipeline.width = int(validated["width"]) + if "height" in validated: + self.pipeline.height = int(validated["height"]) + + # Schedule warmup if requested + if validated.get("warmup", False): + self._schedule_warmup() + + + async def _process_prompts(self, prompts): + """Process and set prompts in the pipeline.""" + try: + converted = convert_prompt(prompts, return_dict=True) + + # Set prompts in pipeline + await self.pipeline.set_prompts([converted]) + logger.info(f"Prompts set successfully: {list(prompts.keys())}") + + # Update text monitoring based on workflow capabilities + if self.pipeline.produces_text_output(): + self._setup_text_monitoring() + else: + await self._stop_text_forwarder() + + except Exception as e: + logger.error(f"Failed to process prompts: {e}") diff --git a/src/comfystream/__init__.py b/src/comfystream/__init__.py index b58bf2e44..8aee624cf 100644 --- a/src/comfystream/__init__.py +++ b/src/comfystream/__init__.py @@ -3,6 +3,7 @@ from .server.utils import temporary_log_level from .server.utils import FPSMeter from .server.metrics import MetricsManager, StreamStatsManager +from .exceptions import ComfyStreamInputTimeoutError, ComfyStreamAudioBufferError __all__ = [ 'ComfyStreamClient', @@ -10,5 +11,7 @@ 'temporary_log_level', 'FPSMeter', 'MetricsManager', - 'StreamStatsManager' + 'StreamStatsManager', + 'ComfyStreamInputTimeoutError', + 'ComfyStreamAudioBufferError' ] diff --git a/src/comfystream/client.py b/src/comfystream/client.py index 5c4408941..291ecdda7 100644 --- a/src/comfystream/client.py +++ b/src/comfystream/client.py @@ -4,6 +4,7 @@ from comfystream import tensor_cache from comfystream.utils import convert_prompt +from comfystream.exceptions import ComfyStreamInputTimeoutError from comfy.api.components.schema.prompt import PromptDictInput from comfy.cli_args_types import Configuration @@ -23,10 +24,24 @@ def __init__(self, max_workers: int = 1, **kwargs): self._stop_event = asyncio.Event() async def set_prompts(self, prompts: List[PromptDictInput]): + """Set new prompts, replacing any existing ones. + + Args: + prompts: List of prompt dictionaries to set + + Raises: + ValueError: If prompts list is empty + Exception: If prompt conversion or validation fails + """ + if not prompts: + raise ValueError("Cannot set empty prompts list") + + # Cancel existing prompts first to avoid conflicts await self.cancel_running_prompts() # Reset stop event for new prompts self._stop_event.clear() self.current_prompts = [convert_prompt(prompt) for prompt in prompts] + logger.info(f"Queuing {len(self.current_prompts)} prompt(s) for execution") for idx in range(len(self.current_prompts)): task = asyncio.create_task(self.run_prompt(idx)) self.running_prompts[idx] = task @@ -54,6 +69,9 @@ async def run_prompt(self, prompt_index: int): await self.comfy_client.queue_prompt(self.current_prompts[prompt_index]) except asyncio.CancelledError: raise + except ComfyStreamInputTimeoutError: + # Timeout errors are expected during stream switching - just continue + continue except Exception as e: await self.cleanup() logger.error(f"Error running prompt: {str(e)}") @@ -117,7 +135,15 @@ async def get_audio_output(self): return await tensor_cache.audio_outputs.get() async def get_text_output(self): - return await tensor_cache.text_outputs.get() + try: + return tensor_cache.text_outputs.get_nowait() + except asyncio.QueueEmpty: + # Expected case - queue is empty, no text available + return None + except Exception as e: + # Unexpected errors logged for debugging + logger.warning(f"Unexpected error in get_text_output: {e}") + return None async def get_available_nodes(self): """Get metadata and available nodes info in a single pass""" @@ -243,4 +269,4 @@ async def get_available_nodes(self): except Exception as e: logger.error(f"Error getting node info: {str(e)}") - return {} + return {} \ No newline at end of file diff --git a/src/comfystream/exceptions.py b/src/comfystream/exceptions.py new file mode 100644 index 000000000..8382a4c30 --- /dev/null +++ b/src/comfystream/exceptions.py @@ -0,0 +1,20 @@ +"""ComfyStream specific exceptions.""" + + +class ComfyStreamInputTimeoutError(Exception): + """Raised when input tensors are not available within timeout.""" + + def __init__(self, input_type: str, timeout_seconds: float): + self.input_type = input_type + self.timeout_seconds = timeout_seconds + message = f"No {input_type} frames available after {timeout_seconds}s timeout" + super().__init__(message) + + +class ComfyStreamAudioBufferError(ComfyStreamInputTimeoutError): + """Audio buffer insufficient data error.""" + + def __init__(self, timeout_seconds: float, needed_samples: int, available_samples: int): + self.needed_samples = needed_samples + self.available_samples = available_samples + super().__init__("audio", timeout_seconds) diff --git a/src/comfystream/pipeline.py b/src/comfystream/pipeline.py index d7c474438..e9e40d3ec 100644 --- a/src/comfystream/pipeline.py +++ b/src/comfystream/pipeline.py @@ -23,8 +23,12 @@ class Pipeline: postprocessing, and queue management. """ - def __init__(self, width: int = 512, height: int = 512, - comfyui_inference_log_level: Optional[int] = None, **kwargs): + def __init__(self, + width: int = 512, + height: int = 512, + comfyui_inference_log_level: Optional[int] = None, + blacklist_nodes: List[str] = ["ComfyUI-Manager"], + **kwargs): """Initialize the pipeline with the given configuration. Args: @@ -79,7 +83,7 @@ async def warm_audio(self): return dummy_frame = av.AudioFrame() - dummy_frame.side_data.input = np.random.randint(-32768, 32767, int(48000 * 0.5), dtype=np.int16) # TODO: adds a lot of delay if it doesn't match the buffer size, is warmup needed? + dummy_frame.side_data.input = np.random.randint(-32768, 32768, int(48000 * 0.5), dtype=np.int16) dummy_frame.sample_rate = 48000 for _ in range(WARMUP_RUNS): @@ -144,7 +148,7 @@ async def put_video_frame(self, frame: av.VideoFrame): self.client.put_video_input(frame) await self.video_incoming_frames.put(frame) - async def put_audio_frame(self, frame: av.AudioFrame): + async def put_audio_frame(self, frame: av.AudioFrame, preprocess: bool = True): """Queue an audio frame for processing. Args: @@ -159,14 +163,14 @@ async def put_audio_frame(self, frame: av.AudioFrame): return # Process and send to client when input is accepted - frame.side_data.input = self.audio_preprocess(frame) + frame.side_data.input = self.audio_preprocess(frame) if preprocess else frame.to_ndarray() frame.side_data.skipped = True # Mark passthrough based on whether workflow produces audio output frame.side_data.passthrough = not self.produces_audio_output() self.client.put_audio_input(frame) await self.audio_incoming_frames.put(frame) - def video_preprocess(self, frame: av.VideoFrame) -> Union[torch.Tensor, np.ndarray]: + def video_preprocess(self, frame: av.VideoFrame) -> torch.Tensor: """Preprocess a video frame before processing. Args: @@ -178,16 +182,39 @@ def video_preprocess(self, frame: av.VideoFrame) -> Union[torch.Tensor, np.ndarr frame_np = frame.to_ndarray(format="rgb24").astype(np.float32) / 255.0 return torch.from_numpy(frame_np).unsqueeze(0) - def audio_preprocess(self, frame: av.AudioFrame) -> Union[torch.Tensor, np.ndarray]: + def audio_preprocess(self, frame: av.AudioFrame) -> np.ndarray: """Preprocess an audio frame before processing. Args: frame: The audio frame to preprocess Returns: - The preprocessed frame as a tensor or numpy array + The preprocessed frame as a numpy array with int16 dtype """ - return frame.to_ndarray().ravel().reshape(-1, 2).mean(axis=1).astype(np.int16) + audio_data = frame.to_ndarray() + + # Handle multi-dimensional audio data + if audio_data.ndim == 2 and audio_data.shape[0] == 1 and audio_data.shape[0] <= audio_data.shape[1]: + audio_data = audio_data.ravel().reshape(-1, 2).mean(axis=1) + elif audio_data.ndim > 1: + audio_data = audio_data.mean(axis=0) + + # Ensure we always return int16 data + if audio_data.dtype in [np.float32, np.float64]: + # Check if data is normalized (-1.0 to 1.0 range) + max_abs_val = np.abs(audio_data).max() + if max_abs_val <= 1.0: + # Normalized float input - scale to int16 range + audio_data = np.clip(audio_data, -1.0, 1.0) + audio_data = (audio_data * 32767).astype(np.int16) + else: + # Large float values - clip and convert directly + audio_data = np.clip(audio_data, -32768, 32767).astype(np.int16) + else: + # Already integer data - ensure it's int16 + audio_data = audio_data.astype(np.int16) + + return audio_data def video_postprocess(self, output: Union[torch.Tensor, np.ndarray]) -> av.VideoFrame: """Postprocess a video frame after processing. @@ -280,7 +307,7 @@ async def get_processed_audio_frame(self) -> av.AudioFrame: return processed_frame - async def get_text_output(self) -> str: + async def get_text_output(self) -> str | None: """Get the next text output from the pipeline. Returns: @@ -288,10 +315,11 @@ async def get_text_output(self) -> str: """ # If workflow doesn't produce text output, return empty string immediately if not self.produces_text_output(): - return "" + return None async with temporary_log_level("comfy", self._comfyui_inference_log_level): out_text = await self.client.get_text_output() + return out_text async def get_nodes_info(self) -> Dict[str, Any]: diff --git a/src/comfystream/scripts/constraints.txt b/src/comfystream/scripts/overrides.txt similarity index 100% rename from src/comfystream/scripts/constraints.txt rename to src/comfystream/scripts/overrides.txt diff --git a/src/comfystream/scripts/setup_models.py b/src/comfystream/scripts/setup_models.py index 9360a542b..f00570b90 100644 --- a/src/comfystream/scripts/setup_models.py +++ b/src/comfystream/scripts/setup_models.py @@ -126,4 +126,11 @@ def setup_models(): setup_directories(workspace_dir) setup_model_files(workspace_dir) -setup_models() + +def main(): + """Entry point for command line usage.""" + setup_models() + + +if __name__ == "__main__": + main() diff --git a/src/comfystream/scripts/setup_nodes.py b/src/comfystream/scripts/setup_nodes.py index 418e55f86..ff8d216d5 100755 --- a/src/comfystream/scripts/setup_nodes.py +++ b/src/comfystream/scripts/setup_nodes.py @@ -54,11 +54,13 @@ def install_custom_nodes(workspace_dir, config_path=None, pull_branches=False): custom_nodes_path.mkdir(parents=True, exist_ok=True) os.chdir(custom_nodes_path) - # Get the absolute path to constraints.txt - constraints_path = Path(__file__).parent / "constraints.txt" - if not constraints_path.exists(): - print(f"Warning: constraints.txt not found at {constraints_path}") - constraints_path = None + # Get the absolute path to overrides.txt and set UV_OVERRIDES env var + overrides_path = Path(__file__).parent / "overrides.txt" + if overrides_path.exists(): + os.environ["UV_OVERRIDES"] = str(overrides_path) + print(f"Set UV_OVERRIDES to {overrides_path}") + else: + print(f"Warning: overrides.txt not found at {overrides_path}") try: for _, node_info in config["nodes"].items(): @@ -90,25 +92,20 @@ def install_custom_nodes(workspace_dir, config_path=None, pull_branches=False): # Install requirements if present requirements_file = node_path / "requirements.txt" if requirements_file.exists(): - pip_cmd = [ - sys.executable, - "-m", + uv_cmd = [ + "uv", "pip", "install", "-r", str(requirements_file), ] - if constraints_path and constraints_path.exists(): - pip_cmd.extend(["-c", str(constraints_path)]) - subprocess.run(pip_cmd, check=True) + subprocess.run(uv_cmd, check=True) # Install additional dependencies if specified if "dependencies" in node_info: for dep in node_info["dependencies"]: - pip_cmd = [sys.executable, "-m", "pip", "install", dep] - if constraints_path and constraints_path.exists(): - pip_cmd.extend(["-c", str(constraints_path)]) - subprocess.run(pip_cmd, check=True) + uv_cmd = ["uv", "pip", "install", dep] + subprocess.run(uv_cmd, check=True) print(f"Installed {node_info['name']}") except Exception as e: @@ -125,5 +122,9 @@ def setup_nodes(): install_custom_nodes(workspace_dir, pull_branches=args.pull_branches) -if __name__ == "__main__": +def main(): + """Entry point for command line usage.""" setup_nodes() + +if __name__ == "__main__": + main() diff --git a/src/comfystream/server/utils/__init__.py b/src/comfystream/server/utils/__init__.py index daa71bb1e..31a559085 100644 --- a/src/comfystream/server/utils/__init__.py +++ b/src/comfystream/server/utils/__init__.py @@ -1,2 +1,2 @@ -from .utils import patch_loop_datagram, add_prefix_to_app_routes, temporary_log_level +from .utils import patch_loop_datagram, add_prefix_to_app_routes, temporary_log_level, ComfyStreamTimeoutFilter from .fps_meter import FPSMeter diff --git a/src/comfystream/server/utils/utils.py b/src/comfystream/server/utils/utils.py index c7a7ac304..974aa74a3 100644 --- a/src/comfystream/server/utils/utils.py +++ b/src/comfystream/server/utils/utils.py @@ -83,3 +83,42 @@ async def temporary_log_level(logger_name: str, level: int): finally: if level is not None: logger.setLevel(original_level) + + +class ComfyStreamTimeoutFilter(logging.Filter): + """Filter to suppress verbose ComfyUI execution logs for ComfyStream timeout exceptions.""" + + def filter(self, record): + """Filter out ComfyUI execution error logs for ComfyStream timeout exceptions.""" + # Only filter ERROR level messages from ComfyUI execution system + if record.levelno != logging.ERROR: + return True + + # Check if this is from ComfyUI execution system + if not (record.name.startswith("comfy") and ("execution" in record.name or record.name == "comfy")): + return True + + # Get the full message including any exception info + message = record.getMessage() + + # Check if this is a ComfyStream timeout-related error + timeout_indicators = [ + "ComfyStreamInputTimeoutError", + "ComfyStreamAudioBufferError", + "No video frames available", + "No audio frames available" + ] + + # Suppress if any timeout indicator is found in the message + for indicator in timeout_indicators: + if indicator in message: + return False + + # Also check the exception info if present + if record.exc_info and record.exc_info[1]: + exc_str = str(record.exc_info[1]) + for indicator in timeout_indicators: + if indicator in exc_str: + return False + + return True diff --git a/src/comfystream/utils.py b/src/comfystream/utils.py index e26b963d0..c2cecd05b 100644 --- a/src/comfystream/utils.py +++ b/src/comfystream/utils.py @@ -1,6 +1,10 @@ import copy +import json +import os +import logging import importlib -from typing import Dict, Any +from typing import Dict, Any, List, Tuple, Optional, Union +from pytrickle.api import StreamParamsUpdateRequest from comfy.api.components.schema.prompt import Prompt, PromptDictInput from .modalities import ( get_node_counts_by_type, @@ -38,7 +42,7 @@ def _validate_prompt_constraints(counts: Dict[str, int]) -> None: if counts["outputs"] == 0: raise Exception("missing output") -def convert_prompt(prompt: PromptDictInput) -> Prompt: +def convert_prompt(prompt: PromptDictInput, return_dict: bool = False) -> Prompt: """Convert a prompt by replacing specific node types with tensor equivalents.""" try: # Note: lazy import is necessary to prevent KeyError during validation @@ -46,7 +50,7 @@ def convert_prompt(prompt: PromptDictInput) -> Prompt: except Exception: pass - # Validate the schema + """Convert and validate a ComfyUI workflow prompt.""" Prompt.validate(prompt) prompt = copy.deepcopy(prompt) @@ -69,7 +73,78 @@ def convert_prompt(prompt: PromptDictInput) -> Prompt: for key in convertible_keys["PreviewImage"] + convertible_keys["SaveImage"]: node = prompt[key] prompt[key] = create_save_tensor_node(node["inputs"]) + + + # Return dict if requested (for downstream components that expect plain dicts) + if return_dict: + return prompt # Already a plain dict at this point + + # Validate the processed prompt and return Pydantic object + return Prompt.validate(prompt) + +class ComfyStreamParamsUpdateRequest(StreamParamsUpdateRequest): + """ComfyStream parameter validation.""" + + def __init__(self, **data): + # Handle prompts parameter + if "prompts" in data: + prompts = data["prompts"] + + # Parse JSON string if needed + if isinstance(prompts, str) and prompts.strip(): + try: + prompts = json.loads(prompts) + except json.JSONDecodeError: + data.pop("prompts") + + # Handle list - use first valid dict + elif isinstance(prompts, list): + prompts = next((p for p in prompts if isinstance(p, dict)), None) + if not prompts: + data.pop("prompts") + + # Validate prompts + if "prompts" in data and isinstance(prompts, dict): + try: + data["prompts"] = convert_prompt(prompts, return_dict=True) + except Exception: + data.pop("prompts") + + # Call parent constructor + super().__init__(**data) + + @classmethod + def model_validate(cls, obj): + return cls(**obj) + + def model_dump(self): + return super().model_dump() + +def get_default_workflow() -> dict: + """Return the default workflow as a dictionary for warmup. + + Returns: + dict: Default workflow dictionary + """ + return { + "1": { + "inputs": { + "images": [ + "2", + 0 + ] + }, + "class_type": "SaveTensor", + "_meta": { + "title": "SaveTensor" + } + }, + "2": { + "inputs": {}, + "class_type": "LoadTensor", + "_meta": { + "title": "LoadTensor" + } + } + } - # Validate the processed prompt - prompt = Prompt.validate(prompt) - return prompt diff --git a/ui/package-lock.json b/ui/package-lock.json index d8f807e89..1261f8422 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -20,7 +20,7 @@ "clsx": "^2.1.1", "idb-keyval": "^6.2.1", "lucide-react": "^0.454.0", - "next": "15.2.4", + "next": "15.5.3", "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -60,9 +60,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, "dependencies": { @@ -218,9 +218,9 @@ "license": "BSD-3-Clause" }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", "cpu": [ "arm64" ], @@ -236,13 +236,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.0" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", "cpu": [ "x64" ], @@ -258,13 +258,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.0" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", "cpu": [ "arm64" ], @@ -278,9 +278,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", "cpu": [ "x64" ], @@ -294,9 +294,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", "cpu": [ "arm" ], @@ -310,9 +310,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", "cpu": [ "arm64" ], @@ -325,10 +325,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", "cpu": [ "s390x" ], @@ -342,9 +358,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", "cpu": [ "x64" ], @@ -358,9 +374,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", "cpu": [ "arm64" ], @@ -374,9 +390,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", "cpu": [ "x64" ], @@ -390,9 +406,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", "cpu": [ "arm" ], @@ -408,13 +424,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.0" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", "cpu": [ "arm64" ], @@ -430,13 +446,35 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", "cpu": [ "s390x" ], @@ -452,13 +490,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.0" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", "cpu": [ "x64" ], @@ -474,13 +512,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.0" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", "cpu": [ "arm64" ], @@ -496,13 +534,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", "cpu": [ "x64" ], @@ -518,20 +556,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.4.4" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -540,10 +578,29 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", "cpu": [ "ia32" ], @@ -560,9 +617,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", "cpu": [ "x64" ], @@ -717,9 +774,9 @@ } }, "node_modules/@next/env": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", - "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", + "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -733,9 +790,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", - "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", + "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", "cpu": [ "arm64" ], @@ -749,9 +806,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", - "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", + "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", "cpu": [ "x64" ], @@ -765,9 +822,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", - "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", + "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", "cpu": [ "arm64" ], @@ -781,9 +838,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", - "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", + "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", "cpu": [ "arm64" ], @@ -797,9 +854,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", - "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", + "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", "cpu": [ "x64" ], @@ -813,9 +870,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", - "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", + "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", "cpu": [ "x64" ], @@ -829,9 +886,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", - "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", + "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", "cpu": [ "arm64" ], @@ -845,9 +902,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", - "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", + "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", "cpu": [ "x64" ], @@ -2117,12 +2174,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2860,17 +2911,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -3315,9 +3355,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "optional": true, "engines": { @@ -5695,15 +5735,13 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", - "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", + "version": "15.5.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", + "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", "license": "MIT", "dependencies": { - "@next/env": "15.2.4", - "@swc/counter": "0.1.3", + "@next/env": "15.5.3", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -5715,19 +5753,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.4", - "@next/swc-darwin-x64": "15.2.4", - "@next/swc-linux-arm64-gnu": "15.2.4", - "@next/swc-linux-arm64-musl": "15.2.4", - "@next/swc-linux-x64-gnu": "15.2.4", - "@next/swc-linux-x64-musl": "15.2.4", - "@next/swc-win32-arm64-msvc": "15.2.4", - "@next/swc-win32-x64-msvc": "15.2.4", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.3", + "@next/swc-darwin-x64": "15.5.3", + "@next/swc-linux-arm64-gnu": "15.5.3", + "@next/swc-linux-arm64-musl": "15.5.3", + "@next/swc-linux-x64-gnu": "15.5.3", + "@next/swc-linux-x64-musl": "15.5.3", + "@next/swc-win32-arm64-msvc": "15.5.3", + "@next/swc-win32-x64-msvc": "15.5.3", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -6758,9 +6796,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "devOptional": true, "license": "ISC", "bin": { @@ -6820,16 +6858,16 @@ } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "detect-libc": "^2.0.4", + "semver": "^7.7.2" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -6838,25 +6876,28 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" } }, "node_modules/shebang-command": { @@ -7034,14 +7075,6 @@ "dev": true, "license": "MIT" }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", diff --git a/ui/package.json b/ui/package.json index ef3ae4370..666c6a5f1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -24,7 +24,7 @@ "clsx": "^2.1.1", "idb-keyval": "^6.2.1", "lucide-react": "^0.454.0", - "next": "15.2.4", + "next": "15.5.3", "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/workflows/comfystream/audio-tensor-utils-example-api.json b/workflows/comfystream/audio-tensor-utils-example-api.json index 37609fe9d..4d2e6c885 100644 --- a/workflows/comfystream/audio-tensor-utils-example-api.json +++ b/workflows/comfystream/audio-tensor-utils-example-api.json @@ -1,40 +1,36 @@ { - "1": { - "inputs": { - "buffer_size": 500.0 - }, - "class_type": "LoadAudioTensor", - "_meta": { - "title": "Load Audio Tensor" - } + "1": { + "inputs": { + "buffer_size": 500.0 }, - "2": { - "inputs": { - "audio": [ - "1", - 0 - ], - "sample_rate": [ - "1", - 1 - ], - "pitch_shift": 4.0 - }, - "class_type": "PitchShifter", - "_meta": { - "title": "Pitch Shift" - } + "class_type": "LoadAudioTensor", + "_meta": { + "title": "Load Audio Tensor" + } + }, + "2": { + "inputs": { + "pitch_shift": 4, + "audio": [ + "1", + 0 + ] + }, + "class_type": "PitchShifter", + "_meta": { + "title": "Pitch Shift" + } + }, + "3": { + "inputs": { + "audio": [ + "2", + 0 + ] }, - "3": { - "inputs": { - "audio": [ - "2", - 0 - ] - }, - "class_type": "SaveAudioTensor", - "_meta": { - "title": "Save Audio Tensor" - } + "class_type": "SaveAudioTensor", + "_meta": { + "title": "Save Audio Tensor" } + } } \ No newline at end of file diff --git a/workflows/comfystream/audio-transcription-api.json b/workflows/comfystream/audio-transcription-api.json index 3865a6fb0..57458d4b5 100644 --- a/workflows/comfystream/audio-transcription-api.json +++ b/workflows/comfystream/audio-transcription-api.json @@ -1,7 +1,6 @@ { "10": { "inputs": { - "sample_rate": 16000, "transcription_interval": 2, "accumulation_duration": 3, "whisper_model": "base", @@ -43,7 +42,7 @@ "title": "SRT Generator" } }, - "13": { + "26": { "inputs": { "data": [ "12",