[Feature] feat: add video frame interpolation postprocess (#2555)
Signed-off-by: David Chen <530634352@qq.com>
This commit is contained in:
1 parent
61a3cbdff5
commit
6c6551dff8
17 files changed
+961
-9
No files matched your search
@@ -0,0 +1,92 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for OpenAI-compatible video API encoding helpers."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.postprocess import rife_interpolator
|
||||
from vllm_omni.entrypoints.openai import video_api_utils
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _install_fake_video_mux(monkeypatch, mux_calls):
|
||||
def _fake_mux_video_audio_bytes(frames, audio, fps, audio_sample_rate, video_codec_options=None):
|
||||
mux_calls.append(
|
||||
{
|
||||
"frames": frames,
|
||||
"audio": audio,
|
||||
"fps": fps,
|
||||
"audio_sample_rate": audio_sample_rate,
|
||||
"video_codec_options": video_codec_options,
|
||||
}
|
||||
)
|
||||
return b"fake-video"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vllm_omni.diffusion.utils.media_utils.mux_video_audio_bytes",
|
||||
_fake_mux_video_audio_bytes,
|
||||
)
|
||||
|
||||
|
||||
def test_encode_video_bytes_exports_frames_without_interpolation(monkeypatch):
|
||||
mux_calls = []
|
||||
_install_fake_video_mux(monkeypatch, mux_calls)
|
||||
|
||||
frames = [np.full((2, 2, 3), fill_value=i / 5, dtype=np.float32) for i in range(5)]
|
||||
video_bytes = video_api_utils._encode_video_bytes(
|
||||
frames,
|
||||
fps=8,
|
||||
)
|
||||
|
||||
assert video_bytes == b"fake-video"
|
||||
assert mux_calls[0]["frames"].shape == (5, 2, 2, 3)
|
||||
assert mux_calls[0]["frames"].dtype == np.uint8
|
||||
assert mux_calls[0]["fps"] == 8.0
|
||||
assert mux_calls[0]["audio"] is None
|
||||
|
||||
|
||||
def test_rife_model_inference_runs_on_dummy_tensors():
|
||||
model = rife_interpolator.Model().eval()
|
||||
img0 = torch.rand(1, 3, 32, 32)
|
||||
img1 = torch.rand(1, 3, 32, 32)
|
||||
|
||||
output = model.inference(img0, img1, scale=1.0)
|
||||
|
||||
assert output.shape == (1, 3, 32, 32)
|
||||
assert torch.isfinite(output).all()
|
||||
|
||||
|
||||
def test_frame_interpolator_runs_actual_torch_tensor_path(monkeypatch):
|
||||
model = rife_interpolator.Model().eval()
|
||||
interpolator = rife_interpolator.FrameInterpolator()
|
||||
monkeypatch.setattr(interpolator, "_ensure_model_loaded", lambda preferred_device=None: model)
|
||||
|
||||
video = torch.zeros(1, 3, 2, 32, 32)
|
||||
output_video, multiplier = interpolator.interpolate_tensor(video, exp=1, scale=1.0)
|
||||
|
||||
assert multiplier == 2
|
||||
assert output_video.shape == (1, 3, 3, 32, 32)
|
||||
assert torch.isfinite(output_video).all()
|
||||
|
||||
|
||||
def test_frame_interpolator_prefers_input_tensor_device(monkeypatch):
|
||||
chosen_devices = []
|
||||
model = rife_interpolator.Model().eval()
|
||||
|
||||
def _fake_ensure_model_loaded(*, preferred_device=None):
|
||||
chosen_devices.append(preferred_device)
|
||||
return model
|
||||
|
||||
interpolator = rife_interpolator.FrameInterpolator()
|
||||
monkeypatch.setattr(interpolator, "_ensure_model_loaded", _fake_ensure_model_loaded)
|
||||
monkeypatch.setattr(model.flownet, "to", lambda device: model.flownet)
|
||||
|
||||
video = torch.zeros(1, 3, 2, 32, 32)
|
||||
output_video, multiplier = interpolator.interpolate_tensor(video, exp=1, scale=1.0)
|
||||
|
||||
assert chosen_devices == [video.device]
|
||||
assert multiplier == 2
|
||||
assert output_video.shape == (1, 3, 3, 32, 32)
|
||||
@@ -34,15 +34,28 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
class MockVideoResult:
|
||||
def __init__(self, videos, audios=None, sample_rate=None, stage_durations=None, peak_memory_mb=0.0):
|
||||
def __init__(
|
||||
self,
|
||||
videos,
|
||||
audios=None,
|
||||
sample_rate=None,
|
||||
custom_output=None,
|
||||
stage_durations=None,
|
||||
peak_memory_mb=0.0,
|
||||
):
|
||||
self.multimodal_output = {"video": videos}
|
||||
if audios is not None:
|
||||
self.multimodal_output["audio"] = audios
|
||||
if sample_rate is not None:
|
||||
self.multimodal_output["audio_sample_rate"] = sample_rate
|
||||
self._custom_output = custom_output or {}
|
||||
self.stage_durations = stage_durations or {}
|
||||
self.peak_memory_mb = peak_memory_mb
|
||||
|
||||
@property
|
||||
def custom_output(self):
|
||||
return self._custom_output
|
||||
|
||||
|
||||
class FakeAsyncOmni:
|
||||
def __init__(self):
|
||||
@@ -400,6 +413,67 @@ def test_sampling_params_pass_through(test_client, mocker: MockerFixture):
|
||||
assert captured.extra_args["flow_shift"] == 0.25
|
||||
|
||||
|
||||
def test_frame_interpolation_params_pass_to_diffusion_sampling_params(test_client, mocker: MockerFixture):
|
||||
"""Frame interpolation parameters should be forwarded to diffusion worker sampling params."""
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
return_value=b"fake-video",
|
||||
)
|
||||
response = test_client.post(
|
||||
"/v1/videos",
|
||||
data={
|
||||
"prompt": "smooth motion",
|
||||
"fps": "8",
|
||||
"enable_frame_interpolation": "true",
|
||||
"frame_interpolation_exp": "2",
|
||||
"frame_interpolation_scale": "0.5",
|
||||
"frame_interpolation_model_path": "local-rife",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
video_id = response.json()["id"]
|
||||
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
|
||||
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
captured = engine.captured_sampling_params_list[0]
|
||||
assert captured.enable_frame_interpolation is True
|
||||
assert captured.frame_interpolation_exp == 2
|
||||
assert captured.frame_interpolation_scale == 0.5
|
||||
assert captured.frame_interpolation_model_path == "local-rife"
|
||||
|
||||
|
||||
def test_worker_fps_multiplier_is_applied_to_async_encoding(test_client, mocker: MockerFixture):
|
||||
fps_values = []
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
|
||||
async def _generate(prompt, request_id, sampling_params_list):
|
||||
engine.captured_prompt = prompt
|
||||
engine.captured_sampling_params_list = sampling_params_list
|
||||
import numpy as np
|
||||
|
||||
yield MockVideoResult([np.zeros((1, 64, 64, 3), dtype=np.uint8)], custom_output={"video_fps_multiplier": 2})
|
||||
|
||||
engine.generate = _generate
|
||||
|
||||
def _fake_encode(video, fps, **kwargs):
|
||||
del video, kwargs
|
||||
fps_values.append(fps)
|
||||
return b"fake-video"
|
||||
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
side_effect=_fake_encode,
|
||||
)
|
||||
|
||||
response = test_client.post("/v1/videos", data={"prompt": "fps multiplier", "fps": "8"})
|
||||
|
||||
assert response.status_code == 200
|
||||
video_id = response.json()["id"]
|
||||
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
|
||||
assert fps_values == [16]
|
||||
|
||||
|
||||
def test_audio_sample_rate_comes_from_model_config(test_client, mocker: MockerFixture):
|
||||
audio_sample_rates = []
|
||||
|
||||
@@ -595,6 +669,10 @@ def test_video_request_validation():
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
VideoGenerationRequest(prompt="test", image_reference={"file_id": "file-1", "image_url": "https://example.com"})
|
||||
with pytest.raises(ValueError):
|
||||
VideoGenerationRequest(prompt="test", frame_interpolation_exp=0)
|
||||
with pytest.raises(ValueError):
|
||||
VideoGenerationRequest(prompt="test", frame_interpolation_scale=0)
|
||||
|
||||
|
||||
def test_list_videos_supports_order_after_and_limit(test_client, mocker: MockerFixture):
|
||||
@@ -1032,3 +1110,57 @@ def test_sync_sampling_params_pass_through(test_client, mocker: MockerFixture):
|
||||
assert captured.num_inference_steps == 30
|
||||
assert captured.guidance_scale == 6.5
|
||||
assert captured.seed == 42
|
||||
|
||||
|
||||
def test_sync_frame_interpolation_params_pass_to_sampling_params(test_client, mocker: MockerFixture):
|
||||
"""Frame interpolation parameters should be forwarded on the sync path."""
|
||||
encode_mock = _mock_encode_video_bytes(mocker)
|
||||
response = test_client.post(
|
||||
"/v1/videos/sync",
|
||||
data={
|
||||
"prompt": "smooth sync",
|
||||
"fps": "8",
|
||||
"enable_frame_interpolation": "true",
|
||||
"frame_interpolation_exp": "2",
|
||||
"frame_interpolation_scale": "0.5",
|
||||
"frame_interpolation_model_path": "local-rife",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
captured = engine.captured_sampling_params_list[0]
|
||||
assert captured.enable_frame_interpolation is True
|
||||
assert captured.frame_interpolation_exp == 2
|
||||
assert captured.frame_interpolation_scale == 0.5
|
||||
assert captured.frame_interpolation_model_path == "local-rife"
|
||||
_, kwargs = encode_mock.call_args
|
||||
assert kwargs["fps"] == 8
|
||||
|
||||
|
||||
def test_worker_fps_multiplier_is_applied_to_sync_encoding(test_client, mocker: MockerFixture):
|
||||
engine = test_client.app.state.openai_serving_video._engine_client
|
||||
fps_values = []
|
||||
|
||||
async def _generate(prompt, request_id, sampling_params_list):
|
||||
engine.captured_prompt = prompt
|
||||
engine.captured_sampling_params_list = sampling_params_list
|
||||
yield MockVideoResult([object()], custom_output={"video_fps_multiplier": 2})
|
||||
|
||||
engine.generate = _generate
|
||||
|
||||
def _fake_encode(video, fps, **kwargs):
|
||||
del video, kwargs
|
||||
fps_values.append(fps)
|
||||
return b"fps-multiplied"
|
||||
|
||||
mocker.patch(
|
||||
"vllm_omni.entrypoints.openai.serving_video._encode_video_bytes",
|
||||
side_effect=_fake_encode,
|
||||
)
|
||||
|
||||
response = test_client.post("/v1/videos/sync", data={"prompt": "fps multiplier", "fps": "8"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"fps-multiplied"
|
||||
assert fps_values == [16]
|
||||
Reference in new issue
Block a user