chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,755 @@
|
||||
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
||||
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
DiffGenerator module for sglang-diffusion.
|
||||
|
||||
This module provides a consolidated interface for generating videos using
|
||||
diffusion models.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, List, Optional, Sequence, Union
|
||||
|
||||
import imageio
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
import scipy.io.wavfile as scipy_wavfile
|
||||
except ImportError: # pragma: no cover
|
||||
scipy_wavfile = None
|
||||
|
||||
try:
|
||||
import imageio_ffmpeg as _imageio_ffmpeg
|
||||
except ImportError: # pragma: no cover
|
||||
_imageio_ffmpeg = None
|
||||
|
||||
from sglang.multimodal_gen.configs.sample.sampling_params import (
|
||||
DataType,
|
||||
SamplingParams,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import CYAN, RESET, init_logger
|
||||
from sglang.srt.observability.trace import TraceReqContext
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SetLoraReq:
|
||||
lora_nickname: Union[str, List[str]]
|
||||
lora_path: Optional[Union[str, List[Optional[str]]]] = None
|
||||
target: Union[str, List[str]] = "all"
|
||||
strength: Union[float, List[float]] = 1.0
|
||||
merge_mode: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MergeLoraWeightsReq:
|
||||
target: str = "all"
|
||||
strength: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnmergeLoraWeightsReq:
|
||||
target: str = "all"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListLorasReq:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShutdownReq:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReleaseRealtimeSessionReq:
|
||||
session_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetDisaggStatsReq:
|
||||
"""Request to get disagg pipeline metrics from the scheduler."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def format_lora_message(
|
||||
lora_nickname: Union[str, List[str]],
|
||||
target: Union[str, List[str]],
|
||||
strength: Union[float, List[float]],
|
||||
) -> tuple[str, str, str]:
|
||||
"""Format success message for single or multiple LoRAs."""
|
||||
if isinstance(lora_nickname, list):
|
||||
nickname_str = ", ".join(lora_nickname)
|
||||
target_str = ", ".join(target) if isinstance(target, list) else target
|
||||
strength_str = (
|
||||
", ".join(f"{s:.2f}" for s in strength)
|
||||
if isinstance(strength, list)
|
||||
else f"{strength:.2f}"
|
||||
)
|
||||
else:
|
||||
nickname_str = lora_nickname
|
||||
target_str = target if isinstance(target, str) else ", ".join(target)
|
||||
strength_str = (
|
||||
f"{strength:.2f}"
|
||||
if isinstance(strength, (int, float))
|
||||
else ", ".join(f"{s:.2f}" for s in strength)
|
||||
)
|
||||
return nickname_str, target_str, strength_str
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenerationResult:
|
||||
"""Result of a single generation request from DiffGenerator."""
|
||||
|
||||
samples: Any = None
|
||||
frames: Any = None
|
||||
audio: Any = None
|
||||
action: Any = None # [T, raw_action_dim] predicted action (policy/inverse_dynamics)
|
||||
prompt: str | None = None
|
||||
size: tuple | None = None # (height, width, num_frames)
|
||||
generation_time: float = 0.0
|
||||
peak_memory_mb: float = 0.0
|
||||
metrics: dict = field(default_factory=dict)
|
||||
trajectory_latents: Any = None
|
||||
trajectory_timesteps: Any = None
|
||||
rollout_trajectory_data: Any = None
|
||||
trajectory_decoded: Any = None
|
||||
prompt_index: int = 0
|
||||
output_file_path: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaterializedOutput:
|
||||
sample: Any
|
||||
frames: list[Any]
|
||||
audio: Any = None
|
||||
fps: int = 0
|
||||
|
||||
|
||||
def normalize_output_seeds(
|
||||
seed: int | list[int],
|
||||
*,
|
||||
num_outputs_per_prompt: int,
|
||||
num_prompts: int = 1,
|
||||
prompt_index: int = 0,
|
||||
) -> list[int]:
|
||||
"""
|
||||
return a list of seed with size equal to `num_outputs_per_prompt`
|
||||
"""
|
||||
if num_outputs_per_prompt <= 0:
|
||||
raise ValueError(
|
||||
f"num_outputs_per_prompt must be positive, got {num_outputs_per_prompt}"
|
||||
)
|
||||
|
||||
if isinstance(seed, list):
|
||||
seeds = [int(item) for item in seed]
|
||||
total_outputs = num_outputs_per_prompt * num_prompts
|
||||
if len(seeds) == num_outputs_per_prompt:
|
||||
return seeds
|
||||
if len(seeds) == total_outputs:
|
||||
start = prompt_index * num_outputs_per_prompt
|
||||
return seeds[start : start + num_outputs_per_prompt]
|
||||
raise ValueError(
|
||||
"seed list length must match num_outputs_per_prompt "
|
||||
f"({num_outputs_per_prompt}) or total outputs ({total_outputs}), "
|
||||
f"got {len(seeds)}"
|
||||
)
|
||||
|
||||
base_seed = int(seed)
|
||||
return [base_seed + i for i in range(num_outputs_per_prompt)]
|
||||
|
||||
|
||||
def _with_output_index_suffix(output_file_name: str, output_index: int) -> str:
|
||||
base, ext = os.path.splitext(output_file_name)
|
||||
return f"{base}_{output_index}{ext}"
|
||||
|
||||
|
||||
def _copy_trace_ctx_for_output(req: Req, request_id: str | None, output_index: int):
|
||||
trace_ctx = req.trace_ctx
|
||||
if output_index == 0 or not trace_ctx.tracing_enable:
|
||||
return trace_ctx
|
||||
|
||||
output_trace_ctx = TraceReqContext(
|
||||
rid=request_id,
|
||||
module_name=trace_ctx.module_name,
|
||||
external_trace_header=trace_ctx.external_trace_header,
|
||||
)
|
||||
output_trace_ctx.trace_req_start()
|
||||
return output_trace_ctx
|
||||
|
||||
|
||||
def _copy_req_for_output(
|
||||
req: Req,
|
||||
*,
|
||||
request_id: str | None,
|
||||
output_index: int,
|
||||
) -> Req:
|
||||
"""Create a lightweight per-output ``Req`` without deep-copying tensors."""
|
||||
output_req = copy(req)
|
||||
output_req.sampling_params = copy(req.sampling_params)
|
||||
output_req.extra = dict(req.extra)
|
||||
output_req.condition_inputs = dict(req.condition_inputs)
|
||||
output_req.trace_ctx = _copy_trace_ctx_for_output(req, request_id, output_index)
|
||||
return output_req
|
||||
|
||||
|
||||
def expand_request_outputs(
|
||||
req: Req,
|
||||
*,
|
||||
num_prompts: int = 1,
|
||||
prompt_index: int = 0,
|
||||
) -> list[Req]:
|
||||
"""
|
||||
Expand a req to a list with size equal to `num_prompts`
|
||||
"""
|
||||
num_outputs = int(req.num_outputs_per_prompt)
|
||||
# each req must has different seed
|
||||
seeds = normalize_output_seeds(
|
||||
req.seed,
|
||||
num_outputs_per_prompt=num_outputs,
|
||||
num_prompts=num_prompts,
|
||||
prompt_index=prompt_index,
|
||||
)
|
||||
|
||||
if num_outputs == 1:
|
||||
req.seed = seeds[0]
|
||||
req.seeds = None
|
||||
req.generator = None
|
||||
return [req]
|
||||
|
||||
expanded: list[Req] = []
|
||||
for output_index, seed in enumerate(seeds):
|
||||
output_request_id = (
|
||||
f"{req.request_id}:{output_index}" if req.request_id is not None else None
|
||||
)
|
||||
output_req = _copy_req_for_output(
|
||||
req, request_id=output_request_id, output_index=output_index
|
||||
)
|
||||
output_req.seed = seed
|
||||
output_req.num_outputs_per_prompt = 1
|
||||
output_req.seeds = None
|
||||
output_req.generator = None
|
||||
output_req.extra["parent_request_id"] = req.request_id
|
||||
output_req.extra["output_index"] = output_index
|
||||
|
||||
if output_request_id is not None:
|
||||
output_req.request_id = output_request_id
|
||||
|
||||
if req.output_file_name:
|
||||
output_req.output_file_name = _with_output_index_suffix(
|
||||
req.output_file_name, output_index
|
||||
)
|
||||
output_req.validate()
|
||||
expanded.append(output_req)
|
||||
|
||||
return expanded
|
||||
|
||||
|
||||
def _normalize_audio_to_numpy(audio: Any) -> np.ndarray | None:
|
||||
"""Convert audio (torch / numpy) into a float32 numpy array in [-1, 1], best-effort."""
|
||||
if audio is None:
|
||||
return None
|
||||
if isinstance(audio, torch.Tensor):
|
||||
audio_np = audio.detach().float().clamp(-1.0, 1.0).cpu().numpy()
|
||||
elif isinstance(audio, np.ndarray):
|
||||
audio_np = audio.astype(np.float32, copy=False)
|
||||
audio_np = np.clip(audio_np, -1.0, 1.0)
|
||||
else:
|
||||
return None
|
||||
|
||||
# 1. Squeeze leading singleton dimensions (Batch, etc.)
|
||||
while audio_np.ndim > 1 and audio_np.shape[0] == 1:
|
||||
audio_np = audio_np.squeeze(0)
|
||||
|
||||
# 2. Handle (C, L) -> (L, C)
|
||||
if audio_np.ndim == 2 and audio_np.shape[0] < audio_np.shape[1]:
|
||||
audio_np = audio_np.transpose(1, 0)
|
||||
|
||||
# 3. Final safety check: if still 2D and channels (dim 1) is huge, something is wrong
|
||||
if audio_np.ndim == 2 and audio_np.shape[1] > 256 and audio_np.shape[0] == 1:
|
||||
audio_np = audio_np.flatten()
|
||||
|
||||
return audio_np
|
||||
|
||||
|
||||
def _pick_audio_sample_rate(
|
||||
*,
|
||||
audio_np: np.ndarray,
|
||||
audio_sample_rate: Optional[int],
|
||||
fps: int,
|
||||
num_frames: int,
|
||||
) -> int:
|
||||
"""Pick a plausible sample rate, falling back to inferring from video duration."""
|
||||
selected_sr = int(audio_sample_rate) if audio_sample_rate is not None else None
|
||||
if selected_sr is None or not (8000 <= selected_sr <= 192000):
|
||||
selected_sr = 24000
|
||||
try:
|
||||
duration_s = float(num_frames) / float(fps) if fps else 0.0
|
||||
if duration_s > 0:
|
||||
audio_len = (
|
||||
int(audio_np.shape[0])
|
||||
if audio_np.ndim == 2
|
||||
else int(audio_np.shape[-1])
|
||||
)
|
||||
inferred_sr = int(round(float(audio_len) / duration_s))
|
||||
if 8000 <= inferred_sr <= 192000:
|
||||
selected_sr = inferred_sr
|
||||
except Exception:
|
||||
pass
|
||||
return selected_sr
|
||||
|
||||
|
||||
def _resolve_ffmpeg_exe() -> str:
|
||||
ffmpeg_exe = "ffmpeg"
|
||||
ffmpeg_on_path = shutil.which("ffmpeg")
|
||||
if ffmpeg_on_path:
|
||||
ffmpeg_exe = ffmpeg_on_path
|
||||
try:
|
||||
if _imageio_ffmpeg is not None:
|
||||
ffmpeg_exe = _imageio_ffmpeg.get_ffmpeg_exe()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ffmpeg_ok = False
|
||||
if ffmpeg_exe:
|
||||
if os.path.isabs(ffmpeg_exe):
|
||||
ffmpeg_ok = os.path.exists(ffmpeg_exe)
|
||||
else:
|
||||
ffmpeg_ok = shutil.which(ffmpeg_exe) is not None
|
||||
if not ffmpeg_ok:
|
||||
raise RuntimeError("ffmpeg not found")
|
||||
return ffmpeg_exe
|
||||
|
||||
|
||||
def _mux_audio_np_into_mp4(
|
||||
*,
|
||||
save_file_path: str,
|
||||
audio_np: np.ndarray,
|
||||
sample_rate: int,
|
||||
ffmpeg_exe: str,
|
||||
) -> None:
|
||||
merged_path = save_file_path.rsplit(".", 1)[0] + ".tmp_mux.mp4"
|
||||
tmp_wav_path = None
|
||||
try:
|
||||
if scipy_wavfile is None:
|
||||
raise RuntimeError(
|
||||
"scipy is required to mux audio into mp4 (pip install scipy)"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
||||
tmp_wav_path = f.name
|
||||
scipy_wavfile.write(tmp_wav_path, sample_rate, audio_np)
|
||||
subprocess.run(
|
||||
[
|
||||
ffmpeg_exe,
|
||||
"-y",
|
||||
"-i",
|
||||
save_file_path,
|
||||
"-i",
|
||||
tmp_wav_path,
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-strict",
|
||||
"experimental",
|
||||
merged_path,
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
os.replace(merged_path, save_file_path)
|
||||
finally:
|
||||
if tmp_wav_path:
|
||||
try:
|
||||
os.remove(tmp_wav_path)
|
||||
except OSError:
|
||||
pass
|
||||
if os.path.exists(merged_path):
|
||||
try:
|
||||
os.remove(merged_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _maybe_mux_audio_into_mp4(
|
||||
*,
|
||||
save_file_path: str,
|
||||
audio: Any,
|
||||
frames: list,
|
||||
fps: int,
|
||||
audio_sample_rate: Optional[int],
|
||||
) -> None:
|
||||
"""Best-effort mux audio into an already-written mp4 at save_file_path.
|
||||
|
||||
Any failure should keep the silent video and only log a warning.
|
||||
"""
|
||||
audio_np = _normalize_audio_to_numpy(audio)
|
||||
if audio_np is None:
|
||||
return
|
||||
selected_sr = _pick_audio_sample_rate(
|
||||
audio_np=audio_np,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
fps=fps,
|
||||
num_frames=len(frames),
|
||||
)
|
||||
|
||||
try:
|
||||
ffmpeg_exe = _resolve_ffmpeg_exe()
|
||||
_mux_audio_np_into_mp4(
|
||||
save_file_path=save_file_path,
|
||||
audio_np=audio_np,
|
||||
sample_rate=selected_sr,
|
||||
ffmpeg_exe=ffmpeg_exe,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to mux audio into mp4 (saved silent video): %s",
|
||||
str(e),
|
||||
)
|
||||
|
||||
|
||||
def prepare_request(
|
||||
server_args: ServerArgs,
|
||||
sampling_params: SamplingParams,
|
||||
external_trace_header: dict[str, str] | None = None,
|
||||
) -> Req:
|
||||
"""
|
||||
Create a Req object with sampling_params as a parameter.
|
||||
"""
|
||||
req = Req(
|
||||
sampling_params=sampling_params,
|
||||
VSA_sparsity=server_args.attention_backend_config.VSA_sparsity,
|
||||
)
|
||||
sampling_params.apply_request_extra(req)
|
||||
if getattr(sampling_params, "max_sequence_length", None) is not None:
|
||||
req.max_sequence_length = sampling_params.max_sequence_length
|
||||
|
||||
diffusers_kwargs = getattr(sampling_params, "diffusers_kwargs", None)
|
||||
if diffusers_kwargs and "max_sequence_length" in diffusers_kwargs:
|
||||
req.max_sequence_length = diffusers_kwargs["max_sequence_length"]
|
||||
|
||||
if not isinstance(req.prompt, str):
|
||||
raise TypeError(f"`prompt` must be a string, but got {type(req.prompt)}")
|
||||
|
||||
req_width = getattr(req, "width", None)
|
||||
req_height = getattr(req, "height", None)
|
||||
if (req_width is not None and req_width <= 0) or (
|
||||
req_height is not None and req_height <= 0
|
||||
):
|
||||
raise ValueError(
|
||||
f"Height and width must be positive, got height={req_height}, width={req_width}"
|
||||
)
|
||||
|
||||
if server_args.enable_trace:
|
||||
trace_ctx = TraceReqContext(
|
||||
rid=sampling_params.request_id,
|
||||
module_name="diffusion",
|
||||
external_trace_header=external_trace_header,
|
||||
)
|
||||
trace_ctx.trace_req_start()
|
||||
req.trace_ctx = trace_ctx
|
||||
|
||||
return req
|
||||
|
||||
|
||||
def attach_audio_to_video_sample(
|
||||
sample: Any,
|
||||
audio: Any,
|
||||
output_idx: int,
|
||||
) -> Any:
|
||||
"""Attach per-sample audio for video outputs when available."""
|
||||
audio = select_output_audio(audio, output_idx)
|
||||
if audio is None:
|
||||
return sample
|
||||
if not (isinstance(sample, (tuple, list)) and len(sample) == 2):
|
||||
return (sample, audio)
|
||||
return sample
|
||||
|
||||
|
||||
def select_output_audio(audio: Any, output_idx: int) -> Any:
|
||||
if isinstance(audio, torch.Tensor) and audio.ndim >= 2:
|
||||
return audio[output_idx] if audio.shape[0] > output_idx else None
|
||||
if isinstance(audio, np.ndarray) and audio.ndim >= 2:
|
||||
return audio[output_idx] if audio.shape[0] > output_idx else None
|
||||
return audio
|
||||
|
||||
|
||||
def _split_sample_audio(sample: Any) -> tuple[Any, Any]:
|
||||
if isinstance(sample, (tuple, list)) and len(sample) == 2:
|
||||
return sample[0], sample[1]
|
||||
return sample, None
|
||||
|
||||
|
||||
def _sample_to_uint8_frames(sample: Any) -> list[Any]:
|
||||
"""return numpy frames in THCW format"""
|
||||
if isinstance(sample, torch.Tensor):
|
||||
# sample is raw tensor
|
||||
if sample.dim() == 3:
|
||||
sample = sample.unsqueeze(1)
|
||||
sample = (sample * 255).clamp(0, 255).to(torch.uint8)
|
||||
videos = sample.permute(1, 2, 3, 0).contiguous().cpu().numpy()
|
||||
return list(videos)
|
||||
|
||||
if not isinstance(sample, np.ndarray):
|
||||
raise TypeError(f"Unsupported sample type: {type(sample)}")
|
||||
|
||||
# sample is numpy frames
|
||||
arr = sample
|
||||
if arr.ndim == 3:
|
||||
if arr.shape[-1] in (1, 3, 4):
|
||||
arr = arr[None, ...]
|
||||
else:
|
||||
arr = arr[..., None]
|
||||
if arr.ndim != 4:
|
||||
raise ValueError(f"Unexpected numpy sample shape: {tuple(arr.shape)}")
|
||||
|
||||
if arr.shape[-1] not in (1, 3, 4) and arr.shape[0] in (1, 3, 4):
|
||||
t = torch.from_numpy(arr)
|
||||
if t.dim() == 3:
|
||||
t = t.unsqueeze(1)
|
||||
t = (t * 255).clamp(0, 255).to(torch.uint8)
|
||||
videos = t.permute(1, 2, 3, 0).contiguous().cpu().numpy()
|
||||
return list(videos)
|
||||
|
||||
if arr.dtype != np.uint8:
|
||||
arr = (np.clip(arr, 0.0, 1.0) * 255.0).astype(np.uint8)
|
||||
return list(arr)
|
||||
|
||||
|
||||
def materialize_output_sample(
|
||||
sample: Any,
|
||||
data_type: DataType,
|
||||
fps: int,
|
||||
*,
|
||||
enable_frame_interpolation: bool = False,
|
||||
frame_interpolation_exp: int = 1,
|
||||
frame_interpolation_scale: float = 1.0,
|
||||
frame_interpolation_model_path: Optional[str] = None,
|
||||
enable_upscaling: bool = False,
|
||||
upscaling_model_path: Optional[str] = None,
|
||||
upscaling_scale: int = 4,
|
||||
) -> MaterializedOutput:
|
||||
"""materialize samples, apply postprocessing if applicable"""
|
||||
sample_without_audio, audio = _split_sample_audio(sample)
|
||||
frames = _sample_to_uint8_frames(sample_without_audio)
|
||||
|
||||
# frames are uint8 numpy arrays in THWC format at this point
|
||||
if enable_frame_interpolation and data_type == DataType.VIDEO and len(frames) > 1:
|
||||
from sglang.multimodal_gen.runtime.postprocess import (
|
||||
interpolate_video_frames,
|
||||
)
|
||||
|
||||
frames, multiplier = interpolate_video_frames(
|
||||
frames,
|
||||
exp=frame_interpolation_exp,
|
||||
scale=frame_interpolation_scale,
|
||||
model_path=frame_interpolation_model_path,
|
||||
)
|
||||
fps = fps * multiplier
|
||||
|
||||
if enable_upscaling and frames:
|
||||
from sglang.multimodal_gen.runtime.postprocess import upscale_frames
|
||||
|
||||
frames = upscale_frames(
|
||||
frames,
|
||||
model_path=upscaling_model_path,
|
||||
scale=upscaling_scale,
|
||||
)
|
||||
|
||||
return MaterializedOutput(sample=sample, frames=frames, audio=audio, fps=fps)
|
||||
|
||||
|
||||
def save_materialized_output(
|
||||
materialized: MaterializedOutput,
|
||||
data_type: DataType,
|
||||
save_file_path: Optional[str],
|
||||
*,
|
||||
save_output: bool = True,
|
||||
audio_sample_rate: Optional[int] = None,
|
||||
output_compression: Optional[int] = None,
|
||||
) -> None:
|
||||
if not save_output:
|
||||
return
|
||||
if not save_file_path:
|
||||
logger.info("No output path provided, output not saved")
|
||||
return
|
||||
|
||||
os.makedirs(os.path.dirname(save_file_path), exist_ok=True)
|
||||
if data_type == DataType.VIDEO:
|
||||
quality = output_compression / 10 if output_compression is not None else 5
|
||||
imageio.mimsave(
|
||||
save_file_path,
|
||||
materialized.frames,
|
||||
fps=materialized.fps,
|
||||
format=data_type.get_default_extension(),
|
||||
codec="libx264",
|
||||
quality=quality,
|
||||
)
|
||||
|
||||
_maybe_mux_audio_into_mp4(
|
||||
save_file_path=save_file_path,
|
||||
audio=materialized.audio,
|
||||
frames=materialized.frames,
|
||||
fps=materialized.fps,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
)
|
||||
else:
|
||||
quality = output_compression if output_compression is not None else 75
|
||||
if len(materialized.frames) > 1:
|
||||
for i, image in enumerate(materialized.frames):
|
||||
parts = save_file_path.rsplit(".", 1)
|
||||
if len(parts) == 2:
|
||||
indexed_path = f"{parts[0]}_{i}.{parts[1]}"
|
||||
else:
|
||||
indexed_path = f"{save_file_path}_{i}"
|
||||
_save_image_frame(indexed_path, image, quality, output_compression)
|
||||
else:
|
||||
_save_image_frame(
|
||||
save_file_path, materialized.frames[0], quality, output_compression
|
||||
)
|
||||
logger.info(f"Output saved to {CYAN}{save_file_path}{RESET}")
|
||||
|
||||
|
||||
def _save_image_frame(
|
||||
path: str, frame: np.ndarray, quality: int | None, output_compression: int | None
|
||||
) -> None:
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext == ".png":
|
||||
compress_level = 1
|
||||
if output_compression is not None and output_compression != 75:
|
||||
compress_level = max(0, min(9, round(output_compression / 100 * 9)))
|
||||
if frame.ndim == 3 and frame.shape[-1] == 1:
|
||||
frame = frame[..., 0]
|
||||
Image.fromarray(frame).save(path, format="PNG", compress_level=compress_level)
|
||||
else:
|
||||
imageio.imwrite(path, frame, quality=quality)
|
||||
|
||||
|
||||
def save_outputs(
|
||||
outputs: Sequence[Any],
|
||||
data_type: DataType,
|
||||
fps: int,
|
||||
save_output: bool,
|
||||
build_output_path: Callable[[int], str],
|
||||
*,
|
||||
audio: Any = None,
|
||||
audio_sample_rate: Optional[int] = None,
|
||||
samples_out: Optional[list[Any]] = None,
|
||||
audios_out: Optional[list[Any]] = None,
|
||||
frames_out: Optional[list[Any]] = None,
|
||||
output_compression: Optional[int] = None,
|
||||
enable_frame_interpolation: bool = False,
|
||||
frame_interpolation_exp: int = 1,
|
||||
frame_interpolation_scale: float = 1.0,
|
||||
frame_interpolation_model_path: Optional[str] = None,
|
||||
enable_upscaling: bool = False,
|
||||
upscaling_model_path: Optional[str] = None,
|
||||
upscaling_scale: int = 4,
|
||||
) -> list[str]:
|
||||
output_paths: list[str] = []
|
||||
for idx, sample in enumerate(outputs):
|
||||
save_file_path = build_output_path(idx)
|
||||
if data_type == DataType.ACTION:
|
||||
if samples_out is not None:
|
||||
samples_out.append(sample)
|
||||
if audios_out is not None:
|
||||
audios_out.append(None)
|
||||
if frames_out is not None:
|
||||
frames_out.append([])
|
||||
if save_output and save_file_path:
|
||||
os.makedirs(os.path.dirname(save_file_path) or ".", exist_ok=True)
|
||||
with open(save_file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(sample, f, ensure_ascii=False)
|
||||
logger.info(f"Output saved to {CYAN}{save_file_path}{RESET}")
|
||||
output_paths.append(save_file_path)
|
||||
continue
|
||||
|
||||
if data_type == DataType.VIDEO:
|
||||
sample = attach_audio_to_video_sample(sample, audio, idx)
|
||||
|
||||
frames = post_process_sample(
|
||||
sample,
|
||||
data_type,
|
||||
fps,
|
||||
save_output,
|
||||
save_file_path,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
output_compression=output_compression,
|
||||
enable_frame_interpolation=enable_frame_interpolation,
|
||||
frame_interpolation_exp=frame_interpolation_exp,
|
||||
frame_interpolation_scale=frame_interpolation_scale,
|
||||
frame_interpolation_model_path=frame_interpolation_model_path,
|
||||
enable_upscaling=enable_upscaling,
|
||||
upscaling_model_path=upscaling_model_path,
|
||||
upscaling_scale=upscaling_scale,
|
||||
)
|
||||
|
||||
if samples_out is not None:
|
||||
samples_out.append(sample)
|
||||
if audios_out is not None:
|
||||
if data_type == DataType.VIDEO:
|
||||
audios_out.append(select_output_audio(audio, idx))
|
||||
else:
|
||||
audios_out.append(audio)
|
||||
if frames_out is not None:
|
||||
frames_out.append(frames)
|
||||
output_paths.append(save_file_path)
|
||||
return output_paths
|
||||
|
||||
|
||||
def post_process_sample(
|
||||
sample: Any,
|
||||
data_type: DataType,
|
||||
fps: int,
|
||||
save_output: bool = True,
|
||||
save_file_path: Optional[str] = None,
|
||||
audio_sample_rate: Optional[int] = None,
|
||||
output_compression: Optional[int] = None,
|
||||
enable_frame_interpolation: bool = False,
|
||||
frame_interpolation_exp: int = 1,
|
||||
frame_interpolation_scale: float = 1.0,
|
||||
frame_interpolation_model_path: Optional[str] = None,
|
||||
enable_upscaling: bool = False,
|
||||
upscaling_model_path: Optional[str] = None,
|
||||
upscaling_scale: int = 4,
|
||||
) -> list[Any]:
|
||||
"""materialize frames and save outputs (optional)"""
|
||||
if data_type == DataType.ACTION:
|
||||
return []
|
||||
|
||||
materialized = materialize_output_sample(
|
||||
sample,
|
||||
data_type,
|
||||
fps,
|
||||
enable_frame_interpolation=enable_frame_interpolation,
|
||||
frame_interpolation_exp=frame_interpolation_exp,
|
||||
frame_interpolation_scale=frame_interpolation_scale,
|
||||
frame_interpolation_model_path=frame_interpolation_model_path,
|
||||
enable_upscaling=enable_upscaling,
|
||||
upscaling_model_path=upscaling_model_path,
|
||||
upscaling_scale=upscaling_scale,
|
||||
)
|
||||
save_materialized_output(
|
||||
materialized,
|
||||
data_type,
|
||||
save_file_path,
|
||||
save_output=save_output,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
output_compression=output_compression,
|
||||
)
|
||||
return materialized.frames
|
||||
Reference in New Issue
Block a user