[Model][Frontend] MiniMax-H3: Add opt-in planar video response encoding (#6288)
Signed-off-by: MosCloud <bwzhang1991@163.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
3d34b8b977
commit
4882a36c41
9 files changed
+1077
-23
No files matched your search
@@ -118,6 +118,21 @@ On Atlas 800I A3 (64 GB HBM per device) the combined service does not fit at
|
||||
above) or HSDP — see
|
||||
[§ Memory and attention optimizations](#memory-and-attention-optimizations-a3).
|
||||
|
||||
### CPU MP4 response encoding (Atlas A2)
|
||||
|
||||
Non-streaming MP4 responses use one public automatic encoder. It checks the
|
||||
runtime frame shape, common dtype, and RGB channel-plane contiguity for every
|
||||
request; compatible inputs use direct planar PyAV frames, while unsupported
|
||||
inputs fall back to the legacy muxer before opening the PyAV container. No CLI
|
||||
flag, model declaration, or user configuration is required. Streaming fMP4
|
||||
output remains on its existing incremental path.
|
||||
|
||||
Current performance and correctness validation is limited to one Atlas A2 host
|
||||
with 8x Ascend 910B4-1 NPUs, the `FL2VA`/`t2va` partition, one request at a
|
||||
time, 1344x768 at 24 fps, and 5, 8.7, and 15 second requests. This optimization
|
||||
only changes CPU MP4 response encoding; it does not change DiT execution or
|
||||
stage 0.
|
||||
|
||||
### Optional optimizations
|
||||
|
||||
Two independent optimizations may be enabled on top of the configuration
|
||||
|
||||
@@ -65,6 +65,31 @@ On AMD ROCm, install without the `[fa4]` extra (FA4 is CUDA-only) and use
|
||||
`ffmpeg` and `ffprobe` must be available on `PATH`. They are used for
|
||||
reference-video preparation and MP4 output.
|
||||
|
||||
## CPU MP4 response encoding
|
||||
|
||||
For CUDA and ROCm deployments, non-streaming MP4 responses are encoded on the
|
||||
host CPU through PyAV/libx264 after generation. The response encoder selects the
|
||||
path automatically at runtime: inputs with a supported frame shape, common dtype,
|
||||
and RGB per-channel contiguous layout use direct planar frames; other inputs
|
||||
fall back to the legacy path before the PyAV container is opened. No CLI flag,
|
||||
model declaration, or user configuration is required. Streaming fMP4 output is
|
||||
unchanged.
|
||||
|
||||
A community benchmark on 2x Xeon 8480C reported the following comparison
|
||||
between the legacy and direct planar paths
|
||||
([full result](https://github.com/vllm-project/vllm-omni/pull/6288#issuecomment-5337546499)):
|
||||
|
||||
| Metric | Legacy | Direct planar | Change |
|
||||
|---|---:|---:|---:|
|
||||
| Median wall time | 1.805 s | 1.394 s | -22.8% |
|
||||
| Median process CPU time | 3.613 s | 3.207 s | -11.2% |
|
||||
| Peak RSS | 3182 MiB | 2794 MiB | -387 MiB (-12.2%) |
|
||||
|
||||
Across the 1.0-8.7 s sweep, wall-time improvement was approximately 21.8-22.6%;
|
||||
outputs were byte-identical and full decode passed. This is evidence for host
|
||||
CPU response encoding. Actual gains depend on the CPU and runtime, and should
|
||||
not be interpreted as GPU, DiT, or stage 0 speedups.
|
||||
|
||||
## Start a server
|
||||
|
||||
Pass the repository ID directly. The pipeline uses `FL2VA` for model discovery
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Benchmark legacy and automatic MP4 response encoding on synthetic input."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import statistics
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from vllm_omni.entrypoints.openai.video_api_utils import (
|
||||
_encode_video_bytes,
|
||||
_encode_video_bytes_legacy,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BenchmarkVariant:
|
||||
label: str
|
||||
encoder: Callable[..., bytes]
|
||||
|
||||
|
||||
def _positive_int(value: str) -> int:
|
||||
parsed = int(value)
|
||||
if parsed < 1:
|
||||
raise argparse.ArgumentTypeError("value must be at least 1")
|
||||
return parsed
|
||||
|
||||
|
||||
def _build_inputs(
|
||||
*,
|
||||
frames: int,
|
||||
height: int,
|
||||
width: int,
|
||||
fps: int,
|
||||
audio_sample_rate: int,
|
||||
seed: int,
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
rng = np.random.default_rng(seed)
|
||||
planar = rng.random((3, frames, height, width), dtype=np.float32)
|
||||
video = planar.transpose(1, 2, 3, 0)
|
||||
if not all(video[..., channel].flags.c_contiguous for channel in range(3)):
|
||||
raise RuntimeError("synthetic video does not expose contiguous channel planes")
|
||||
|
||||
sample_count = max(1, round(frames / fps * audio_sample_rate))
|
||||
timeline = np.arange(sample_count, dtype=np.float32) / audio_sample_rate
|
||||
left = np.sin(2 * np.pi * 220 * timeline).astype(np.float32, copy=False)
|
||||
right = np.sin(2 * np.pi * 330 * timeline).astype(np.float32, copy=False)
|
||||
return video, np.stack((left, right))
|
||||
|
||||
|
||||
def _measure(
|
||||
variant: BenchmarkVariant,
|
||||
*,
|
||||
video: np.ndarray,
|
||||
audio: np.ndarray,
|
||||
fps: int,
|
||||
audio_sample_rate: int,
|
||||
) -> tuple[bytes, dict[str, object]]:
|
||||
cpu_start = time.process_time_ns()
|
||||
wall_start = time.perf_counter_ns()
|
||||
output = variant.encoder(
|
||||
video,
|
||||
fps=fps,
|
||||
audio=audio,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
video_codec_options={"preset": "ultrafast", "threads": "0"},
|
||||
)
|
||||
wall_ms = (time.perf_counter_ns() - wall_start) / 1_000_000
|
||||
process_cpu_ms = (time.process_time_ns() - cpu_start) / 1_000_000
|
||||
return output, {
|
||||
"label": variant.label,
|
||||
"wall_ms": wall_ms,
|
||||
"process_cpu_ms": process_cpu_ms,
|
||||
"output_bytes": len(output),
|
||||
"output_sha256": hashlib.sha256(output).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
def _summarize(records: list[dict[str, object]], label: str) -> dict[str, object]:
|
||||
selected = [record for record in records if record["label"] == label]
|
||||
wall_values = [float(record["wall_ms"]) for record in selected]
|
||||
cpu_values = [float(record["process_cpu_ms"]) for record in selected]
|
||||
return {
|
||||
"runs": len(selected),
|
||||
"wall_ms": {
|
||||
"median": statistics.median(wall_values),
|
||||
"min": min(wall_values),
|
||||
"max": max(wall_values),
|
||||
},
|
||||
"process_cpu_ms": {
|
||||
"median": statistics.median(cpu_values),
|
||||
"min": min(cpu_values),
|
||||
"max": max(cpu_values),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--frames", type=_positive_int, default=12)
|
||||
parser.add_argument("--height", type=_positive_int, default=96)
|
||||
parser.add_argument("--width", type=_positive_int, default=160)
|
||||
parser.add_argument("--fps", type=_positive_int, default=24)
|
||||
parser.add_argument("--audio-sample-rate", type=_positive_int, default=32000)
|
||||
parser.add_argument("--warmup", type=int, default=1)
|
||||
parser.add_argument("--rounds", type=_positive_int, default=3)
|
||||
parser.add_argument("--seed", type=int, default=20260817)
|
||||
parser.add_argument("--output-json", type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.warmup < 0:
|
||||
parser.error("--warmup must be non-negative")
|
||||
return args
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
video, audio = _build_inputs(
|
||||
frames=args.frames,
|
||||
height=args.height,
|
||||
width=args.width,
|
||||
fps=args.fps,
|
||||
audio_sample_rate=args.audio_sample_rate,
|
||||
seed=args.seed,
|
||||
)
|
||||
baseline = BenchmarkVariant("legacy", _encode_video_bytes_legacy)
|
||||
candidate = BenchmarkVariant("automatic", _encode_video_bytes)
|
||||
|
||||
for _ in range(args.warmup):
|
||||
for variant in (baseline, candidate):
|
||||
_measure(
|
||||
variant,
|
||||
video=video,
|
||||
audio=audio,
|
||||
fps=args.fps,
|
||||
audio_sample_rate=args.audio_sample_rate,
|
||||
)
|
||||
|
||||
records: list[dict[str, object]] = []
|
||||
for round_index in range(args.rounds):
|
||||
order = (baseline, candidate) if round_index % 2 == 0 else (candidate, baseline)
|
||||
for variant in order:
|
||||
_, record = _measure(
|
||||
variant,
|
||||
video=video,
|
||||
audio=audio,
|
||||
fps=args.fps,
|
||||
audio_sample_rate=args.audio_sample_rate,
|
||||
)
|
||||
record["round"] = round_index + 1
|
||||
records.append(record)
|
||||
|
||||
output_hashes = {str(record["output_sha256"]) for record in records}
|
||||
if len(output_hashes) != 1:
|
||||
raise RuntimeError(f"benchmark variants produced different outputs: {sorted(output_hashes)}")
|
||||
|
||||
paired_savings_ms = []
|
||||
paired_savings_percent = []
|
||||
for round_index in range(args.rounds):
|
||||
pair = [record for record in records if record["round"] == round_index + 1]
|
||||
baseline_ms = float(next(record["wall_ms"] for record in pair if record["label"] == baseline.label))
|
||||
candidate_ms = float(next(record["wall_ms"] for record in pair if record["label"] == candidate.label))
|
||||
paired_savings_ms.append(baseline_ms - candidate_ms)
|
||||
paired_savings_percent.append((baseline_ms - candidate_ms) / baseline_ms * 100)
|
||||
|
||||
result = {
|
||||
"config": {
|
||||
"frames": args.frames,
|
||||
"height": args.height,
|
||||
"width": args.width,
|
||||
"fps": args.fps,
|
||||
"audio_sample_rate": args.audio_sample_rate,
|
||||
"warmup": args.warmup,
|
||||
"rounds": args.rounds,
|
||||
"seed": args.seed,
|
||||
"video_shape": list(video.shape),
|
||||
"video_strides": list(video.strides),
|
||||
"codec_options": {"preset": "ultrafast", "threads": "0"},
|
||||
},
|
||||
"records": records,
|
||||
"summary": {
|
||||
baseline.label: _summarize(records, baseline.label),
|
||||
candidate.label: _summarize(records, candidate.label),
|
||||
"paired_wall_saving_ms": {
|
||||
"median": statistics.median(paired_savings_ms),
|
||||
"min": min(paired_savings_ms),
|
||||
"max": max(paired_savings_ms),
|
||||
},
|
||||
"paired_wall_saving_percent": {
|
||||
"median": statistics.median(paired_savings_percent),
|
||||
"min": min(paired_savings_percent),
|
||||
"max": max(paired_savings_percent),
|
||||
},
|
||||
"output_sha256": output_hashes.pop(),
|
||||
},
|
||||
}
|
||||
output_json = json.dumps(result, indent=2, sort_keys=True)
|
||||
if args.output_json is not None:
|
||||
args.output_json.write_text(output_json + "\n", encoding="utf-8")
|
||||
print(output_json)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -5,6 +5,7 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
|
||||
import av
|
||||
import httpx
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -13,6 +14,7 @@ from PIL import Image
|
||||
from vllm import envs
|
||||
|
||||
from vllm_omni.diffusion.postprocess import rife_interpolator
|
||||
from vllm_omni.diffusion.utils import media_utils
|
||||
from vllm_omni.entrypoints.openai import video_api_utils
|
||||
from vllm_omni.entrypoints.openai.errors import InvalidInputReferenceError
|
||||
|
||||
@@ -219,6 +221,462 @@ def test_channel_first_video_tensor_is_converted_to_channel_last():
|
||||
np.testing.assert_array_equal(frames, video.permute(1, 2, 3, 0).numpy())
|
||||
|
||||
|
||||
def test_channel_first_video_tensor_uses_direct_planar_mux(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_planar_mux(frames, width, height, audio_waveform, **kwargs):
|
||||
calls.append((list(frames), width, height, audio_waveform, kwargs))
|
||||
return b"planar-video"
|
||||
|
||||
def fail_compat_mux(*args, **kwargs):
|
||||
raise AssertionError("CFHW-backed FHWC frames should use the planar path")
|
||||
|
||||
monkeypatch.setattr(media_utils, "mux_av_video_audio_bytes", fake_planar_mux)
|
||||
monkeypatch.setattr(media_utils, "mux_video_audio_bytes", fail_compat_mux)
|
||||
|
||||
video = torch.linspace(-1, 1, 3 * 2 * 4 * 6, dtype=torch.float32).reshape(3, 2, 4, 6)
|
||||
encoded = video_api_utils._encode_video_bytes(
|
||||
video,
|
||||
fps=12,
|
||||
)
|
||||
|
||||
assert encoded == b"planar-video"
|
||||
assert len(calls) == 1
|
||||
frames, width, height, audio, options = calls[0]
|
||||
assert (len(frames), width, height) == (2, 6, 4)
|
||||
assert all(frame.format.name == "gbrp" for frame in frames)
|
||||
assert audio is None
|
||||
assert options["fps"] == 12.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("video", "expected_path", "expected_reason", "expected_mux"),
|
||||
[
|
||||
(
|
||||
np.transpose(np.zeros((3, 2, 4, 4), dtype=np.float32), (1, 2, 3, 0)),
|
||||
"direct_planar",
|
||||
None,
|
||||
"direct",
|
||||
),
|
||||
(
|
||||
np.zeros((2, 4, 6, 3), dtype=np.float32),
|
||||
"legacy_fallback",
|
||||
"non_contiguous_rgb_planes",
|
||||
"legacy",
|
||||
),
|
||||
],
|
||||
ids=["automatic-direct", "interleaved-legacy-fallback"],
|
||||
)
|
||||
def test_audio_sample_rate_defaults_and_path_log(monkeypatch, video, expected_path, expected_reason, expected_mux):
|
||||
mux_calls = []
|
||||
path_logs = []
|
||||
default_rate_logs = []
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger,
|
||||
"info",
|
||||
lambda message, *args, **kwargs: path_logs.append(message % args),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger,
|
||||
"info_once",
|
||||
lambda message, *args, **kwargs: default_rate_logs.append(message % args),
|
||||
)
|
||||
|
||||
def fake_planar_mux(frames, width, height, audio_waveform, **kwargs):
|
||||
mux_calls.append({"kind": "direct", "audio_sample_rate": kwargs["audio_sample_rate"]})
|
||||
return b"planar-video"
|
||||
|
||||
def fake_compat_mux(frames, audio, **kwargs):
|
||||
mux_calls.append({"kind": "legacy", "audio_sample_rate": kwargs["audio_sample_rate"]})
|
||||
return b"legacy-video"
|
||||
|
||||
monkeypatch.setattr(media_utils, "mux_av_video_audio_bytes", fake_planar_mux)
|
||||
monkeypatch.setattr(media_utils, "mux_video_audio_bytes", fake_compat_mux)
|
||||
|
||||
encoded = video_api_utils._encode_video_bytes(
|
||||
video,
|
||||
fps=12,
|
||||
audio=np.zeros((1, 8), dtype=np.float32),
|
||||
)
|
||||
|
||||
assert encoded == (b"planar-video" if expected_mux == "direct" else b"legacy-video")
|
||||
assert mux_calls == [{"kind": expected_mux, "audio_sample_rate": 24000}]
|
||||
assert len(default_rate_logs) == 1
|
||||
assert "default sample rate of 24000 Hz" in default_rate_logs[0]
|
||||
assert len(path_logs) == 1
|
||||
assert f"selected_path={expected_path}" in path_logs[0]
|
||||
if expected_reason is not None:
|
||||
assert f"reason={expected_reason}" in path_logs[0]
|
||||
assert "frames=2" in path_logs[0]
|
||||
expected_shape = "(4, 4, 3)" if expected_mux == "direct" else "(4, 6, 3)"
|
||||
assert f"frame_shape={expected_shape}" in path_logs[0]
|
||||
assert "dtype=float32" in path_logs[0]
|
||||
assert "fps=12" in path_logs[0]
|
||||
assert "audio_present=True" in path_logs[0]
|
||||
assert "effective_audio_sample_rate=24000" in path_logs[0]
|
||||
|
||||
|
||||
def test_explicit_audio_sample_rate_bypasses_default_log(monkeypatch):
|
||||
log_messages = []
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger,
|
||||
"info_once",
|
||||
lambda message, *args, **kwargs: log_messages.append(message % args),
|
||||
)
|
||||
|
||||
audio = [[0.0, 1.0], [1.0, 0.0]]
|
||||
assert video_api_utils._resolve_audio_sample_rate(audio, 32000) == 32000
|
||||
assert log_messages == []
|
||||
|
||||
|
||||
def test_video_only_does_not_log_default_audio_sample_rate(monkeypatch):
|
||||
log_messages = []
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger,
|
||||
"info_once",
|
||||
lambda message, *args, **kwargs: log_messages.append(message % args),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
media_utils,
|
||||
"mux_video_audio_bytes",
|
||||
lambda frames, audio, **kwargs: b"legacy-video",
|
||||
)
|
||||
|
||||
video_api_utils._encode_video_bytes(np.zeros((1, 2, 2, 3), dtype=np.uint8), fps=12)
|
||||
|
||||
assert log_messages == []
|
||||
|
||||
|
||||
def test_interleaved_video_uses_legacy_fallback_automatically(monkeypatch):
|
||||
calls = []
|
||||
path_logs = []
|
||||
coerce_calls = []
|
||||
|
||||
real_coerce_video_to_frames = video_api_utils._coerce_video_to_frames
|
||||
|
||||
def count_coerce_video_to_frames(video):
|
||||
coerce_calls.append(video)
|
||||
return real_coerce_video_to_frames(video)
|
||||
|
||||
def fail_planar_mux(*args, **kwargs):
|
||||
raise AssertionError("interleaved FHWC frames must use the legacy path")
|
||||
|
||||
def fake_compat_mux(frames, audio, **kwargs):
|
||||
calls.append((frames, audio, kwargs))
|
||||
return b"legacy-video"
|
||||
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger, "info", lambda message, *args, **kwargs: path_logs.append(message % args)
|
||||
)
|
||||
monkeypatch.setattr(video_api_utils, "_coerce_video_to_frames", count_coerce_video_to_frames)
|
||||
monkeypatch.setattr(media_utils, "mux_av_video_audio_bytes", fail_planar_mux)
|
||||
monkeypatch.setattr(media_utils, "mux_video_audio_bytes", fake_compat_mux)
|
||||
|
||||
video = np.zeros((2, 4, 6, 3), dtype=np.float32)
|
||||
encoded = video_api_utils._encode_video_bytes(video, fps=12)
|
||||
|
||||
assert encoded == b"legacy-video"
|
||||
assert calls[0][0].shape == (2, 4, 6, 3)
|
||||
assert calls[0][0].dtype == np.uint8
|
||||
assert len(path_logs) == 1
|
||||
assert "selected_path=legacy_fallback" in path_logs[0]
|
||||
assert "reason=non_contiguous_rgb_planes" in path_logs[0]
|
||||
assert "audio_present=False" in path_logs[0]
|
||||
assert "effective_audio_sample_rate=None" in path_logs[0]
|
||||
assert len(coerce_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"video",
|
||||
[
|
||||
np.array(
|
||||
[[[[0, 1, 127], [128, 254, 255]], [[255, 127, 1], [2, 3, 4]]]],
|
||||
dtype=np.uint8,
|
||||
),
|
||||
np.array(
|
||||
[[[[-1.0, -0.5, 0.0], [0.5, 1.0, 2.0]], [[1.5, -2.0, 0.25], [0.75, 0.125, 0.875]]]],
|
||||
dtype=np.float32,
|
||||
),
|
||||
np.array(
|
||||
[[[[-512, -1, 0], [128, 255, 512]], [[1024, -256, 64], [127, 300, -300]]]],
|
||||
dtype=np.int16,
|
||||
),
|
||||
],
|
||||
ids=["uint8", "float-bounded-and-out-of-range", "signed-int-out-of-range"],
|
||||
)
|
||||
def test_prepared_automatic_fallback_preserves_legacy_quantization(monkeypatch, video):
|
||||
mux_inputs = []
|
||||
|
||||
def fail_planar_mux(*args, **kwargs):
|
||||
raise AssertionError("interleaved frames must use the legacy mux")
|
||||
|
||||
def fake_compat_mux(frames, audio, **kwargs):
|
||||
mux_inputs.append(frames.copy())
|
||||
return b"legacy-video"
|
||||
|
||||
monkeypatch.setattr(media_utils, "mux_av_video_audio_bytes", fail_planar_mux)
|
||||
monkeypatch.setattr(media_utils, "mux_video_audio_bytes", fake_compat_mux)
|
||||
|
||||
prepared_frames, frame_shape, _ = video_api_utils._prepare_video_frames(video)
|
||||
reference = np.rint(np.clip(np.stack([frame[..., :3] for frame in prepared_frames]), 0.0, 1.0) * 255.0).astype(
|
||||
np.uint8
|
||||
)
|
||||
|
||||
assert video_api_utils._encode_video_bytes(video, fps=12) == b"legacy-video"
|
||||
assert len(mux_inputs) == 1
|
||||
assert mux_inputs[0].dtype == np.uint8
|
||||
assert mux_inputs[0].shape == (len(prepared_frames), *frame_shape[:-1], 3)
|
||||
np.testing.assert_array_equal(mux_inputs[0], reference)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("video", "reason"),
|
||||
[
|
||||
(np.zeros((2, 4, 6), dtype=np.float32), "unsupported_shape"),
|
||||
(np.empty((2, 0, 6, 3), dtype=np.float32), "unsupported_shape"),
|
||||
(np.empty((2, 4, 0, 3), dtype=np.float32), "unsupported_shape"),
|
||||
(np.zeros((2, 2, 2, 3), dtype=np.complex64), "unsupported_dtype"),
|
||||
(np.zeros((2, 2, 2, 3), dtype=object), "unsupported_dtype"),
|
||||
(np.zeros((2, 2, 2, 3), dtype="U1"), "unsupported_dtype"),
|
||||
(np.zeros((2, 2, 2, 3), dtype="datetime64[D]"), "unsupported_dtype"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_direct_capability_uses_legacy_with_reason(monkeypatch, video, reason):
|
||||
events = []
|
||||
|
||||
def fake_legacy(*args, **kwargs):
|
||||
events.append("legacy")
|
||||
return b"legacy-video"
|
||||
|
||||
monkeypatch.setattr(video_api_utils, "_encode_prepared_video_bytes_legacy", fake_legacy)
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger,
|
||||
"info",
|
||||
lambda message, *args, **kwargs: events.append(message % args),
|
||||
)
|
||||
|
||||
assert video_api_utils._encode_video_bytes(video, fps=12) == b"legacy-video"
|
||||
assert events[-1] == "legacy"
|
||||
assert f"selected_path=legacy_fallback reason={reason}" in events[0]
|
||||
assert "frames=" in events[0]
|
||||
assert "fps=12" in events[0]
|
||||
assert "audio_present=False" in events[0]
|
||||
assert "effective_audio_sample_rate=None" in events[0]
|
||||
|
||||
|
||||
def test_non_contiguous_fallback_is_logged_before_legacy_mux(monkeypatch):
|
||||
events = []
|
||||
video = np.zeros((2, 4, 6, 3), dtype=np.float32)
|
||||
|
||||
def fake_legacy(*args, **kwargs):
|
||||
events.append("legacy")
|
||||
return b"legacy-video"
|
||||
|
||||
monkeypatch.setattr(video_api_utils, "_encode_prepared_video_bytes_legacy", fake_legacy)
|
||||
monkeypatch.setattr(
|
||||
video_api_utils.logger,
|
||||
"info",
|
||||
lambda message, *args, **kwargs: events.append(message % args),
|
||||
)
|
||||
|
||||
assert video_api_utils._encode_video_bytes(video, fps=12) == b"legacy-video"
|
||||
assert events[0].startswith("Video response encoding route selected: selected_path=legacy_fallback")
|
||||
assert events[1] == "legacy"
|
||||
|
||||
|
||||
def test_direct_mux_error_is_propagated_without_legacy_retry(monkeypatch):
|
||||
legacy_calls = []
|
||||
|
||||
def fail_direct(*args, **kwargs):
|
||||
raise RuntimeError("direct mux failed")
|
||||
|
||||
def fail_legacy(*args, **kwargs):
|
||||
legacy_calls.append((args, kwargs))
|
||||
raise AssertionError("direct failures must not retry through legacy")
|
||||
|
||||
monkeypatch.setattr(media_utils, "mux_av_video_audio_bytes", fail_direct)
|
||||
monkeypatch.setattr(video_api_utils, "_encode_prepared_video_bytes_legacy", fail_legacy)
|
||||
planar = np.transpose(np.zeros((3, 2, 4, 4), dtype=np.float32), (1, 2, 3, 0))
|
||||
|
||||
with pytest.raises(RuntimeError, match="direct mux failed"):
|
||||
video_api_utils._encode_video_bytes(planar, fps=12)
|
||||
|
||||
assert legacy_calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [np.float16, np.float32])
|
||||
@pytest.mark.parametrize("channels", [3, 4])
|
||||
def test_planar_frames_decode_with_bounded_rgb_quantization(dtype, channels):
|
||||
channel_values = np.array(
|
||||
[
|
||||
[[-0.1, 0.0, 0.1, 0.5, 0.9, 1.0, 1.1]],
|
||||
[[1.0, 0.9, 0.5, 0.1, 0.0, -0.1, 1.1]],
|
||||
[[0.25, 0.75, 0.501, 0.499, 0.125, 0.875, 0.5]],
|
||||
[[0.2, 0.4, 0.6, 0.8, 1.0, 0.0, 0.5]],
|
||||
],
|
||||
dtype=dtype,
|
||||
)[:channels]
|
||||
fhwc = np.transpose(channel_values[:, None, :, :], (1, 2, 3, 0))
|
||||
frames = list(video_api_utils._iter_planar_video_frames(list(fhwc), np.dtype(dtype)))
|
||||
|
||||
decoded = frames[0].to_ndarray(format="rgb24")
|
||||
expected = np.rint(np.clip(fhwc[0, ..., :3], 0.0, 1.0) * 255.0).astype(np.uint8)
|
||||
np.testing.assert_array_equal(decoded, expected)
|
||||
|
||||
|
||||
def test_planar_frame_writes_gbr_planes_and_clears_padding():
|
||||
width = 17
|
||||
planar = np.stack(
|
||||
[
|
||||
np.full((2, width), 11, dtype=np.uint8),
|
||||
np.full((2, width), 22, dtype=np.uint8),
|
||||
np.full((2, width), 33, dtype=np.uint8),
|
||||
]
|
||||
)
|
||||
fhwc = np.transpose(planar[:, None, :, :], (1, 2, 3, 0))
|
||||
|
||||
frame = next(video_api_utils._iter_planar_video_frames(list(fhwc), np.dtype(np.uint8)))
|
||||
|
||||
for plane, expected in zip(frame.planes, (22, 33, 11)):
|
||||
plane_data = np.frombuffer(plane, dtype=np.uint8).reshape(plane.height, plane.line_size)
|
||||
np.testing.assert_array_equal(plane_data[:2, :width], expected)
|
||||
np.testing.assert_array_equal(plane_data[:2, width:], 0)
|
||||
np.testing.assert_array_equal(plane_data[2:], 0)
|
||||
|
||||
|
||||
def test_planar_bool_frames_match_bounded_compatible_output():
|
||||
planar = np.array(
|
||||
[
|
||||
[[[False, True], [True, False]]],
|
||||
[[[True, False], [True, False]]],
|
||||
[[[False, False], [True, True]]],
|
||||
],
|
||||
dtype=np.bool_,
|
||||
)
|
||||
direct_video = np.transpose(planar, (1, 2, 3, 0))
|
||||
compatible_video = np.ascontiguousarray(direct_video)
|
||||
|
||||
prepared_frames, _, common_dtype = video_api_utils._prepare_video_frames(direct_video)
|
||||
direct_frames = list(video_api_utils._iter_planar_video_frames(prepared_frames, common_dtype))
|
||||
decoded_direct = np.stack([frame.to_ndarray(format="rgb24") for frame in direct_frames])
|
||||
bounded_compatible = video_api_utils._coerce_video_to_uint8_frames(compatible_video)
|
||||
|
||||
np.testing.assert_array_equal(decoded_direct, bounded_compatible)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("plane_height", "line_size"),
|
||||
[(1, 2), (2, 1)],
|
||||
)
|
||||
def test_planar_frame_rejects_undersized_plane(monkeypatch, plane_height, line_size):
|
||||
class FakePlane:
|
||||
pass
|
||||
|
||||
plane = FakePlane()
|
||||
plane.height = plane_height
|
||||
plane.line_size = line_size
|
||||
|
||||
class FakeFrame:
|
||||
planes = [plane]
|
||||
|
||||
monkeypatch.setattr(av, "VideoFrame", lambda *args, **kwargs: FakeFrame())
|
||||
planar = np.zeros((3, 1, 2, 2), dtype=np.uint8)
|
||||
fhwc = np.transpose(planar, (1, 2, 3, 0))
|
||||
|
||||
with pytest.raises(ValueError, match="smaller than"):
|
||||
next(video_api_utils._iter_planar_video_frames(list(fhwc), np.dtype(np.uint8)))
|
||||
|
||||
|
||||
def test_unequal_frame_shapes_fail_before_output_allocation(monkeypatch):
|
||||
def fail_empty(*args, **kwargs):
|
||||
raise AssertionError("output allocation must happen after shape validation")
|
||||
|
||||
monkeypatch.setattr(video_api_utils.np, "empty", fail_empty)
|
||||
|
||||
with pytest.raises(ValueError, match="same shape"):
|
||||
video_api_utils._encode_video_bytes(
|
||||
[np.zeros((2, 3, 3), dtype=np.float32), np.zeros((3, 3, 3), dtype=np.float32)],
|
||||
fps=12,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("with_audio", [False, True])
|
||||
def test_direct_and_compatible_paths_produce_equivalent_mp4(with_audio):
|
||||
rng = np.random.default_rng(7)
|
||||
planar = rng.random((3, 3, 32, 32), dtype=np.float32)
|
||||
direct_video = np.transpose(planar, (1, 2, 3, 0))
|
||||
compatible_video = np.ascontiguousarray(direct_video)
|
||||
audio = np.zeros((2, 2400), dtype=np.float32) if with_audio else None
|
||||
|
||||
direct_bytes = video_api_utils._encode_video_bytes(
|
||||
direct_video,
|
||||
fps=12,
|
||||
audio=audio,
|
||||
audio_sample_rate=24000,
|
||||
video_codec_options={"preset": "ultrafast", "threads": "1"},
|
||||
)
|
||||
compatible_bytes = video_api_utils._encode_video_bytes(
|
||||
compatible_video,
|
||||
fps=12,
|
||||
audio=audio,
|
||||
audio_sample_rate=24000,
|
||||
video_codec_options={"preset": "ultrafast", "threads": "1"},
|
||||
)
|
||||
|
||||
def decode(mp4_bytes):
|
||||
with av.open(BytesIO(mp4_bytes), mode="r", format="mp4") as container:
|
||||
stream_types = [stream.type for stream in container.streams]
|
||||
video_stream = container.streams.video[0]
|
||||
frames = np.stack([frame.to_ndarray(format="rgb24") for frame in container.decode(video_stream)])
|
||||
return stream_types, frames
|
||||
|
||||
direct_streams, direct_frames = decode(direct_bytes)
|
||||
compatible_streams, compatible_frames = decode(compatible_bytes)
|
||||
assert direct_bytes == compatible_bytes
|
||||
assert direct_streams == compatible_streams
|
||||
assert ("audio" in direct_streams) is with_audio
|
||||
np.testing.assert_array_equal(direct_frames, compatible_frames)
|
||||
|
||||
|
||||
def test_mux_closes_container_and_preserves_generator_error(monkeypatch):
|
||||
class GeneratorError(RuntimeError):
|
||||
pass
|
||||
|
||||
class FakeStream:
|
||||
options = None
|
||||
|
||||
def encode(self, frame=None):
|
||||
return []
|
||||
|
||||
class FakeContainer:
|
||||
def __init__(self):
|
||||
self.closed = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.closed = True
|
||||
return False
|
||||
|
||||
def add_stream(self, codec, rate):
|
||||
return FakeStream()
|
||||
|
||||
def mux(self, packet):
|
||||
pass
|
||||
|
||||
container = FakeContainer()
|
||||
monkeypatch.setattr(media_utils.av, "open", lambda *args, **kwargs: container)
|
||||
|
||||
def failing_frames():
|
||||
yield object()
|
||||
raise GeneratorError("frame generation failed")
|
||||
|
||||
with pytest.raises(GeneratorError, match="frame generation failed"):
|
||||
media_utils.mux_av_video_audio_bytes(failing_frames(), width=2, height=2)
|
||||
assert container.closed
|
||||
|
||||
|
||||
def test_fragmented_mp4_video_encoder_reuses_single_muxer(monkeypatch):
|
||||
muxers = []
|
||||
|
||||
|
||||
@@ -89,6 +89,56 @@ class FakeAsyncOmni:
|
||||
yield MockVideoResult(videos)
|
||||
|
||||
|
||||
def test_raw_and_base64_encoders_receive_no_policy_config(mocker: MockerFixture):
|
||||
engine = FakeAsyncOmni()
|
||||
handler = OmniOpenAIServingVideo.for_diffusion(
|
||||
engine,
|
||||
model_name="test-model",
|
||||
)
|
||||
raw_encoder = mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"encoded-video",
|
||||
)
|
||||
base64_encoder = mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video.encode_video_base64",
|
||||
return_value="encoded-video",
|
||||
)
|
||||
|
||||
async def _generate_both_response_types():
|
||||
request = VideoGenerationRequest(prompt="test prompt")
|
||||
await handler.generate_video_bytes(request, "raw-request")
|
||||
await handler.generate_videos(request, "base64-request")
|
||||
|
||||
asyncio.run(_generate_both_response_types())
|
||||
|
||||
assert "encoding_config" not in raw_encoder.call_args.kwargs
|
||||
assert "encoding_config" not in base64_encoder.call_args.kwargs
|
||||
|
||||
|
||||
def test_resolve_diffusion_od_config_falls_back_to_attribute():
|
||||
od_config = SimpleNamespace(model_class_name="WanPipeline")
|
||||
handler = OmniOpenAIServingVideo.for_diffusion(
|
||||
SimpleNamespace(od_config=od_config),
|
||||
model_name="test-model",
|
||||
)
|
||||
|
||||
assert handler._resolve_diffusion_od_config() is od_config
|
||||
|
||||
|
||||
def test_resolve_diffusion_od_config_prefers_getter_over_attribute():
|
||||
attribute_config = SimpleNamespace(model_class_name="WanPipeline")
|
||||
getter_config = SimpleNamespace(model_class_name="MiniMaxH3Pipeline")
|
||||
handler = OmniOpenAIServingVideo.for_diffusion(
|
||||
SimpleNamespace(
|
||||
od_config=attribute_config,
|
||||
get_diffusion_od_config=lambda: getter_config,
|
||||
),
|
||||
model_name="test-model",
|
||||
)
|
||||
|
||||
assert handler._resolve_diffusion_od_config() is getter_config
|
||||
|
||||
|
||||
class BlockingVideoHandler:
|
||||
def __init__(self):
|
||||
self.model_name = "Wan-AI/Wan2.2-T2V-A14B-Diffusers"
|
||||
@@ -1157,7 +1207,13 @@ def test_worker_fps_multiplier_is_applied_to_async_encoding(test_client, mocker:
|
||||
def test_audio_sample_rate_comes_from_model_config(test_client, mocker: MockerFixture):
|
||||
audio_sample_rates = []
|
||||
|
||||
def _fake_encode(video, fps, audio=None, audio_sample_rate=None, video_codec_options=None):
|
||||
def _fake_encode(
|
||||
video,
|
||||
fps,
|
||||
audio=None,
|
||||
audio_sample_rate=None,
|
||||
video_codec_options=None,
|
||||
):
|
||||
del video, fps, audio, video_codec_options
|
||||
audio_sample_rates.append(audio_sample_rate)
|
||||
return b"fake-video"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from collections.abc import Iterable
|
||||
from fractions import Fraction
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -200,3 +201,70 @@ def mux_video_audio_bytes(
|
||||
|
||||
container.close()
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def mux_av_video_audio_bytes(
|
||||
video_frames: Iterable[av.VideoFrame],
|
||||
width: int,
|
||||
height: int,
|
||||
audio_waveform: np.ndarray | None = None,
|
||||
*,
|
||||
fps: float = 25.0,
|
||||
audio_sample_rate: int = 44100,
|
||||
video_codec: str = "h264",
|
||||
audio_codec: str = "aac",
|
||||
crf: str = "18",
|
||||
video_codec_options: dict[str, str] | None = None,
|
||||
) -> bytes:
|
||||
"""Mux preconstructed video frames and optional audio into MP4 bytes."""
|
||||
buf = io.BytesIO()
|
||||
with cast(Any, av.open(buf, mode="w", format="mp4")) as container:
|
||||
v_stream = cast(
|
||||
av.VideoStream,
|
||||
container.add_stream(video_codec, rate=Fraction(fps).limit_denominator(10000)),
|
||||
)
|
||||
v_stream.width = width
|
||||
v_stream.height = height
|
||||
v_stream.pix_fmt = "yuv420p"
|
||||
|
||||
options: dict[str, object] = {"crf": str(crf)}
|
||||
if video_codec_options:
|
||||
options.update(video_codec_options)
|
||||
v_stream.options = options
|
||||
|
||||
a_stream: av.AudioStream | None = None
|
||||
samples: np.ndarray | None = None
|
||||
layout: str | None = None
|
||||
if audio_waveform is not None:
|
||||
samples = audio_waveform.astype(np.float32)
|
||||
if samples.ndim == 1:
|
||||
samples = samples.reshape(1, -1)
|
||||
elif samples.ndim == 2 and samples.shape[0] > samples.shape[1]:
|
||||
samples = np.ascontiguousarray(samples.T)
|
||||
num_channels = samples.shape[0]
|
||||
layout = "stereo" if num_channels >= 2 else "mono"
|
||||
a_stream = cast(av.AudioStream, container.add_stream(audio_codec, rate=audio_sample_rate))
|
||||
a_stream.layout = layout
|
||||
|
||||
for frame in video_frames:
|
||||
for packet in v_stream.encode(frame):
|
||||
container.mux(packet)
|
||||
for packet in v_stream.encode():
|
||||
container.mux(packet)
|
||||
|
||||
if a_stream is not None and audio_waveform is not None:
|
||||
if samples is None or layout is None:
|
||||
raise ValueError("Audio samples were not prepared for muxing.")
|
||||
audio_frame = av.AudioFrame.from_ndarray(samples, format="fltp", layout=layout)
|
||||
audio_frame.sample_rate = audio_sample_rate
|
||||
# AAC has a one-frame encoder delay. Mark the input waveform as
|
||||
# starting at t=0 so the MP4 muxer writes the corresponding negative
|
||||
# priming timestamp instead of exposing the delay as leading silence.
|
||||
audio_frame.pts = 0
|
||||
audio_frame.time_base = Fraction(1, audio_sample_rate)
|
||||
for packet in a_stream.encode(audio_frame):
|
||||
container.mux(packet)
|
||||
for packet in a_stream.encode():
|
||||
container.mux(packet)
|
||||
|
||||
return buf.getvalue()
|
||||
@@ -823,7 +823,6 @@ class OmniServeCommand(CLISubcommand):
|
||||
default=False,
|
||||
help="Enable chunked streaming output for diffusion (mainly video generation) models that support it.",
|
||||
)
|
||||
|
||||
# TTS-specific parameters
|
||||
omni_config_group.add_argument(
|
||||
"--tts-max-instructions-length",
|
||||
|
||||
@@ -9,7 +9,8 @@ from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from http import HTTPStatus
|
||||
from typing import Any, cast
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from fastapi import HTTPException
|
||||
from PIL import Image
|
||||
@@ -29,7 +30,10 @@ from vllm_omni.entrypoints.openai.stage_params import (
|
||||
get_default_sampling_params_list,
|
||||
)
|
||||
from vllm_omni.entrypoints.openai.utils import get_stage_type, parse_lora_request
|
||||
from vllm_omni.entrypoints.openai.video_api_utils import _encode_video_bytes, encode_video_base64
|
||||
from vllm_omni.entrypoints.openai.video_api_utils import (
|
||||
_encode_video_bytes,
|
||||
encode_video_base64,
|
||||
)
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
||||
from vllm_omni.model_extras import should_preserve_reference_image_size
|
||||
from vllm_omni.outputs.output_metadata import (
|
||||
@@ -40,6 +44,9 @@ from vllm_omni.outputs.output_metadata import (
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.diffusion.data import OmniDiffusionConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReferenceImage:
|
||||
@@ -90,6 +97,12 @@ class OmniOpenAIServingVideo:
|
||||
self._model_name = model_name
|
||||
self._stage_configs = stage_configs
|
||||
|
||||
def _resolve_diffusion_od_config(self) -> OmniDiffusionConfig | SimpleNamespace | None:
|
||||
get_od_config = getattr(self._engine_client, "get_diffusion_od_config", None)
|
||||
if callable(get_od_config):
|
||||
return get_od_config()
|
||||
return getattr(self._engine_client, "od_config", None)
|
||||
|
||||
@property
|
||||
def model_name(self) -> str | None:
|
||||
return self._model_name
|
||||
@@ -105,8 +118,7 @@ class OmniOpenAIServingVideo:
|
||||
@cached_property
|
||||
def preserves_reference_image_size(self) -> bool:
|
||||
"""Return whether the active pipeline owns reference-image resizing."""
|
||||
get_od_config = getattr(self._engine_client, "get_diffusion_od_config", None)
|
||||
od_config = get_od_config() if callable(get_od_config) else getattr(self._engine_client, "od_config", None)
|
||||
od_config = self._resolve_diffusion_od_config()
|
||||
model_class_name = None if od_config is None else getattr(od_config, "model_class_name", None)
|
||||
model = getattr(od_config, "model", None) if od_config is not None else None
|
||||
model = model or self.model_name
|
||||
@@ -120,8 +132,7 @@ class OmniOpenAIServingVideo:
|
||||
@property
|
||||
def supports_mixed_reference_inputs(self) -> bool:
|
||||
"""Return whether the configured diffusion model accepts mixed refs."""
|
||||
get_od_config = getattr(self._engine_client, "get_diffusion_od_config", None)
|
||||
od_config = get_od_config() if callable(get_od_config) else getattr(self._engine_client, "od_config", None)
|
||||
od_config = self._resolve_diffusion_od_config()
|
||||
if od_config is None:
|
||||
return False
|
||||
|
||||
@@ -332,7 +343,11 @@ class OmniOpenAIServingVideo:
|
||||
video_data = [
|
||||
VideoData(
|
||||
b64_json=(
|
||||
encode_video_base64(video, fps=artifacts.output_fps, video_codec_options=video_codec_options)
|
||||
encode_video_base64(
|
||||
video,
|
||||
fps=artifacts.output_fps,
|
||||
video_codec_options=video_codec_options,
|
||||
)
|
||||
if artifacts.audios[idx] is None
|
||||
else encode_video_base64(
|
||||
video,
|
||||
|
||||
@@ -10,14 +10,16 @@ import base64
|
||||
import binascii
|
||||
import os
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from io import BytesIO
|
||||
from typing import Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
import httpx
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from vllm import envs
|
||||
from vllm.logger import init_logger
|
||||
from vllm.multimodal.video import (
|
||||
VIDEO_LOADER_REGISTRY,
|
||||
VideoBackend,
|
||||
@@ -35,6 +37,20 @@ from vllm_omni.entrypoints.openai.protocol.videos import (
|
||||
VideoReference,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import av
|
||||
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
DEFAULT_AUDIO_SAMPLE_RATE = 24_000
|
||||
|
||||
|
||||
VideoInput = torch.Tensor | np.ndarray | list[torch.Tensor | np.ndarray | Image.Image]
|
||||
AudioSample = int | float
|
||||
AudioInput = torch.Tensor | np.ndarray | list[AudioSample] | list[list[AudioSample]]
|
||||
|
||||
|
||||
class VideoFrames(list[Image.Image]):
|
||||
"""Decoded video frames plus source metadata."""
|
||||
@@ -398,7 +414,7 @@ def _normalize_single_video_array(video_array: np.ndarray) -> np.ndarray:
|
||||
video_array = np.transpose(video_array, (0, 2, 3, 1))
|
||||
|
||||
if np.issubdtype(video_array.dtype, np.floating):
|
||||
if video_array.min() < 0.0 or video_array.max() > 1.0:
|
||||
if video_array.size and (video_array.min() < 0.0 or video_array.max() > 1.0):
|
||||
video_array = np.clip(video_array, -1.0, 1.0) * 0.5 + 0.5
|
||||
elif np.issubdtype(video_array.dtype, np.integer):
|
||||
video_array = video_array.astype(np.float32) / 255.0
|
||||
@@ -432,7 +448,7 @@ def _normalize_frames(frames: list[Any]) -> list[np.ndarray]:
|
||||
frame_array = np.transpose(frame_array, (1, 2, 0))
|
||||
|
||||
if np.issubdtype(frame_array.dtype, np.floating):
|
||||
if frame_array.min() < 0.0 or frame_array.max() > 1.0:
|
||||
if frame_array.size and (frame_array.min() < 0.0 or frame_array.max() > 1.0):
|
||||
frame_array = np.clip(frame_array, -1.0, 1.0) * 0.5 + 0.5
|
||||
elif np.issubdtype(frame_array.dtype, np.integer):
|
||||
frame_array = frame_array.astype(np.float32) / 255.0
|
||||
@@ -486,17 +502,29 @@ def _coerce_audio_to_numpy(audio: Any) -> np.ndarray:
|
||||
return arr.astype(np.float32)
|
||||
|
||||
|
||||
def _coerce_video_to_uint8_frames(video: Any) -> np.ndarray:
|
||||
"""Convert a video payload into contiguous uint8 frames shaped (F, H, W, 3)."""
|
||||
def _prepare_video_frames(video: Any) -> tuple[list[np.ndarray], tuple[int, ...], np.dtype]:
|
||||
"""Normalize and validate frames for the common video encoding dispatcher."""
|
||||
frames = _coerce_video_to_frames(video)
|
||||
if not frames:
|
||||
raise ValueError("No frames found to encode.")
|
||||
|
||||
frame_shape = frames[0].shape
|
||||
if any(frame.shape != frame_shape for frame in frames[1:]):
|
||||
raise ValueError("All video frames must have the same shape.")
|
||||
|
||||
common_dtype = np.result_type(*(frame.dtype for frame in frames))
|
||||
return frames, frame_shape, common_dtype
|
||||
|
||||
|
||||
def _coerce_prepared_video_to_uint8_frames(
|
||||
frames: list[np.ndarray],
|
||||
frame_shape: tuple[int, ...],
|
||||
common_dtype: np.dtype,
|
||||
) -> np.ndarray:
|
||||
"""Convert prepared frames into contiguous uint8 frames for the legacy muxer."""
|
||||
has_alpha = len(frame_shape) == 3 and frame_shape[-1] == 4
|
||||
output_shape = (*frame_shape[:-1], 3) if has_alpha else frame_shape
|
||||
frames_u8 = np.empty((len(frames), *output_shape), dtype=np.uint8)
|
||||
common_dtype = np.result_type(*(frame.dtype for frame in frames))
|
||||
|
||||
# Convert one frame at a time instead of stacking the normalized float
|
||||
# payload first. Long videos can otherwise require another full-size
|
||||
@@ -520,6 +548,145 @@ def _coerce_video_to_uint8_frames(video: Any) -> np.ndarray:
|
||||
return frames_u8
|
||||
|
||||
|
||||
def _coerce_video_to_uint8_frames(video: Any) -> np.ndarray:
|
||||
"""Convert a video payload into contiguous uint8 frames shaped (F, H, W, 3)."""
|
||||
frames, frame_shape, common_dtype = _prepare_video_frames(video)
|
||||
return _coerce_prepared_video_to_uint8_frames(frames, frame_shape, common_dtype)
|
||||
|
||||
|
||||
def _direct_planar_fallback_reason(
|
||||
frames: list[np.ndarray],
|
||||
frame_shape: tuple[int, ...],
|
||||
common_dtype: np.dtype,
|
||||
) -> str | None:
|
||||
"""Return a stable reason when direct planar muxing cannot consume frames."""
|
||||
if len(frame_shape) != 3 or frame_shape[0] <= 0 or frame_shape[1] <= 0 or frame_shape[2] not in (3, 4):
|
||||
return "unsupported_shape"
|
||||
|
||||
if not (
|
||||
common_dtype == np.dtype(np.uint8)
|
||||
or np.issubdtype(common_dtype, np.bool_)
|
||||
or np.issubdtype(common_dtype, np.floating)
|
||||
):
|
||||
return "unsupported_dtype"
|
||||
|
||||
if not all(frame[..., channel].flags.c_contiguous for frame in frames for channel in range(3)):
|
||||
return "non_contiguous_rgb_planes"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _log_video_encoding_path(
|
||||
*,
|
||||
selected_path: str,
|
||||
frames: list[np.ndarray],
|
||||
frame_shape: tuple[int, ...],
|
||||
common_dtype: np.dtype,
|
||||
fps: int,
|
||||
audio: AudioInput | None,
|
||||
audio_sample_rate: int | None,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
reason_field = "" if reason is None else f" reason={reason}"
|
||||
logger.info(
|
||||
"Video response encoding route selected: selected_path=%s%s frames=%d frame_shape=%s dtype=%s fps=%s "
|
||||
"audio_present=%s effective_audio_sample_rate=%s",
|
||||
selected_path,
|
||||
reason_field,
|
||||
len(frames),
|
||||
frame_shape,
|
||||
np.dtype(common_dtype).name,
|
||||
fps,
|
||||
audio is not None,
|
||||
audio_sample_rate,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_audio_sample_rate(audio: AudioInput | None, audio_sample_rate: int | None) -> int:
|
||||
if audio is not None and audio_sample_rate is None:
|
||||
logger.info_once(
|
||||
"Audio sample rate was not provided; using default sample rate of %s Hz.",
|
||||
DEFAULT_AUDIO_SAMPLE_RATE,
|
||||
)
|
||||
return audio_sample_rate or DEFAULT_AUDIO_SAMPLE_RATE
|
||||
|
||||
|
||||
def _iter_planar_video_frames(
|
||||
frames: list[np.ndarray],
|
||||
common_dtype: np.dtype,
|
||||
) -> Iterator[av.VideoFrame]:
|
||||
"""Yield planar PyAV frames while retaining only one channel scratch buffer."""
|
||||
import av
|
||||
|
||||
height, width = frames[0].shape[:2]
|
||||
scratch_dtype = np.float64 if np.issubdtype(common_dtype, np.bool_) else common_dtype
|
||||
scratch = None if common_dtype == np.uint8 else np.empty((height, width), dtype=scratch_dtype)
|
||||
|
||||
for frame in frames:
|
||||
av_frame = av.VideoFrame(width, height, format="gbrp")
|
||||
for plane, channel in zip(av_frame.planes, (1, 2, 0)):
|
||||
if plane.height < height or plane.line_size < width:
|
||||
raise ValueError("PyAV video plane is smaller than the requested frame dimensions.")
|
||||
plane_view = np.frombuffer(
|
||||
plane,
|
||||
dtype=np.uint8,
|
||||
count=plane.height * plane.line_size,
|
||||
).reshape(plane.height, plane.line_size)
|
||||
plane_view.fill(0)
|
||||
if frame.dtype == np.uint8:
|
||||
plane_view[:height, :width] = frame[..., channel]
|
||||
else:
|
||||
scratch_buffer = cast(np.ndarray, scratch)
|
||||
np.copyto(scratch_buffer, frame[..., channel], casting="unsafe")
|
||||
np.clip(scratch_buffer, 0.0, 1.0, out=scratch_buffer)
|
||||
scratch_buffer *= 255.0
|
||||
np.rint(scratch_buffer, out=scratch_buffer)
|
||||
plane_view[:height, :width] = scratch_buffer
|
||||
yield av_frame
|
||||
|
||||
|
||||
def _encode_prepared_video_bytes_legacy(
|
||||
frames: list[np.ndarray],
|
||||
frame_shape: tuple[int, ...],
|
||||
common_dtype: np.dtype,
|
||||
fps: int,
|
||||
audio: Any | None = None,
|
||||
audio_sample_rate: int | None = None,
|
||||
video_codec_options: dict[str, str] | None = None,
|
||||
) -> bytes:
|
||||
"""Encode validated frames through the compatibility path used before planar encoding."""
|
||||
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
|
||||
|
||||
audio_np = _coerce_audio_to_numpy(audio) if audio is not None else None
|
||||
return mux_video_audio_bytes(
|
||||
_coerce_prepared_video_to_uint8_frames(frames, frame_shape, common_dtype),
|
||||
audio_np,
|
||||
fps=float(fps),
|
||||
audio_sample_rate=audio_sample_rate or DEFAULT_AUDIO_SAMPLE_RATE,
|
||||
video_codec_options=video_codec_options,
|
||||
)
|
||||
|
||||
|
||||
def _encode_video_bytes_legacy(
|
||||
video: Any,
|
||||
fps: int,
|
||||
audio: Any | None = None,
|
||||
audio_sample_rate: int | None = None,
|
||||
video_codec_options: dict[str, str] | None = None,
|
||||
) -> bytes:
|
||||
"""Encode through the compatibility path used before planar encoding."""
|
||||
frames, frame_shape, common_dtype = _prepare_video_frames(video)
|
||||
return _encode_prepared_video_bytes_legacy(
|
||||
frames,
|
||||
frame_shape,
|
||||
common_dtype,
|
||||
fps,
|
||||
audio=audio,
|
||||
audio_sample_rate=_resolve_audio_sample_rate(audio, audio_sample_rate),
|
||||
video_codec_options=video_codec_options,
|
||||
)
|
||||
|
||||
|
||||
def _encode_video_bytes(
|
||||
video: Any,
|
||||
fps: int,
|
||||
@@ -527,16 +694,52 @@ def _encode_video_bytes(
|
||||
audio_sample_rate: int | None = None,
|
||||
video_codec_options: dict[str, str] | None = None,
|
||||
) -> bytes:
|
||||
"""Encode a video payload into MP4 bytes, optionally muxing audio."""
|
||||
from vllm_omni.diffusion.utils.media_utils import mux_video_audio_bytes
|
||||
"""Encode a video payload through the automatic capability dispatcher."""
|
||||
from vllm_omni.diffusion.utils.media_utils import mux_av_video_audio_bytes
|
||||
|
||||
# Prepare once so validation is shared by both paths and malformed common
|
||||
# input is reported before any muxer is opened.
|
||||
frames, frame_shape, common_dtype = _prepare_video_frames(video)
|
||||
effective_audio_sample_rate = _resolve_audio_sample_rate(audio, audio_sample_rate) if audio is not None else None
|
||||
fallback_reason = _direct_planar_fallback_reason(frames, frame_shape, common_dtype)
|
||||
if fallback_reason is not None:
|
||||
_log_video_encoding_path(
|
||||
selected_path="legacy_fallback",
|
||||
reason=fallback_reason,
|
||||
frames=frames,
|
||||
frame_shape=frame_shape,
|
||||
common_dtype=common_dtype,
|
||||
fps=fps,
|
||||
audio=audio,
|
||||
audio_sample_rate=effective_audio_sample_rate,
|
||||
)
|
||||
return _encode_prepared_video_bytes_legacy(
|
||||
frames,
|
||||
frame_shape,
|
||||
common_dtype,
|
||||
fps,
|
||||
audio=audio,
|
||||
audio_sample_rate=effective_audio_sample_rate,
|
||||
video_codec_options=video_codec_options,
|
||||
)
|
||||
|
||||
_log_video_encoding_path(
|
||||
selected_path="direct_planar",
|
||||
frames=frames,
|
||||
frame_shape=frame_shape,
|
||||
common_dtype=common_dtype,
|
||||
fps=fps,
|
||||
audio=audio,
|
||||
audio_sample_rate=effective_audio_sample_rate,
|
||||
)
|
||||
audio_np = _coerce_audio_to_numpy(audio) if audio is not None else None
|
||||
|
||||
return mux_video_audio_bytes(
|
||||
_coerce_video_to_uint8_frames(video),
|
||||
audio_np,
|
||||
return mux_av_video_audio_bytes(
|
||||
_iter_planar_video_frames(frames, common_dtype),
|
||||
width=frame_shape[1],
|
||||
height=frame_shape[0],
|
||||
audio_waveform=audio_np,
|
||||
fps=float(fps),
|
||||
audio_sample_rate=audio_sample_rate or 24000,
|
||||
audio_sample_rate=effective_audio_sample_rate,
|
||||
video_codec_options=video_codec_options,
|
||||
)
|
||||
|
||||
@@ -599,6 +802,10 @@ def encode_video_base64(
|
||||
) -> str:
|
||||
"""Encode a video (frames/array/tensor) to base64 MP4."""
|
||||
video_bytes = _encode_video_bytes(
|
||||
video, fps=fps, audio=audio, audio_sample_rate=audio_sample_rate, video_codec_options=video_codec_options
|
||||
video,
|
||||
fps=fps,
|
||||
audio=audio,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
video_codec_options=video_codec_options,
|
||||
)
|
||||
return base64.b64encode(video_bytes).decode("utf-8")
|
||||
Reference in new issue
Block a user