Signed-off-by: ZhengWG <zwg0606@gmail.com> Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
2289 lines
95 KiB
Python
2289 lines
95 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
"""Online/offline request clients and response types for tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import concurrent.futures
|
|
import copy
|
|
import io
|
|
import json
|
|
import time
|
|
from collections.abc import Mapping
|
|
from dataclasses import asdict, dataclass
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
from urllib.parse import quote
|
|
|
|
import numpy as np
|
|
import requests
|
|
import soundfile as sf
|
|
import torch
|
|
from openai import OpenAI, omit
|
|
from PIL import Image
|
|
from vllm.logger import init_logger
|
|
|
|
from tests.helpers.assertions import (
|
|
SuccessRateGate,
|
|
assert_audio_speech_response,
|
|
assert_diffusion_response,
|
|
assert_http_error,
|
|
assert_images_generations_response,
|
|
assert_omni_response,
|
|
collect_at_success_rate,
|
|
)
|
|
from tests.helpers.media import (
|
|
_merge_base64_audio_to_segment,
|
|
)
|
|
from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt
|
|
from vllm_omni.outputs import OmniRequestOutput
|
|
|
|
if TYPE_CHECKING:
|
|
from tests.helpers.runtime import OmniRunner
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def _parse_response_json(r: requests.Response) -> dict[str, Any] | list[Any] | None:
|
|
try:
|
|
data = r.json()
|
|
if isinstance(data, (dict, list)):
|
|
return data
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def _split_request_config_by_per_output_sizes(cfg: dict[str, Any]) -> list[dict[str, Any]] | None:
|
|
"""If ``extra_body`` has list ``height``/``width``, return one config per index (scalar h/w, ``num_outputs_per_prompt=1``)."""
|
|
eb = cfg.get("extra_body")
|
|
if not eb:
|
|
return None
|
|
h, w = eb.get("height"), eb.get("width")
|
|
if (isinstance(h, (list, tuple)) or isinstance(w, (list, tuple))) and not (
|
|
isinstance(h, (list, tuple)) and isinstance(w, (list, tuple))
|
|
):
|
|
raise ValueError("extra_body height and width must both be lists or both be scalars")
|
|
if not (isinstance(h, (list, tuple)) and isinstance(w, (list, tuple))):
|
|
return None
|
|
if len(h) != len(w):
|
|
raise ValueError(f"height and width lists must have equal length; got {len(h)=} {len(w)=}")
|
|
n = len(h)
|
|
n_out = eb.get("num_outputs_per_prompt")
|
|
if n_out is not None:
|
|
n_out = int(n_out)
|
|
if n_out != n:
|
|
raise ValueError(
|
|
"When height/width are lists, num_outputs_per_prompt must equal their length; "
|
|
f"got num_outputs_per_prompt={n_out}, len(lists)={n}"
|
|
)
|
|
splits: list[dict[str, Any]] = []
|
|
for i in range(n):
|
|
sub = copy.deepcopy(cfg)
|
|
sub_eb = dict(sub.get("extra_body") or {})
|
|
sub_eb["height"] = int(h[i])
|
|
sub_eb["width"] = int(w[i])
|
|
sub_eb["num_outputs_per_prompt"] = 1
|
|
sub["extra_body"] = sub_eb
|
|
splits.append(sub)
|
|
return splits
|
|
|
|
|
|
@dataclass
|
|
class OmniResponse:
|
|
"""Decoded multimodal / chat output from the OpenAI SDK or offline runner (not raw ``requests``)."""
|
|
|
|
request_id: str | None = None
|
|
text_content: str | None = None
|
|
audio_data: list[str] | None = None
|
|
audio_content: str | None = None
|
|
audio_format: str | None = None
|
|
audio_bytes: bytes | None = None
|
|
word_timestamps: list[dict[str, str | int]] | None = None
|
|
#: End-to-end wall time in **seconds** (``perf_counter`` delta), from just before the
|
|
#: OpenAI client call through response parsing and local post-process (e.g. audio decode).
|
|
e2e_latency: float | None = None
|
|
success: bool = False
|
|
prompt_tokens: int | None = None
|
|
cached_tokens: int | None = None
|
|
multimodal_tokens: dict[str, int] | None = None
|
|
logprobs: list | None = None
|
|
|
|
|
|
@dataclass
|
|
class DiffusionResponse:
|
|
"""Decoded diffusion output from chat completions or offline runner (not raw ``requests``)."""
|
|
|
|
text_content: str | None = None
|
|
images: list[Image.Image] | None = None
|
|
audios: list[Any] | None = None
|
|
videos: list[Any] | None = None
|
|
#: End-to-end wall time in **seconds** (``perf_counter`` delta), from just before
|
|
#: ``chat.completions.create`` through local image / audio decode.
|
|
e2e_latency: float | None = None
|
|
success: bool = False
|
|
|
|
|
|
@dataclass
|
|
class HttpResponse:
|
|
"""Normalized view of a ``requests`` response from :class:`OnlineOmniClient` HTTP helpers."""
|
|
|
|
status_code: int
|
|
success: bool
|
|
error_message: str | None = None
|
|
json_body: dict[str, Any] | list[Any] | None = None
|
|
|
|
|
|
@dataclass
|
|
class WebSocketJsonResponse:
|
|
"""First JSON object delivered as a text WebSocket frame (streaming endpoints)."""
|
|
|
|
json_body: dict[str, Any]
|
|
|
|
|
|
@dataclass
|
|
class OpenPIWebSocketResponse:
|
|
"""Msgpack WebSocket session against ``/v1/realtime/robot/openpi``."""
|
|
|
|
server_metadata: dict[str, Any]
|
|
operation_responses: list[Any]
|
|
actions: dict[str, np.ndarray] | None = None
|
|
action_tensors: list[np.ndarray] | None = None
|
|
|
|
|
|
def build_openpi_droid_observation(*, session_id: str = "gr00t-smoke", seed: int | None = None) -> dict[str, Any]:
|
|
"""Build a minimal DROID-style observation payload for the OpenPI robot endpoint.
|
|
|
|
``seed`` pins the policy's sampling noise for this request (see ``docs/serving/openpi_api.md``).
|
|
"""
|
|
identity_eef_9d = np.zeros((1, 1, 9), dtype=np.float32)
|
|
identity_eef_9d[..., 3:] = np.array([1, 0, 0, 0, 1, 0], dtype=np.float32)
|
|
observation: dict[str, Any] = {
|
|
"session_id": session_id,
|
|
"video": {
|
|
"exterior_image_1_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8),
|
|
"wrist_image_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8),
|
|
},
|
|
"state": {
|
|
"eef_9d": identity_eef_9d,
|
|
"gripper_position": np.zeros((1, 1, 1), dtype=np.float32),
|
|
"joint_position": np.zeros((1, 1, 7), dtype=np.float32),
|
|
},
|
|
"language": {"annotation.language.language_instruction": [["pick up the object"]]},
|
|
}
|
|
if seed is not None:
|
|
observation["seed"] = seed
|
|
return observation
|
|
|
|
|
|
DREAMZERO_DEFAULT_PROMPT = (
|
|
"Move the pan forward and use the brush in the middle of the plates to brush the inside of the pan"
|
|
)
|
|
DREAMZERO_ACTION_HORIZON = 24
|
|
DREAMZERO_ACTION_DIM = 8
|
|
DREAMZERO_CAMERA_FILES = {
|
|
"observation/exterior_image_0_left": "exterior_image_1_left.mp4",
|
|
"observation/exterior_image_1_left": "exterior_image_2_left.mp4",
|
|
"observation/wrist_image_left": "wrist_image_left.mp4",
|
|
}
|
|
|
|
|
|
def _require_opencv() -> Any:
|
|
try:
|
|
import cv2
|
|
except ImportError as exc: # pragma: no cover - optional e2e dependency
|
|
raise ModuleNotFoundError("DreamZero OpenPI test dependencies are missing: opencv-python") from exc
|
|
return cv2
|
|
|
|
|
|
def load_dreamzero_camera_frames(video_dir: Path) -> dict[str, np.ndarray]:
|
|
cv2 = _require_opencv()
|
|
camera_frames: dict[str, np.ndarray] = {}
|
|
for camera_key, file_name in DREAMZERO_CAMERA_FILES.items():
|
|
video_path = video_dir / file_name
|
|
if not video_path.exists():
|
|
raise FileNotFoundError(f"Missing DreamZero test asset: {video_path}")
|
|
cap = cv2.VideoCapture(str(video_path))
|
|
frames = []
|
|
while True:
|
|
ok, frame = cap.read()
|
|
if not ok:
|
|
break
|
|
frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
|
cap.release()
|
|
if not frames:
|
|
raise RuntimeError(f"No frames loaded from {video_path}")
|
|
camera_frames[camera_key] = np.stack(frames, axis=0)
|
|
return camera_frames
|
|
|
|
|
|
def build_dreamzero_demo_observations(
|
|
camera_frames: dict[str, np.ndarray],
|
|
*,
|
|
prompt: str,
|
|
session_id: str,
|
|
num_chunks: int = 2,
|
|
) -> list[dict[str, Any]]:
|
|
if num_chunks < 1:
|
|
raise ValueError("num_chunks must be at least 1")
|
|
|
|
relative_offsets = [-23, -16, -8, 0]
|
|
total_frames = min(frames.shape[0] for frames in camera_frames.values())
|
|
frame_schedules = [[0]]
|
|
current_frame = 23
|
|
for _ in range(num_chunks - 1):
|
|
indices = [max(current_frame + offset, 0) for offset in relative_offsets]
|
|
if indices[-1] >= total_frames:
|
|
break
|
|
frame_schedules.append(indices)
|
|
current_frame += DREAMZERO_ACTION_HORIZON
|
|
|
|
observations: list[dict[str, Any]] = []
|
|
for frame_indices in frame_schedules:
|
|
obs: dict[str, Any] = {}
|
|
for camera_key, all_frames in camera_frames.items():
|
|
selected = all_frames[frame_indices]
|
|
obs[camera_key] = selected[0] if len(frame_indices) == 1 else selected
|
|
obs["observation/joint_position"] = np.zeros(7, dtype=np.float32)
|
|
obs["observation/cartesian_position"] = np.zeros(6, dtype=np.float32)
|
|
obs["observation/gripper_position"] = np.zeros(1, dtype=np.float32)
|
|
obs["prompt"] = prompt
|
|
obs["session_id"] = session_id
|
|
observations.append(obs)
|
|
return observations
|
|
|
|
|
|
class OpenPIWebSocketSession:
|
|
"""Persistent msgpack session for ``/v1/realtime/robot/openpi``."""
|
|
|
|
DEFAULT_PING_INTERVAL_SECS = 300
|
|
DEFAULT_PING_TIMEOUT_SECS = 3600
|
|
|
|
@staticmethod
|
|
def _require_dependencies() -> tuple[Any, Any]:
|
|
try:
|
|
import websockets.sync.client as websockets_client
|
|
except ImportError as exc: # pragma: no cover - optional e2e dependency
|
|
raise ModuleNotFoundError("GR00T OpenPI test dependencies are missing: websockets") from exc
|
|
try:
|
|
from openpi_client import msgpack_numpy
|
|
except ImportError as exc: # pragma: no cover - optional e2e dependency
|
|
raise ModuleNotFoundError("GR00T OpenPI test dependencies are missing: openpi-client") from exc
|
|
return websockets_client, msgpack_numpy
|
|
|
|
def __init__(
|
|
self,
|
|
uri: str,
|
|
*,
|
|
ping_interval: float = DEFAULT_PING_INTERVAL_SECS,
|
|
ping_timeout: float = DEFAULT_PING_TIMEOUT_SECS,
|
|
open_timeout: float = 120.0,
|
|
close_timeout: float = 120.0,
|
|
) -> None:
|
|
websockets_client, msgpack_numpy = self._require_dependencies()
|
|
self._msgpack_numpy = msgpack_numpy
|
|
self._packer = msgpack_numpy.Packer()
|
|
self._conn = websockets_client.connect(
|
|
uri,
|
|
compression=None,
|
|
max_size=None,
|
|
ping_interval=ping_interval,
|
|
ping_timeout=ping_timeout,
|
|
open_timeout=open_timeout,
|
|
close_timeout=close_timeout,
|
|
)
|
|
server_metadata = msgpack_numpy.unpackb(self._conn.recv())
|
|
if not isinstance(server_metadata, dict):
|
|
raise TypeError(f"Expected dict metadata from {uri}, got {type(server_metadata)!r}")
|
|
self._server_metadata = server_metadata
|
|
|
|
def get_server_metadata(self) -> dict[str, Any]:
|
|
return dict(self._server_metadata)
|
|
|
|
def infer(self, obs: dict[str, Any]) -> dict[str, np.ndarray]:
|
|
response = self._send_operation("infer", obs)
|
|
if not isinstance(response, dict):
|
|
raise TypeError(f"Expected dict infer response, got {type(response)!r}")
|
|
return {str(key): np.asarray(value, dtype=np.float32) for key, value in response.items()}
|
|
|
|
def reset(self, reset_info: dict[str, Any] | None = None) -> str:
|
|
response = self._send_operation("reset", dict(reset_info or {}))
|
|
return str(response["status"])
|
|
|
|
def close(self) -> None:
|
|
self._conn.close()
|
|
|
|
def _send_operation(self, endpoint: str, payload: dict[str, Any]) -> Any:
|
|
body = dict(payload)
|
|
body["endpoint"] = endpoint
|
|
self._conn.send(self._packer.pack(body))
|
|
raw = self._conn.recv()
|
|
if isinstance(raw, str):
|
|
raise RuntimeError(f"OpenPI {endpoint!r} failed: {raw}")
|
|
decoded = self._msgpack_numpy.unpackb(raw)
|
|
if isinstance(decoded, dict) and decoded.get("type") == "error":
|
|
raise RuntimeError(f"OpenPI {endpoint!r} failed: {decoded.get('message')}")
|
|
if endpoint == "reset":
|
|
if not isinstance(decoded, dict) or decoded.get("status") != "reset successful":
|
|
raise RuntimeError(f"Unexpected OpenPI reset response: {decoded!r}")
|
|
return decoded
|
|
return decoded
|
|
|
|
|
|
def _merge_http_expectation_kwargs(
|
|
base: dict[str, Any] | None,
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
cfg = dict(base or {})
|
|
if err_code is not None:
|
|
cfg["err_code"] = err_code
|
|
if err_message is not None:
|
|
cfg["err_message"] = err_message
|
|
return cfg
|
|
|
|
|
|
def _merge_ws_expectation_kwargs(
|
|
base: dict[str, Any] | None,
|
|
*,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
ws_json_type: str | None = None,
|
|
ws_error_code: str | None = None,
|
|
) -> dict[str, Any]:
|
|
cfg = dict(base or {})
|
|
if err_message is not None:
|
|
cfg["err_message"] = err_message
|
|
if ws_json_type is not None:
|
|
cfg["ws_json_type"] = ws_json_type
|
|
if ws_error_code is not None:
|
|
cfg["ws_error_code"] = ws_error_code
|
|
return cfg
|
|
|
|
|
|
def _run_ws_expectations_from_request_config(cfg: dict[str, Any], resp: WebSocketJsonResponse) -> None:
|
|
jb = resp.json_body
|
|
want_type = cfg.get("ws_json_type")
|
|
if want_type is not None:
|
|
assert jb.get("type") == want_type, (jb, want_type)
|
|
want_code = cfg.get("ws_error_code")
|
|
if want_code is not None:
|
|
assert jb.get("code") == want_code, (jb, want_code)
|
|
err_message = cfg.get("err_message")
|
|
if err_message is not None:
|
|
assert_http_error(resp, err_message=err_message, websocket_json_message=True)
|
|
|
|
|
|
def _merge_diffusion_responses(parts: list[DiffusionResponse]) -> DiffusionResponse:
|
|
"""Concatenate images in order; ``e2e_latency`` is wall-clock of the batch (set by caller) or max of parts."""
|
|
merged = DiffusionResponse()
|
|
merged.success = all(p.success for p in parts) and len(parts) > 0
|
|
imgs: list[Image.Image] = []
|
|
for p in parts:
|
|
if p.images:
|
|
imgs.extend(p.images)
|
|
merged.images = imgs if imgs else None
|
|
latencies = [p.e2e_latency for p in parts if p.e2e_latency is not None]
|
|
merged.e2e_latency = max(latencies) if latencies else None
|
|
return merged
|
|
|
|
|
|
class OnlineOmniClient:
|
|
def __init__(
|
|
self,
|
|
host: str = "127.0.0.1",
|
|
port: int | None = None,
|
|
api_key: str = "EMPTY",
|
|
run_level: str | None = None,
|
|
*,
|
|
log_stats: bool = True,
|
|
):
|
|
if port is None:
|
|
from tests.helpers.runtime import get_open_port
|
|
|
|
port = get_open_port()
|
|
self.base_url = f"http://{host}:{port}"
|
|
self.client = OpenAI(base_url=f"http://{host}:{port}/v1", api_key=api_key)
|
|
self.run_level = run_level
|
|
self.log_stats = log_stats
|
|
|
|
def _print_client_stat(self, message: str) -> None:
|
|
if self.log_stats:
|
|
print(message, flush=True)
|
|
|
|
def _process_stream_omni_response(self, chat_completion, *, wall_start: float) -> OmniResponse:
|
|
"""Wall clock from *before* ``chat.completions.create`` through stream drain + local decode."""
|
|
result = OmniResponse()
|
|
try:
|
|
text_content = ""
|
|
audio_data = []
|
|
for chunk in chat_completion:
|
|
if result.request_id is None:
|
|
result.request_id = getattr(chunk, "id", None)
|
|
for choice in chunk.choices:
|
|
content = getattr(getattr(choice, "delta", None), "content", None)
|
|
modality = getattr(chunk, "modality", None)
|
|
if modality == "audio" and content:
|
|
audio_data.append(content)
|
|
elif modality == "text" and content:
|
|
text_content += content
|
|
# Usage is yielded after the last token
|
|
if chunk.usage:
|
|
result.prompt_tokens = chunk.usage.prompt_tokens
|
|
if details := getattr(chunk.usage, "prompt_tokens_details", None):
|
|
result.cached_tokens = details.cached_tokens
|
|
result.multimodal_tokens = getattr(details, "multimodal_tokens", None)
|
|
|
|
if audio_data:
|
|
merged_seg = _merge_base64_audio_to_segment(audio_data)
|
|
wav_buf = BytesIO()
|
|
merged_seg.export(wav_buf, format="wav")
|
|
result.audio_bytes = wav_buf.getvalue()
|
|
result.text_content = text_content
|
|
result.audio_data = audio_data
|
|
result.e2e_latency = time.perf_counter() - wall_start
|
|
result.success = True
|
|
except Exception as e:
|
|
msg = f"Stream processing error: {str(e)}"
|
|
print(f"Error: {msg}")
|
|
return result
|
|
|
|
def _process_non_stream_omni_response(self, chat_completion, *, wall_start: float) -> OmniResponse:
|
|
"""Wall clock from *before* ``chat.completions.create`` through response parse + local decode."""
|
|
result = OmniResponse()
|
|
try:
|
|
result.request_id = getattr(chat_completion, "id", None)
|
|
audio_data = None
|
|
text_content = None
|
|
for choice in chat_completion.choices:
|
|
if hasattr(choice.message, "audio") and choice.message.audio is not None:
|
|
audio_data = choice.message.audio.data
|
|
if hasattr(choice.message, "content") and choice.message.content is not None:
|
|
text_content = choice.message.content
|
|
# Extract cached & prompt token counts for prefix caching tests
|
|
usage = getattr(chat_completion, "usage", None)
|
|
if usage:
|
|
result.prompt_tokens = usage.prompt_tokens
|
|
if details := getattr(usage, "prompt_tokens_details", None):
|
|
result.cached_tokens = details.cached_tokens
|
|
result.multimodal_tokens = getattr(details, "multimodal_tokens", None)
|
|
if audio_data:
|
|
result.audio_bytes = base64.b64decode(audio_data)
|
|
result.text_content = text_content
|
|
result.e2e_latency = time.perf_counter() - wall_start
|
|
if chat_completion.choices and chat_completion.choices[0].logprobs is not None:
|
|
result.logprobs = chat_completion.choices[0].logprobs.content
|
|
result.success = True
|
|
except Exception as e:
|
|
msg = f"Non-stream processing error: {str(e)}"
|
|
print(f"Error: {msg}")
|
|
return result
|
|
|
|
def _process_diffusion_response(self, chat_completion, *, wall_start: float) -> DiffusionResponse:
|
|
"""Wall clock from *before* ``chat.completions.create`` through image decode."""
|
|
result = DiffusionResponse()
|
|
try:
|
|
images = []
|
|
audios = []
|
|
for choice in chat_completion.choices:
|
|
content = getattr(choice.message, "content", None)
|
|
if isinstance(content, list):
|
|
for item in content:
|
|
image_url = None
|
|
if isinstance(item, dict):
|
|
image_url = item.get("image_url", {}).get("url")
|
|
else:
|
|
image_url_obj = getattr(item, "image_url", None)
|
|
image_url = getattr(image_url_obj, "url", None) if image_url_obj else None
|
|
if image_url and image_url.startswith("data:image"):
|
|
b64_data = image_url.split(",", 1)[1]
|
|
img = Image.open(io.BytesIO(base64.b64decode(b64_data)))
|
|
img.load()
|
|
images.append(img)
|
|
|
|
# OpenAI audio responses (e.g. AudioX text-to-audio) populate `message.audio`.
|
|
audio_obj = getattr(choice.message, "audio", None)
|
|
audio_b64 = getattr(audio_obj, "data", None) if audio_obj is not None else None
|
|
if audio_b64:
|
|
audios.append(
|
|
{
|
|
"wav_bytes": base64.b64decode(audio_b64),
|
|
"id": getattr(audio_obj, "id", None),
|
|
"expires_at": getattr(audio_obj, "expires_at", None),
|
|
}
|
|
)
|
|
result.images = images if images else None
|
|
result.audios = audios if audios else None
|
|
result.e2e_latency = time.perf_counter() - wall_start
|
|
result.success = True
|
|
except Exception as e:
|
|
msg = f"Diffusion response processing error: {str(e)}"
|
|
print(f"Error: {msg}")
|
|
return result
|
|
|
|
def _http_response_from_requests(self, r: requests.Response) -> HttpResponse:
|
|
payload = _parse_response_json(r)
|
|
ok = 200 <= r.status_code < 300
|
|
return HttpResponse(
|
|
status_code=r.status_code,
|
|
success=ok,
|
|
error_message=None if ok else (r.text[:8000] if r.text else None),
|
|
json_body=payload,
|
|
)
|
|
|
|
def send_health_http_request(
|
|
self,
|
|
request_config: dict[str, Any] | None = None,
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""GET ``/health`` (raw ``requests``).
|
|
|
|
``request_config``: optional ``timeout`` plus optional ``err_code`` / ``err_message`` for
|
|
:func:`~tests.helpers.assertions.assert_http_error` (also as keyword-only args).
|
|
"""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = requests.get(self._build_url("/health"), timeout=float(cfg.get("timeout", 120.0)))
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_models_http_request(
|
|
self,
|
|
request_config: dict[str, Any] | None = None,
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""GET ``/v1/models``. Optional ``timeout`` and HTTP assertions (see :func:`~tests.helpers.assertions.assert_http_error`)."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = requests.get(
|
|
self._build_url("/v1/models"),
|
|
headers={"Accept": "application/json"},
|
|
timeout=float(cfg.get("timeout", 120.0)),
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_chat_completions_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/chat/completions`` with ``json`` or ``raw_body`` (malformed-body / contract tests)."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/chat/completions", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_batched_chat_completions_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""Post to batched chat completions."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/chat/completions/batch", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_completions_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/completions`` with ``json`` or ``raw_body``."""
|
|
# TODO (Alex): A lot of these helpers should be consolidated as they differ only by endpoint
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/completions", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_omni_sleep_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/omni/sleep`` — ``json`` or ``raw_body``, ``timeout``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/omni/sleep", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_omni_wakeup_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/omni/wakeup``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/omni/wakeup", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_audio_voices_list_http_request(
|
|
self,
|
|
request_config: dict[str, Any] | None = None,
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""GET ``/v1/audio/voices``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = requests.get(
|
|
self._build_url("/v1/audio/voices"),
|
|
headers={"Accept": "application/json"},
|
|
timeout=float(cfg.get("timeout", 120.0)),
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_audio_voices_create_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/audio/voices`` (multipart): ``data`` / ``files`` / ``timeout``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_form_endpoint("/v1/audio/voices", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_audio_voices_delete_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""DELETE ``/v1/audio/voices/{name}`` — requires ``name``, optional ``timeout``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
name = cfg["name"]
|
|
timeout = float(cfg.get("timeout", 120.0))
|
|
path = f"/v1/audio/voices/{quote(str(name), safe='')}"
|
|
r = requests.delete(
|
|
self._build_url(path),
|
|
headers={"Accept": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_audio_speech_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/audio/speech`` with ``json`` or ``raw_body``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/audio/speech", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_audio_speech_batch_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/audio/speech/batch``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/audio/speech/batch", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_audio_generate_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/audio/generate``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/audio/generate", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_images_generations_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/images/generations`` — ``json`` or ``raw_body``, ``timeout``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_json_endpoint("/v1/images/generations", cfg, default_timeout=300.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
if cfg.get("err_code") is None:
|
|
assert resp.success, resp.error_message
|
|
payload = resp.json_body
|
|
assert isinstance(payload, dict)
|
|
assert_images_generations_response(payload, cfg, run_level=self.run_level)
|
|
return [resp]
|
|
|
|
def send_images_edits_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/images/edits`` — ``data`` / ``files`` / ``timeout``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_form_endpoint("/v1/images/edits", cfg, default_timeout=300.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_videos_create_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/videos`` (async job) — multipart ``data`` / ``files``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_form_endpoint("/v1/videos", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_videos_sync_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""POST ``/v1/videos/sync``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = self._post_form_endpoint("/v1/videos/sync", cfg, default_timeout=120.0)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_videos_list_http_request(
|
|
self,
|
|
request_config: dict[str, Any] | None = None,
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""GET ``/v1/videos`` — optional ``params``, ``timeout``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
r = requests.get(
|
|
self._build_url("/v1/videos"),
|
|
params=cfg.get("params"),
|
|
headers={"Accept": "application/json"},
|
|
timeout=float(cfg.get("timeout", 120.0)),
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_video_retrieve_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""GET ``/v1/videos/{video_id}``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
video_id = cfg["video_id"]
|
|
timeout = float(cfg.get("timeout", 120.0))
|
|
r = requests.get(
|
|
self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}"),
|
|
headers={"Accept": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_video_delete_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""DELETE ``/v1/videos/{video_id}``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
video_id = cfg["video_id"]
|
|
timeout = float(cfg.get("timeout", 120.0))
|
|
r = requests.delete(
|
|
self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}"),
|
|
headers={"Accept": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def send_video_content_http_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_code: int | tuple[int, ...] | list[int] | None = None,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
) -> list[HttpResponse]:
|
|
"""GET ``/v1/videos/{video_id}/content``."""
|
|
cfg = _merge_http_expectation_kwargs(
|
|
request_config,
|
|
err_code=err_code,
|
|
err_message=err_message,
|
|
)
|
|
video_id = cfg["video_id"]
|
|
timeout = float(cfg.get("timeout", 120.0))
|
|
r = requests.get(
|
|
self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}/content"),
|
|
timeout=timeout,
|
|
)
|
|
resp = self._http_response_from_requests(r)
|
|
assert_http_error(
|
|
resp,
|
|
err_code=cfg.get("err_code"),
|
|
err_message=cfg.get("err_message"),
|
|
)
|
|
return [resp]
|
|
|
|
def _build_ws_url(self, path: str) -> str:
|
|
"""Turn HTTP ``base_url`` into ``ws`` / ``wss`` for WebSocket helpers."""
|
|
base = self.base_url.rstrip("/")
|
|
suffix = "/" + path.lstrip("/")
|
|
if base.startswith("http://"):
|
|
return "ws://" + base.removeprefix("http://") + suffix
|
|
if base.startswith("https://"):
|
|
return "wss://" + base.removeprefix("https://") + suffix
|
|
raise ValueError(f"Unsupported base_url for WebSocket: {base!r}")
|
|
|
|
def _send_websocket_first_json_request(
|
|
self,
|
|
path: str,
|
|
cfg: dict[str, Any],
|
|
) -> list[WebSocketJsonResponse]:
|
|
"""Connect, optionally send text frames, return first JSON text frame as :class:`WebSocketJsonResponse`.
|
|
|
|
``request_config`` keys:
|
|
|
|
- ``send_frames``: optional ``str`` or sequence of ``str`` raw WebSocket text frames (omit when the server
|
|
speaks first, e.g. ``/v1/realtime`` rejection path).
|
|
- ``ws_skip_types``: optional event ``type`` strings to ignore while waiting for the first matching frame
|
|
(e.g. ``["session.created"]`` on ``/v1/realtime``).
|
|
- ``timeout``: seconds to wait for the first inbound text frame (default ``120``).
|
|
- ``ws_max_size``: passed through as ``max_size`` to :func:`websockets.connect` when the key is present.
|
|
"""
|
|
send_frames_raw = cfg.get("send_frames")
|
|
if send_frames_raw is None:
|
|
frames: list[str] = []
|
|
elif isinstance(send_frames_raw, str):
|
|
frames = [send_frames_raw]
|
|
else:
|
|
frames = list(send_frames_raw)
|
|
|
|
timeout = float(cfg.get("timeout", 120.0))
|
|
uri = self._build_ws_url(path)
|
|
skip_types = set(cfg.get("ws_skip_types") or [])
|
|
|
|
connect_kw: dict[str, Any] = {}
|
|
if "ws_max_size" in cfg:
|
|
connect_kw["max_size"] = cfg["ws_max_size"]
|
|
|
|
async def _recv_first_json_object() -> WebSocketJsonResponse:
|
|
import websockets
|
|
|
|
async with websockets.connect(uri, **connect_kw) as ws:
|
|
for frame in frames:
|
|
await ws.send(frame)
|
|
while True:
|
|
raw = await asyncio.wait_for(ws.recv(), timeout=timeout)
|
|
if not isinstance(raw, str):
|
|
raise AssertionError(f"Expected JSON text frame from {uri}, got {type(raw).__name__}")
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise AssertionError(f"Expected JSON text frame from {uri}, body={raw[:500]!r}") from exc
|
|
if not isinstance(data, dict):
|
|
raise AssertionError(f"Expected JSON object from {uri}, got {type(data).__name__}")
|
|
if skip_types and data.get("type") in skip_types:
|
|
continue
|
|
return WebSocketJsonResponse(json_body=data)
|
|
|
|
resp = asyncio.run(_recv_first_json_object())
|
|
_run_ws_expectations_from_request_config(cfg, resp)
|
|
return [resp]
|
|
|
|
def send_audio_speech_stream_ws_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
ws_json_type: str | None = None,
|
|
ws_error_code: str | None = None,
|
|
) -> list[WebSocketJsonResponse]:
|
|
"""WebSocket ``/v1/audio/speech/stream`` — send ``send_frames`` then read first JSON text frame."""
|
|
cfg = _merge_ws_expectation_kwargs(
|
|
request_config,
|
|
err_message=err_message,
|
|
ws_json_type=ws_json_type,
|
|
ws_error_code=ws_error_code,
|
|
)
|
|
return self._send_websocket_first_json_request("/v1/audio/speech/stream", cfg)
|
|
|
|
def send_video_chat_stream_ws_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
ws_json_type: str | None = None,
|
|
ws_error_code: str | None = None,
|
|
) -> list[WebSocketJsonResponse]:
|
|
"""WebSocket ``/v1/video/chat/stream`` — send ``send_frames`` then read first JSON text frame."""
|
|
cfg = _merge_ws_expectation_kwargs(
|
|
request_config,
|
|
err_message=err_message,
|
|
ws_json_type=ws_json_type,
|
|
ws_error_code=ws_error_code,
|
|
)
|
|
return self._send_websocket_first_json_request("/v1/video/chat/stream", cfg)
|
|
|
|
def send_realtime_ws_request(
|
|
self,
|
|
request_config: dict[str, Any] | None = None,
|
|
*,
|
|
err_message: str | tuple[str, ...] | list[str] | None = None,
|
|
ws_json_type: str | None = None,
|
|
ws_error_code: str | None = None,
|
|
) -> list[WebSocketJsonResponse]:
|
|
"""WebSocket ``/v1/realtime`` — optional outbound frames, then first JSON text frame (often server-initiated)."""
|
|
cfg = _merge_ws_expectation_kwargs(
|
|
request_config,
|
|
err_message=err_message,
|
|
ws_json_type=ws_json_type,
|
|
ws_error_code=ws_error_code,
|
|
)
|
|
return self._send_websocket_first_json_request("/v1/realtime", cfg)
|
|
|
|
def send_robot_openpi_ws_request(
|
|
self,
|
|
request_config: dict[str, Any] | None = None,
|
|
) -> list[OpenPIWebSocketResponse]:
|
|
"""WebSocket ``/v1/realtime/robot/openpi`` — msgpack metadata plus optional infer/reset ops.
|
|
|
|
``request_config`` keys:
|
|
|
|
- ``operations``: optional sequence of ``{"endpoint": "infer"|"reset", "payload": {...}}``.
|
|
- ``run_default_policy_session``: when true and ``operations`` is omitted, run infer then reset
|
|
using :func:`build_openpi_droid_observation`.
|
|
- ``run_dreamzero_policy_session``: when true and ``operations`` is omitted, run the DreamZero
|
|
infer/reset/infer sequence using :func:`build_dreamzero_demo_observations`.
|
|
- ``video_dir``: required for ``run_dreamzero_policy_session``.
|
|
- ``prompt``: language instruction for DreamZero observations (optional).
|
|
- ``session_id``: used by the default policy session observation builder.
|
|
- ``num_chunks``: DreamZero chunk count (default ``2``).
|
|
- ``timeout``: seconds to wait for each inbound msgpack frame (default ``120``).
|
|
- ``ping_interval`` / ``ping_timeout``: forwarded to :func:`websockets.sync.client.connect`.
|
|
"""
|
|
cfg = dict(request_config or {})
|
|
operations_cfg = cfg.get("operations")
|
|
if operations_cfg is not None:
|
|
operations = [dict(op) for op in operations_cfg]
|
|
elif cfg.get("run_dreamzero_policy_session"):
|
|
video_dir = cfg.get("video_dir")
|
|
if video_dir is None:
|
|
raise ValueError("run_dreamzero_policy_session requires video_dir")
|
|
session_id = str(cfg.get("session_id", "dreamzero-smoke"))
|
|
prompt = str(cfg.get("prompt", DREAMZERO_DEFAULT_PROMPT))
|
|
num_chunks = int(cfg.get("num_chunks", 2))
|
|
observations = build_dreamzero_demo_observations(
|
|
load_dreamzero_camera_frames(Path(video_dir)),
|
|
prompt=prompt,
|
|
session_id=session_id,
|
|
num_chunks=num_chunks,
|
|
)
|
|
operations = [{"endpoint": "infer", "payload": obs} for obs in observations]
|
|
operations.append({"endpoint": "reset", "payload": {}})
|
|
operations.append({"endpoint": "infer", "payload": observations[0]})
|
|
elif cfg.get("run_default_policy_session"):
|
|
session_id = str(cfg.get("session_id", "gr00t-smoke"))
|
|
operations = [
|
|
{"endpoint": "infer", "payload": build_openpi_droid_observation(session_id=session_id)},
|
|
{"endpoint": "reset", "payload": {}},
|
|
]
|
|
else:
|
|
operations = []
|
|
timeout = float(cfg.get("timeout", 120.0))
|
|
ping_interval = float(cfg.get("ping_interval", OpenPIWebSocketSession.DEFAULT_PING_INTERVAL_SECS))
|
|
ping_timeout = float(cfg.get("ping_timeout", OpenPIWebSocketSession.DEFAULT_PING_TIMEOUT_SECS))
|
|
|
|
uri = self._build_ws_url("/v1/realtime/robot/openpi")
|
|
session = OpenPIWebSocketSession(
|
|
uri,
|
|
ping_interval=ping_interval,
|
|
ping_timeout=ping_timeout,
|
|
open_timeout=timeout,
|
|
close_timeout=timeout,
|
|
)
|
|
try:
|
|
operation_responses: list[Any] = []
|
|
actions: dict[str, np.ndarray] | None = None
|
|
action_tensors: list[np.ndarray] = []
|
|
for operation in operations:
|
|
endpoint = str(operation["endpoint"])
|
|
payload = dict(operation.get("payload") or {})
|
|
response = session._send_operation(endpoint, payload)
|
|
operation_responses.append(response)
|
|
if endpoint == "infer":
|
|
if isinstance(response, dict):
|
|
actions = {str(key): np.asarray(value, dtype=np.float32) for key, value in response.items()}
|
|
else:
|
|
action_tensors.append(np.asarray(response, dtype=np.float32))
|
|
finally:
|
|
session.close()
|
|
|
|
return [
|
|
OpenPIWebSocketResponse(
|
|
server_metadata=session.get_server_metadata(),
|
|
operation_responses=operation_responses,
|
|
actions=actions,
|
|
action_tensors=action_tensors or None,
|
|
)
|
|
]
|
|
|
|
def send_omni_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
request_num: int = 1,
|
|
*,
|
|
min_successes: int | None = None,
|
|
max_concurrency: int | None = None,
|
|
) -> list[OmniResponse]:
|
|
"""Chat completions via the OpenAI Python SDK (not raw HTTP).
|
|
|
|
``min_successes`` gates the batch on how many of the ``request_num`` requests pass
|
|
rather than requiring each one to, and returns only the successes; ``max_concurrency``
|
|
caps how many are in flight at once. See ``SuccessRateGate``.
|
|
"""
|
|
responses: list[OmniResponse] = []
|
|
stream = request_config.get("stream", False)
|
|
modalities = request_config.get("modalities", ["text", "audio"])
|
|
extra_body: dict[str, Any] = {}
|
|
if "speaker" in request_config:
|
|
extra_body["speaker"] = request_config["speaker"]
|
|
if request_config.get("use_audio_in_video"):
|
|
mm = dict(extra_body.get("mm_processor_kwargs") or {})
|
|
mm["use_audio_in_video"] = True
|
|
extra_body["mm_processor_kwargs"] = mm
|
|
if "sampling_params_list" in request_config:
|
|
extra_body["sampling_params_list"] = request_config["sampling_params_list"]
|
|
if request_config.get("extra_body"):
|
|
extra_body.update(request_config["extra_body"])
|
|
|
|
create_kwargs: dict[str, Any] = {
|
|
"model": request_config.get("model"),
|
|
"messages": request_config.get("messages"),
|
|
"stream": stream,
|
|
"modalities": modalities,
|
|
}
|
|
if "logprobs" in request_config:
|
|
create_kwargs["logprobs"] = request_config["logprobs"]
|
|
if "top_logprobs" in request_config:
|
|
create_kwargs["top_logprobs"] = request_config["top_logprobs"]
|
|
if "stream_options" in request_config:
|
|
create_kwargs["stream_options"] = request_config["stream_options"]
|
|
if extra_body:
|
|
create_kwargs["extra_body"] = extra_body
|
|
|
|
def _one():
|
|
wall_start = time.perf_counter()
|
|
chat_completion = self.client.chat.completions.create(**create_kwargs)
|
|
return (
|
|
self._process_stream_omni_response(chat_completion, wall_start=wall_start)
|
|
if stream
|
|
else self._process_non_stream_omni_response(chat_completion, wall_start=wall_start)
|
|
)
|
|
|
|
if min_successes is not None:
|
|
gate = SuccessRateGate(min_successes=min_successes, concurrency=max_concurrency)
|
|
|
|
def _sample() -> OmniResponse:
|
|
resp = _one()
|
|
assert_omni_response(resp, request_config, run_level=self.run_level)
|
|
return resp
|
|
|
|
# Not _print_client_stat: the count is the gate's verdict, so it has to survive
|
|
# log_stats=False.
|
|
return collect_at_success_rate(
|
|
_sample,
|
|
gate,
|
|
request_num=request_num,
|
|
report=lambda line: print(f"[omni] {line}", flush=True),
|
|
)
|
|
|
|
if request_num == 1:
|
|
resp = _one()
|
|
assert_omni_response(resp, request_config, run_level=self.run_level)
|
|
if resp.e2e_latency is not None:
|
|
self._print_client_stat(f"[omni] request#1 success in {resp.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat("[omni] request#1 completed")
|
|
responses.append(resp)
|
|
return responses
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
|
futures = {executor.submit(_one): i + 1 for i in range(request_num)}
|
|
for future in concurrent.futures.as_completed(futures):
|
|
request_idx = futures[future]
|
|
resp = future.result()
|
|
assert_omni_response(resp, request_config, run_level=self.run_level)
|
|
if resp.e2e_latency is not None:
|
|
self._print_client_stat(f"[omni] request#{request_idx} success in {resp.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat(f"[omni] request#{request_idx} completed")
|
|
responses.append(resp)
|
|
return responses
|
|
|
|
def _process_stream_audio_speech_response(
|
|
self, response, *, response_format: str | None = None, wall_start: float
|
|
) -> OmniResponse:
|
|
"""
|
|
Process streaming /v1/audio/speech responses into an OmniResponse.
|
|
|
|
This mirrors _process_stream_omni_response but operates on low-level
|
|
audio bytes. Whisper transcription runs in assert_audio_speech_response
|
|
when the run_level requires it.
|
|
"""
|
|
result = OmniResponse()
|
|
|
|
try:
|
|
# Aggregate all audio bytes from the streaming response.
|
|
data = bytearray()
|
|
|
|
# Preferred OpenAI helper.
|
|
if hasattr(response, "iter_bytes") and callable(getattr(response, "iter_bytes")):
|
|
for chunk in response.iter_bytes():
|
|
if chunk:
|
|
data.extend(chunk)
|
|
else:
|
|
# Generic iterable-of-bytes fallback (e.g., generator or list of chunks).
|
|
try:
|
|
iterator = iter(response)
|
|
except TypeError:
|
|
iterator = None
|
|
|
|
if iterator is not None:
|
|
for chunk in iterator:
|
|
if not chunk:
|
|
continue
|
|
if isinstance(chunk, (bytes, bytearray)):
|
|
data.extend(chunk)
|
|
elif hasattr(chunk, "data"):
|
|
data.extend(chunk.data) # type: ignore[arg-type]
|
|
elif hasattr(chunk, "content"):
|
|
data.extend(chunk.content) # type: ignore[arg-type]
|
|
else:
|
|
raise TypeError(f"Unsupported stream chunk type: {type(chunk)}")
|
|
else:
|
|
raise TypeError(f"Unsupported audio speech streaming response type: {type(response)}")
|
|
|
|
raw_bytes = bytes(data)
|
|
|
|
# Populate OmniResponse.
|
|
result.audio_bytes = raw_bytes
|
|
result.e2e_latency = time.perf_counter() - wall_start
|
|
result.success = True
|
|
result.audio_format = getattr(response, "response", None)
|
|
if result.audio_format is not None:
|
|
result.audio_format = result.audio_format.headers.get("content-type", "")
|
|
|
|
except Exception as e:
|
|
msg = f"Audio speech stream processing error: {str(e)}"
|
|
print(f"Error: {msg}")
|
|
|
|
return result
|
|
|
|
def _process_non_stream_audio_speech_response(
|
|
self, response, *, response_format: str | None = None, wall_start: float
|
|
) -> OmniResponse:
|
|
"""
|
|
Process non-streaming /v1/audio/speech responses into an OmniResponse.
|
|
|
|
This mirrors _process_non_stream_omni_response but for the binary
|
|
audio payload returned by audio.speech.create.
|
|
"""
|
|
result = OmniResponse()
|
|
|
|
try:
|
|
# OpenAI non-streaming audio.speech.create returns HttpxBinaryResponseContent (.read() or .content)
|
|
if hasattr(response, "read") and callable(getattr(response, "read")):
|
|
raw_bytes = response.read()
|
|
elif hasattr(response, "content"):
|
|
raw_bytes = response.content # type: ignore[assignment]
|
|
else:
|
|
raise TypeError(f"Unsupported audio speech response type: {type(response)}")
|
|
|
|
result.audio_bytes = raw_bytes
|
|
result.e2e_latency = time.perf_counter() - wall_start
|
|
result.success = True
|
|
result.audio_format = getattr(response, "response", None)
|
|
if result.audio_format is not None:
|
|
timestamps = result.audio_format.headers.get("X-Word-Timestamps")
|
|
if timestamps is not None:
|
|
result.word_timestamps = json.loads(timestamps)
|
|
result.audio_format = result.audio_format.headers.get("content-type", "")
|
|
|
|
except Exception as e:
|
|
msg = f"Audio speech non-stream processing error: {str(e)}"
|
|
print(f"Error: {msg}")
|
|
|
|
return result
|
|
|
|
def send_audio_speech_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[OmniResponse]:
|
|
"""
|
|
Call the /v1/audio/speech endpoint using the same configuration-dict
|
|
style as send_omni_request, but via the OpenAI Python client's
|
|
audio.speech APIs.
|
|
|
|
Expected keys in request_config:
|
|
- model: model name/path (required)
|
|
- input: text to synthesize (required)
|
|
- response_format: audio format such as "wav" or "pcm" (optional)
|
|
- task_type, ref_text, ref_audio: TTS-specific extras (optional, passed via extra_body)
|
|
- min_audio_bytes: optional minimum ``len(audio_bytes)`` checked in ``assert_audio_speech_response``
|
|
- transcript_expected_text: local expected spoken text; defaults to ``input``
|
|
- transcript_model: primary Whisper model for content checks; defaults to ``small``
|
|
- transcript_pcm_sample_rate: local-only opt-in to transcribe mono int16 PCM;
|
|
must be the actual output sample rate, also used for HNR
|
|
- timeout: request timeout in seconds (float, optional, default 120.0)
|
|
- stream: whether to use streaming API (bool, optional, default False)
|
|
|
|
For negative / contract checks (expected 4xx), use
|
|
:meth:`send_audio_speech_http_request` with ``err_code`` / ``err_message``.
|
|
"""
|
|
timeout = float(request_config.get("timeout", 120.0))
|
|
|
|
model = request_config["model"]
|
|
text_input = request_config["input"]
|
|
stream = bool(request_config.get("stream", False))
|
|
voice = request_config.get("voice", None)
|
|
|
|
# Standard OpenAI param: use omit when not provided to keep default behavior.
|
|
response_format = request_config.get("response_format", omit)
|
|
|
|
# Qwen3-TTS custom fields, forwarded via extra_body.
|
|
extra_body: dict[str, Any] = {}
|
|
# Keep this list aligned with vllm_omni.entrypoints.openai.protocol.audio params.
|
|
for key in (
|
|
"task_type",
|
|
"ref_text",
|
|
"ref_audio",
|
|
"extra_params",
|
|
"language",
|
|
"max_new_tokens",
|
|
"seed",
|
|
"instructions",
|
|
"duration_seconds",
|
|
"speed",
|
|
"sample_rate",
|
|
"extra_params",
|
|
"stream_format",
|
|
"x_vector_only_mode",
|
|
"word_timestamps",
|
|
):
|
|
if key in request_config:
|
|
extra_body[key] = request_config[key]
|
|
|
|
responses: list[OmniResponse] = []
|
|
|
|
speech_fmt: str | None = None if response_format is omit else str(response_format).lower()
|
|
|
|
print(f"[audio.speech] start model={model}, stream={stream}, request_num={request_num}, timeout={timeout:.1f}s")
|
|
|
|
if request_num == 1:
|
|
if stream:
|
|
# Use streaming response helper.
|
|
wall_start = time.perf_counter()
|
|
with self.client.audio.speech.with_streaming_response.create(
|
|
model=model,
|
|
input=text_input,
|
|
response_format=response_format,
|
|
extra_body=extra_body or None,
|
|
timeout=timeout,
|
|
voice=voice,
|
|
) as resp:
|
|
omni_resp = self._process_stream_audio_speech_response(
|
|
resp, response_format=speech_fmt, wall_start=wall_start
|
|
)
|
|
else:
|
|
# Non-streaming response.
|
|
wall_start = time.perf_counter()
|
|
resp = self.client.audio.speech.create(
|
|
model=model,
|
|
input=text_input,
|
|
response_format=response_format,
|
|
extra_body=extra_body or None,
|
|
timeout=timeout,
|
|
voice=voice,
|
|
)
|
|
omni_resp = self._process_non_stream_audio_speech_response(
|
|
resp, response_format=speech_fmt, wall_start=wall_start
|
|
)
|
|
|
|
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
|
if omni_resp.e2e_latency is not None:
|
|
self._print_client_stat(f"[audio.speech] request#1 success in {omni_resp.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat("[audio.speech] request#1 completed")
|
|
responses.append(omni_resp)
|
|
return responses
|
|
else:
|
|
# request_num > 1: concurrent requests (use same params as single-request path)
|
|
|
|
if stream:
|
|
|
|
def _stream_task(request_idx: int):
|
|
wall_start = time.perf_counter()
|
|
with self.client.audio.speech.with_streaming_response.create(
|
|
model=model,
|
|
input=text_input,
|
|
response_format=response_format,
|
|
extra_body=extra_body or None,
|
|
timeout=timeout,
|
|
voice=voice,
|
|
) as resp:
|
|
result = self._process_stream_audio_speech_response(
|
|
resp, response_format=speech_fmt, wall_start=wall_start
|
|
)
|
|
if result.e2e_latency is not None:
|
|
self._print_client_stat(
|
|
f"[audio.speech] request#{request_idx} success in {result.e2e_latency:.3f}s"
|
|
)
|
|
else:
|
|
self._print_client_stat(f"[audio.speech] request#{request_idx} completed")
|
|
return result
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
|
futures = {executor.submit(_stream_task, i + 1): i + 1 for i in range(request_num)}
|
|
for future in concurrent.futures.as_completed(futures):
|
|
request_idx = futures[future]
|
|
try:
|
|
omni_resp = future.result()
|
|
except Exception as e:
|
|
print(
|
|
f"[audio.speech] request#{request_idx} failed "
|
|
f"(stream={stream}, timeout={timeout:.1f}s): {e!r}"
|
|
)
|
|
raise
|
|
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
|
responses.append(omni_resp)
|
|
else:
|
|
|
|
def _non_stream_task(request_idx: int):
|
|
wall_start = time.perf_counter()
|
|
r = self.client.audio.speech.create(
|
|
model=model,
|
|
input=text_input,
|
|
response_format=response_format,
|
|
extra_body=extra_body or None,
|
|
timeout=timeout,
|
|
voice=voice,
|
|
)
|
|
result = self._process_non_stream_audio_speech_response(
|
|
r, response_format=speech_fmt, wall_start=wall_start
|
|
)
|
|
if result.e2e_latency is not None:
|
|
self._print_client_stat(
|
|
f"[audio.speech] request#{request_idx} success in {result.e2e_latency:.3f}s"
|
|
)
|
|
else:
|
|
self._print_client_stat(f"[audio.speech] request#{request_idx} completed")
|
|
return result
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
|
futures = {executor.submit(_non_stream_task, i + 1): i + 1 for i in range(request_num)}
|
|
for future in concurrent.futures.as_completed(futures):
|
|
request_idx = futures[future]
|
|
try:
|
|
omni_resp = future.result()
|
|
except Exception as e:
|
|
print(
|
|
f"[audio.speech] request#{request_idx} failed "
|
|
f"(stream={stream}, timeout={timeout:.1f}s): {e!r}"
|
|
)
|
|
raise
|
|
assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level)
|
|
responses.append(omni_resp)
|
|
|
|
return responses
|
|
|
|
def send_diffusion_request(
|
|
self, request_config: dict[str, Any] | list[dict[str, Any]], request_num: int = 1
|
|
) -> list[DiffusionResponse]:
|
|
"""
|
|
Send OpenAI requests for diffusion models.
|
|
If ``extra_body`` has list ``height``/``width``, sends one chat completion per index in parallel
|
|
(scalar h/w, ``num_outputs_per_prompt=1`` each) and merges images in list order.
|
|
|
|
Args:
|
|
request_config: A single request configuration dict, or a list of
|
|
request configuration dicts (one request per element)
|
|
request_num: Number of requests to send concurrently, defaults to 1 (single request)
|
|
Returns:
|
|
list[DiffusionResponse]: List of DiffusionResponse objects containing the response data
|
|
"""
|
|
responses: list[DiffusionResponse] = []
|
|
|
|
def _create_from_config(cfg: dict[str, Any]) -> tuple[Any, float]:
|
|
stream = cfg.get("stream", False)
|
|
if stream:
|
|
raise NotImplementedError("Streaming is not currently implemented for diffusion model e2e test")
|
|
modalities = cfg.get("modalities", omit) # Most diffusion models don't require modalities param
|
|
eb = cfg.get("extra_body")
|
|
extra = copy.deepcopy(eb) if eb else None
|
|
wall_start = time.perf_counter()
|
|
chat_completion = self.client.chat.completions.create(
|
|
model=cfg.get("model"),
|
|
messages=cfg.get("messages"),
|
|
extra_body=extra,
|
|
modalities=modalities,
|
|
)
|
|
return chat_completion, wall_start
|
|
|
|
if isinstance(request_config, list):
|
|
if not request_config:
|
|
raise ValueError("request_config list must not be empty")
|
|
if request_num != 1:
|
|
raise ValueError("request_num is not supported when request_config is a list")
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=len(request_config)) as executor:
|
|
futures = {
|
|
executor.submit(_create_from_config, cfg): (i + 1, cfg) for i, cfg in enumerate(request_config)
|
|
}
|
|
for future in concurrent.futures.as_completed(futures):
|
|
request_idx, cfg = futures[future]
|
|
chat_completion, wall_start = future.result()
|
|
response = self._process_diffusion_response(chat_completion, wall_start=wall_start)
|
|
assert_diffusion_response(response, cfg, run_level=self.run_level)
|
|
if response.e2e_latency is not None:
|
|
self._print_client_stat(
|
|
f"[diffusion] request#{request_idx} success in {response.e2e_latency:.3f}s"
|
|
)
|
|
else:
|
|
self._print_client_stat(f"[diffusion] request#{request_idx} completed")
|
|
responses.append(response)
|
|
return responses
|
|
|
|
size_splits = _split_request_config_by_per_output_sizes(request_config)
|
|
if size_splits is not None:
|
|
if request_num != 1:
|
|
raise ValueError(
|
|
"request_num must be 1 when extra_body height/width are lists (split into concurrent per-size calls)"
|
|
)
|
|
t0 = time.perf_counter()
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=len(size_splits)) as executor:
|
|
split_futures = [executor.submit(_create_from_config, sub) for sub in size_splits]
|
|
chat_completions = [f.result() for f in split_futures]
|
|
parts = [self._process_diffusion_response(cc, wall_start=ws) for cc, ws in chat_completions]
|
|
merged = _merge_diffusion_responses(parts)
|
|
merged.e2e_latency = time.perf_counter() - t0
|
|
assert_diffusion_response(merged, request_config, run_level=self.run_level)
|
|
if merged.e2e_latency is not None:
|
|
self._print_client_stat(f"[diffusion] request#1 success in {merged.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat("[diffusion] request#1 completed")
|
|
return [merged]
|
|
|
|
if request_num == 1:
|
|
# Send single request
|
|
chat_completion, wall_start = _create_from_config(request_config)
|
|
response = self._process_diffusion_response(chat_completion, wall_start=wall_start)
|
|
assert_diffusion_response(response, request_config, run_level=self.run_level)
|
|
if response.e2e_latency is not None:
|
|
self._print_client_stat(f"[diffusion] request#1 success in {response.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat("[diffusion] request#1 completed")
|
|
responses.append(response)
|
|
return responses
|
|
|
|
# Send concurrent requests for the same request_config
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor:
|
|
idx_by_future = {executor.submit(_create_from_config, request_config): i + 1 for i in range(request_num)}
|
|
for future in concurrent.futures.as_completed(idx_by_future):
|
|
request_idx = idx_by_future[future]
|
|
chat_completion, wall_start = future.result()
|
|
response = self._process_diffusion_response(chat_completion, wall_start=wall_start)
|
|
assert_diffusion_response(response, request_config, run_level=self.run_level)
|
|
if response.e2e_latency is not None:
|
|
self._print_client_stat(f"[diffusion] request#{request_idx} success in {response.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat(f"[diffusion] request#{request_idx} completed")
|
|
responses.append(response)
|
|
return responses
|
|
|
|
def send_video_diffusion_request(
|
|
self, request_config: dict[str, Any], request_num: int = 1
|
|
) -> list[DiffusionResponse]:
|
|
"""
|
|
Send native /v1/videos requests: multipart ``form_data`` job create, poll until done, download content.
|
|
|
|
For raw HTTP to video routes without polling, use ``send_videos_create_http_request``, etc.
|
|
"""
|
|
if request_num != 1:
|
|
raise NotImplementedError("Concurrent video diffusion requests are not currently implemented")
|
|
|
|
form_data = request_config.get("form_data")
|
|
if not isinstance(form_data, dict):
|
|
raise ValueError("Video request_config must contain 'form_data'")
|
|
normalized_form_data = {key: str(value) for key, value in form_data.items() if value is not None}
|
|
files: dict[str, tuple[str, BytesIO, str]] = {}
|
|
image_reference = request_config.get("image_reference")
|
|
video_reference = request_config.get("video_reference")
|
|
if image_reference and video_reference:
|
|
raise ValueError("Only one of image_reference or video_reference can be provided")
|
|
if image_reference:
|
|
if image_reference.startswith("data:image"):
|
|
header, encoded = image_reference.split(",", 1)
|
|
content_type = header.split(";")[0].removeprefix("data:")
|
|
extension = content_type.split("/")[-1]
|
|
file_data = base64.b64decode(encoded)
|
|
files["input_reference"] = (f"reference.{extension}", BytesIO(file_data), content_type)
|
|
else:
|
|
normalized_form_data["image_reference"] = json.dumps({"image_url": image_reference})
|
|
if video_reference:
|
|
if video_reference.startswith("data:video"):
|
|
header, encoded = video_reference.split(",", 1)
|
|
content_type = header.split(";")[0].removeprefix("data:")
|
|
extension = content_type.split("/")[-1]
|
|
file_data = base64.b64decode(encoded)
|
|
files["input_reference"] = (f"reference.{extension}", BytesIO(file_data), content_type)
|
|
else:
|
|
normalized_form_data["video_reference"] = json.dumps({"video_url": video_reference})
|
|
|
|
result = DiffusionResponse()
|
|
create_url = self._build_url("/v1/videos")
|
|
response = requests.post(
|
|
create_url,
|
|
data=normalized_form_data,
|
|
files=files,
|
|
headers={"Accept": "application/json"},
|
|
timeout=60,
|
|
)
|
|
start_time = time.perf_counter()
|
|
response.raise_for_status()
|
|
job_data = response.json()
|
|
video_id = job_data["id"]
|
|
self._wait_until_video_completed(video_id)
|
|
end_time = time.perf_counter()
|
|
video_content = self._download_video_content(video_id)
|
|
result.success = True
|
|
result.videos = [video_content]
|
|
result.e2e_latency = end_time - start_time
|
|
assert_diffusion_response(result, request_config, run_level=self.run_level)
|
|
if result.e2e_latency is not None:
|
|
self._print_client_stat(f"[diffusion] request#1 success in {result.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat("[diffusion] request#1 completed")
|
|
return [result]
|
|
|
|
def _post_json_endpoint(
|
|
self,
|
|
path: str,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
default_timeout: float,
|
|
) -> requests.Response:
|
|
url = self._build_url(path)
|
|
timeout = float(request_config.get("timeout", default_timeout))
|
|
if "raw_body" in request_config:
|
|
raw = request_config["raw_body"]
|
|
payload = raw.encode("utf-8") if isinstance(raw, str) else raw
|
|
return requests.post(
|
|
url,
|
|
data=payload,
|
|
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
if "json" not in request_config:
|
|
raise ValueError(f"{path} request_config must include 'json' or 'raw_body'")
|
|
return requests.post(
|
|
url,
|
|
json=request_config["json"],
|
|
headers={"Accept": "application/json", "Content-Type": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
|
|
def _post_form_endpoint(
|
|
self,
|
|
path: str,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
default_timeout: float = 120.0,
|
|
) -> requests.Response:
|
|
url = self._build_url(path)
|
|
timeout = float(request_config.get("timeout", default_timeout))
|
|
data = request_config.get("data")
|
|
files = request_config.get("files")
|
|
if data is None and not files:
|
|
data = {}
|
|
return requests.post(
|
|
url,
|
|
data=data,
|
|
files=files,
|
|
headers={"Accept": "application/json"} if not files else {"Accept": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
|
|
def send_streaming_video_diffusion_request(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
request_num: int = 1,
|
|
*,
|
|
timeout_seconds: float = 600.0,
|
|
) -> list[DiffusionResponse]:
|
|
"""
|
|
Send a native ``/v1/realtime/video`` WebSocket request and return one
|
|
finalized MP4 artifact assembled from the streamed binary fragments.
|
|
"""
|
|
if request_num != 1:
|
|
raise NotImplementedError("Concurrent streaming video diffusion requests are not currently implemented")
|
|
|
|
response = asyncio.run(
|
|
self._send_streaming_video_diffusion_request_once(
|
|
request_config,
|
|
timeout_seconds=timeout_seconds,
|
|
)
|
|
)
|
|
assert_diffusion_response(response, request_config, run_level=self.run_level)
|
|
if response.e2e_latency is not None:
|
|
self._print_client_stat(f"[diffusion.stream] request#1 success in {response.e2e_latency:.3f}s")
|
|
else:
|
|
self._print_client_stat("[diffusion.stream] request#1 completed")
|
|
return [response]
|
|
|
|
async def _send_streaming_video_diffusion_request_once(
|
|
self,
|
|
request_config: dict[str, Any],
|
|
*,
|
|
timeout_seconds: float,
|
|
) -> DiffusionResponse:
|
|
form_data = request_config.get("form_data")
|
|
if not isinstance(form_data, dict):
|
|
raise ValueError("Video request_config must contain 'form_data'")
|
|
payload: dict[str, Any] = {
|
|
"type": "session.start",
|
|
**{key: value for key, value in form_data.items() if value is not None},
|
|
}
|
|
model = request_config.get("model")
|
|
if model is not None:
|
|
payload["model"] = model
|
|
payload.setdefault("format", "m4s")
|
|
|
|
fps = float(payload.get("fps") or 16)
|
|
stream_format = payload["format"]
|
|
url = self._build_ws_url("/v1/realtime/video")
|
|
|
|
result = DiffusionResponse()
|
|
chunks: list[bytes] = []
|
|
start_time = time.perf_counter()
|
|
deadline = start_time + timeout_seconds
|
|
|
|
import websockets
|
|
|
|
async with websockets.connect(url, max_size=None) as websocket:
|
|
await websocket.send(json.dumps(payload))
|
|
|
|
while True:
|
|
remaining = deadline - time.perf_counter()
|
|
if remaining <= 0:
|
|
raise TimeoutError(f"Streaming video request did not complete within {timeout_seconds}s")
|
|
|
|
message = await asyncio.wait_for(websocket.recv(), timeout=remaining)
|
|
if isinstance(message, bytes):
|
|
chunks.append(message)
|
|
continue
|
|
|
|
msg = json.loads(message)
|
|
msg_type = msg.get("type")
|
|
if msg_type == "video.start":
|
|
stream_format = msg.get("format") or stream_format
|
|
continue
|
|
if msg_type == "session.done":
|
|
break
|
|
if msg_type == "error":
|
|
raise RuntimeError(str(msg.get("message", msg)))
|
|
|
|
from vllm_omni.diffusion.utils.media_utils import finalize_streaming_video_bytes
|
|
from vllm_omni.entrypoints.openai.video_api_utils import StreamingVideoFormat
|
|
|
|
streamed_bytes = b"".join(chunks)
|
|
if not streamed_bytes:
|
|
raise RuntimeError("Streaming video request completed without binary video chunks")
|
|
result.videos = [
|
|
finalize_streaming_video_bytes(
|
|
streamed_bytes,
|
|
input_format=cast(StreamingVideoFormat, stream_format),
|
|
fps=fps,
|
|
)
|
|
]
|
|
result.e2e_latency = time.perf_counter() - start_time
|
|
result.success = True
|
|
return result
|
|
|
|
def _wait_until_video_completed(
|
|
self, video_id: str, poll_interval_seconds: int = 2, timeout_seconds: int = 300
|
|
) -> None:
|
|
status_url = self._build_url(f"/v1/videos/{video_id}")
|
|
deadline = time.monotonic() + timeout_seconds
|
|
while time.monotonic() < deadline:
|
|
status_resp = requests.get(status_url, headers={"Accept": "application/json"}, timeout=30)
|
|
status_resp.raise_for_status()
|
|
status_data = status_resp.json()
|
|
current_status = status_data["status"]
|
|
if current_status == "completed":
|
|
return
|
|
if current_status == "failed":
|
|
error_msg = status_data.get("last_error", "Unknown error")
|
|
raise RuntimeError(f"Job failed: {error_msg}")
|
|
time.sleep(poll_interval_seconds)
|
|
raise TimeoutError(f"Video job {video_id} did not complete within {timeout_seconds}s")
|
|
|
|
def _download_video_content(self, video_id: str) -> bytes:
|
|
download_url = self._build_url(f"/v1/videos/{video_id}/content")
|
|
video_resp = requests.get(download_url, stream=True, timeout=60)
|
|
video_resp.raise_for_status()
|
|
video_bytes = BytesIO()
|
|
for chunk in video_resp.iter_content(chunk_size=8192):
|
|
if chunk:
|
|
video_bytes.write(chunk)
|
|
return video_bytes.getvalue()
|
|
|
|
def _build_url(self, path: str) -> str:
|
|
return f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
|
|
|
|
class OfflineOmniClient:
|
|
def __init__(self, omni_runner: OmniRunner):
|
|
self.runner = omni_runner
|
|
|
|
def _process_omni_output(self, outputs: list[OmniRequestOutput]) -> OmniResponse:
|
|
result = OmniResponse()
|
|
try:
|
|
text_content = None
|
|
audio_content = None
|
|
for stage_output in outputs:
|
|
if getattr(stage_output, "final_output_type", None) == "text":
|
|
text_content = stage_output.outputs[0].text
|
|
if getattr(stage_output, "final_output_type", None) == "audio":
|
|
audio_content = stage_output.outputs[0].multimodal_output["audio"]
|
|
result.audio_content = audio_content
|
|
result.text_content = text_content
|
|
result.success = True
|
|
except Exception as e:
|
|
msg = f"Output processing error: {str(e)}"
|
|
result.success = False
|
|
print(f"Error: {msg}")
|
|
return result
|
|
|
|
def _process_diffusion_output(self, outputs: list[OmniRequestOutput]) -> DiffusionResponse:
|
|
result = DiffusionResponse()
|
|
output = outputs[0]
|
|
if isinstance(output.images[0], list):
|
|
# Returning frames of images as a video
|
|
result.videos = output.images
|
|
else:
|
|
# Returning actual images
|
|
result.images = output.images
|
|
# [TODO] Add audio processing when tests are introduced
|
|
result.success = True
|
|
return result
|
|
|
|
def send_omni_request(self, request_config: dict[str, Any] | None = None) -> OmniResponse:
|
|
if request_config is None:
|
|
request_config = {}
|
|
prompts_value = request_config.get("prompts")
|
|
prompts: str | list[str] = prompts_value if isinstance(prompts_value, (str, list)) else ""
|
|
videos = request_config.get("videos")
|
|
images = request_config.get("images")
|
|
audios = request_config.get("audios")
|
|
modalities = request_config.get("modalities", ["text", "audio"])
|
|
outputs = self.runner.generate_multimodal(
|
|
prompts=prompts, videos=videos, images=images, audios=audios, modalities=modalities
|
|
)
|
|
response = self._process_omni_output(outputs)
|
|
assert_omni_response(response, request_config, run_level="core_model")
|
|
return response
|
|
|
|
def send_diffusion_request(self, request_config: dict[str, Any]) -> DiffusionResponse:
|
|
prompt = request_config.get("prompt")
|
|
if prompt is None:
|
|
prompts = request_config.get("prompts")
|
|
if not prompts:
|
|
raise ValueError("request_config must contain a prompt")
|
|
if len(prompts) > 1:
|
|
raise ValueError(
|
|
"In the current internal data structure, "
|
|
"only one prompt is supported for diffusion requests. "
|
|
"Because one prompt can contain multiple images or videos, "
|
|
"the current internal data structure is ambiguous when multiple prompts are provided."
|
|
)
|
|
prompt = prompts[0]
|
|
|
|
# Sync previous `extra_body` field with sampling params object
|
|
sampling_params: OmniDiffusionSamplingParams | None = request_config.get("sampling_params")
|
|
if sampling_params is None:
|
|
extra_body = request_config.get("extra_body", {})
|
|
if extra_body:
|
|
sampling_params = OmniDiffusionSamplingParams(**extra_body)
|
|
else:
|
|
extra_body = asdict(sampling_params)
|
|
request_config["extra_body"] = extra_body
|
|
if not extra_body:
|
|
logger.warning("No sampling params provided in request_config, will skip output assertion")
|
|
|
|
negative_prompt = extra_body.get("negative_prompt") or request_config.get("negative_prompt")
|
|
videos = request_config.get("videos")
|
|
images = request_config.get("images")
|
|
audios = request_config.get("audios")
|
|
# Full dict (e.g. image + mask_image for inpainting) or partial; merged with top-level image/video keys.
|
|
extra_multi_modal = request_config.get("multi_modal_data")
|
|
modalities = request_config.get("modalities") # only used by limited models. Do not add default value here
|
|
|
|
prompt_object = OmniTextPrompt(prompt=prompt)
|
|
if negative_prompt:
|
|
prompt_object["negative_prompt"] = negative_prompt
|
|
multi_modal: dict = {}
|
|
if extra_multi_modal is not None:
|
|
multi_modal.update(dict(extra_multi_modal))
|
|
if videos is not None:
|
|
multi_modal["video"] = videos
|
|
if images is not None:
|
|
multi_modal["image"] = images
|
|
if audios is not None:
|
|
multi_modal["audio"] = audios
|
|
if multi_modal:
|
|
prompt_object["multi_modal_data"] = multi_modal
|
|
if modalities:
|
|
prompt_object["modalities"] = modalities # pyright: ignore[reportGeneralTypeIssues]
|
|
|
|
start_time = time.perf_counter()
|
|
raw_outputs = self.runner.generate([prompt_object], [sampling_params] if sampling_params else None)
|
|
end_time = time.perf_counter()
|
|
if not isinstance(raw_outputs, list):
|
|
raw_outputs = list(raw_outputs)
|
|
|
|
response = self._process_diffusion_output(raw_outputs)
|
|
response.e2e_latency = end_time - start_time
|
|
assert_diffusion_response(response, request_config, run_level="core_model")
|
|
return response
|
|
|
|
def send_audio_speech_request(self, request_config: dict[str, Any]) -> OmniResponse:
|
|
"""
|
|
Offline TTS: text -> audio via generate_multimodal, then validate with assert_audio_speech_response.
|
|
|
|
request_config must contain:
|
|
- 'input' or 'prompts': text to synthesize.
|
|
Optional keys:
|
|
- 'voice' -> speaker (CustomVoice)
|
|
- 'task_type' -> task_type in additional_information (default: "CustomVoice")
|
|
- 'language' -> language in additional_information (default: "Auto")
|
|
- 'max_new_tokens' -> max_new_tokens in additional_information (default: 2048)
|
|
- 'response_format' -> desired audio format (used only for assertion)
|
|
"""
|
|
input_text = request_config.get("input") or request_config.get("prompts")
|
|
if input_text is None:
|
|
raise ValueError("request_config must contain 'input' or 'prompts' for TTS")
|
|
if isinstance(input_text, list):
|
|
input_text = input_text[0] if input_text else ""
|
|
|
|
mm_processor_kwargs: dict[str, Any] = {}
|
|
if "voice" in request_config:
|
|
mm_processor_kwargs["speaker"] = request_config["voice"]
|
|
if "task_type" in request_config:
|
|
mm_processor_kwargs["task_type"] = request_config["task_type"]
|
|
if "ref_audio" in request_config:
|
|
mm_processor_kwargs["ref_audio"] = request_config["ref_audio"]
|
|
if "ref_text" in request_config:
|
|
mm_processor_kwargs["ref_text"] = request_config["ref_text"]
|
|
if "language" in request_config:
|
|
mm_processor_kwargs["language"] = request_config["language"]
|
|
if "max_new_tokens" in request_config:
|
|
mm_processor_kwargs["max_new_tokens"] = request_config["max_new_tokens"]
|
|
|
|
outputs = self.runner.generate_multimodal(
|
|
prompts=input_text,
|
|
modalities=["audio"],
|
|
mm_processor_kwargs=mm_processor_kwargs or None,
|
|
)
|
|
mm_out: dict[str, Any] | None = None
|
|
for stage_out in outputs:
|
|
if getattr(stage_out, "final_output_type", None) == "audio":
|
|
mm_out = stage_out.outputs[0].multimodal_output
|
|
break
|
|
if mm_out is None:
|
|
raise AssertionError("No audio output from pipeline")
|
|
|
|
audio_data = mm_out.get("audio")
|
|
if audio_data is None:
|
|
raise AssertionError("No audio tensor in multimodal output")
|
|
|
|
sr_raw = mm_out.get("sr")
|
|
sr_val = sr_raw[-1] if isinstance(sr_raw, list) and sr_raw else sr_raw
|
|
sr = int(sr_val.item() if hasattr(sr_val, "item") else sr_val)
|
|
wav_tensor = torch.cat(audio_data, dim=-1) if isinstance(audio_data, list) else audio_data
|
|
wav_buf = io.BytesIO()
|
|
sf.write(
|
|
wav_buf,
|
|
wav_tensor.float().cpu().numpy().reshape(-1),
|
|
samplerate=sr,
|
|
format="WAV",
|
|
subtype="PCM_16",
|
|
)
|
|
result = OmniResponse(success=True, audio_bytes=wav_buf.getvalue(), audio_format="audio/wav")
|
|
assert_audio_speech_response(result, request_config, run_level="core_model")
|
|
return result
|
|
|
|
def send_tokenized_tts_request(self, request_config: dict[str, Any]) -> OmniResponse:
|
|
"""Offline TTS for models whose engine prompt is *pre-tokenized*.
|
|
|
|
Fish-Speech-style DualAR models (and any model whose prompt embeds codec
|
|
codes) cannot go through :meth:`send_audio_speech_request`: the prompt is
|
|
built by the caller as ``prompt_token_ids`` plus per-request
|
|
``additional_information``, not from ``mm_processor_kwargs``.
|
|
|
|
request_config keys:
|
|
- ``prompt_token_ids``: ``list[int]`` or ``list[list[int]]`` (one entry
|
|
per request).
|
|
- ``additional_information``: dict, or list of dicts matching
|
|
``prompt_token_ids``.
|
|
- ``expected_sample_rate``: asserted against the reported ``sr``.
|
|
- ``min_duration_s`` / ``max_duration_s``: bounds on decoded audio.
|
|
- plus every key ``assert_audio_speech_response`` understands
|
|
(``response_format``, ``min_audio_bytes``, ``input``, ...).
|
|
"""
|
|
token_ids = request_config.get("prompt_token_ids")
|
|
if not token_ids:
|
|
raise ValueError("request_config must contain 'prompt_token_ids'")
|
|
batched_ids = token_ids if isinstance(token_ids[0], (list, tuple)) else [token_ids]
|
|
|
|
extra = request_config.get("additional_information")
|
|
if extra is None:
|
|
batched_extra: list[dict[str, Any] | None] = [None] * len(batched_ids)
|
|
elif isinstance(extra, dict):
|
|
batched_extra = [extra] * len(batched_ids)
|
|
else:
|
|
batched_extra = list(extra)
|
|
if len(batched_extra) != len(batched_ids):
|
|
raise ValueError("'additional_information' must match the number of prompts")
|
|
|
|
prompts: list[dict[str, Any]] = []
|
|
for ids, info in zip(batched_ids, batched_extra, strict=True):
|
|
prompt: dict[str, Any] = {"prompt_token_ids": list(ids)}
|
|
if info is not None:
|
|
prompt["additional_information"] = info
|
|
prompts.append(prompt)
|
|
|
|
start_time = time.perf_counter()
|
|
outputs = self.runner.omni.generate(prompts)
|
|
e2e_latency = time.perf_counter() - start_time
|
|
|
|
assert len(outputs) == len(prompts), f"Expected {len(prompts)} outputs, got {len(outputs)}"
|
|
expected_sr = request_config.get("expected_sample_rate")
|
|
min_duration = request_config.get("min_duration_s")
|
|
max_duration = request_config.get("max_duration_s")
|
|
|
|
result = OmniResponse()
|
|
wavs: list[torch.Tensor] = []
|
|
for index, stage_output in enumerate(outputs):
|
|
assert stage_output.outputs, f"Request {index} produced no output"
|
|
multimodal_output = stage_output.outputs[0].multimodal_output
|
|
assert isinstance(multimodal_output, Mapping), (
|
|
f"Request {index}: expected a mapping, got {type(multimodal_output)}"
|
|
)
|
|
|
|
# Never use ``a or b`` here: truthiness on a tensor raises.
|
|
audio = multimodal_output.get("model_outputs")
|
|
if audio is None:
|
|
audio = multimodal_output.get("audio")
|
|
assert audio is not None, f"Request {index}: no audio key in {sorted(multimodal_output)}"
|
|
if isinstance(audio, list):
|
|
chunks = [torch.as_tensor(chunk).float().cpu().reshape(-1) for chunk in audio if chunk is not None]
|
|
assert chunks, f"Request {index}: audio list contained no tensors"
|
|
audio = torch.cat(chunks, dim=0)
|
|
wav = torch.as_tensor(audio).float().cpu().reshape(-1)
|
|
assert wav.numel() > 0, f"Request {index}: decoded audio is empty"
|
|
wavs.append(wav)
|
|
|
|
sr_raw = multimodal_output.get("sr")
|
|
if isinstance(sr_raw, list):
|
|
sr_raw = sr_raw[-1] if sr_raw else None
|
|
assert sr_raw is not None, f"Request {index}: missing sample rate"
|
|
sample_rate = int(sr_raw.item() if hasattr(sr_raw, "item") else sr_raw)
|
|
if expected_sr is not None:
|
|
assert sample_rate == int(expected_sr), (
|
|
f"Request {index}: expected sample rate {expected_sr}, got {sample_rate}"
|
|
)
|
|
|
|
duration_s = wav.numel() / sample_rate
|
|
if min_duration is not None:
|
|
assert duration_s >= float(min_duration), (
|
|
f"Request {index}: audio too short ({duration_s:.2f}s < {min_duration}s)"
|
|
)
|
|
if max_duration is not None:
|
|
assert duration_s <= float(max_duration), (
|
|
f"Request {index}: audio too long ({duration_s:.2f}s > {max_duration}s)"
|
|
)
|
|
|
|
wav_buf = io.BytesIO()
|
|
sf.write(wav_buf, wav.numpy(), samplerate=sample_rate, format="WAV", subtype="PCM_16")
|
|
result = OmniResponse(
|
|
success=True,
|
|
audio_bytes=wav_buf.getvalue(),
|
|
audio_format="audio/wav",
|
|
e2e_latency=e2e_latency,
|
|
)
|
|
assert_audio_speech_response(result, request_config, run_level="core_model")
|
|
|
|
if request_config.get("assert_distinct_outputs") and len(wavs) > 1:
|
|
# Distinct inputs must decode to distinct audio. Leaked per-request
|
|
# codec state shows up as two requests producing (near-)identical
|
|
# waveforms; different lengths already prove distinctness.
|
|
for i in range(len(wavs)):
|
|
for j in range(i + 1, len(wavs)):
|
|
a, b = wavs[i], wavs[j]
|
|
if a.numel() != b.numel():
|
|
continue
|
|
max_diff = (a - b).abs().max().item()
|
|
assert max_diff > 1e-3, (
|
|
f"Requests {i} and {j} produced near-identical audio "
|
|
f"(max sample diff {max_diff:.2e}); possible codec-state crosstalk"
|
|
)
|
|
return result
|
|
|
|
def start_profile(self, profile_prefix: str | None = None, stages: list[int] | None = None) -> list[Any]:
|
|
return self.runner.start_profile(profile_prefix=profile_prefix, stages=stages)
|
|
|
|
def stop_profile(self, stages: list[int] | None = None) -> list[Any]:
|
|
return self.runner.stop_profile(stages=stages)
|
|
|
|
|
|
# Backward-compatible aliases (historical test fixture / type names).
|
|
OpenAIClientHandler = OnlineOmniClient
|
|
OmniRunnerHandler = OfflineOmniClient
|
|
|
|
|
|
__all__ = [
|
|
"DiffusionResponse",
|
|
"HttpResponse",
|
|
"WebSocketJsonResponse",
|
|
"OpenPIWebSocketResponse",
|
|
"build_openpi_droid_observation",
|
|
"build_dreamzero_demo_observations",
|
|
"DREAMZERO_ACTION_DIM",
|
|
"DREAMZERO_ACTION_HORIZON",
|
|
"DREAMZERO_CAMERA_FILES",
|
|
"DREAMZERO_DEFAULT_PROMPT",
|
|
"load_dreamzero_camera_frames",
|
|
"OpenPIWebSocketSession",
|
|
"OmniResponse",
|
|
"OfflineOmniClient",
|
|
"OnlineOmniClient",
|
|
"OpenAIClientHandler",
|
|
"OmniRunnerHandler",
|
|
]
|