Files
hao-ai-lab-FastVideo/fastvideo/entrypoints/openai/protocol.py
T

254 lines
8.4 KiB
Python

# Adapted from SGLang
# (https://github.com/sgl-project/sglang/blob/main/python/sglang/multimodal_gen/runtime/entrypoints/openai/protocol.py)
import time
import uuid
from enum import Enum
from typing import Annotated, Any, Literal
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, field_validator
class ImageResponseData(BaseModel):
b64_json: str | None = None
url: str | None = None
revised_prompt: str | None = None
file_path: str | None = None
class ImageResponse(BaseModel):
id: str
created: int = Field(default_factory=lambda: int(time.time()))
data: list[ImageResponseData]
peak_memory_mb: float | None = None
inference_time_s: float | None = None
class ImageGenerationsRequest(BaseModel):
prompt: str
model: str | None = None
n: int | None = 1
quality: str | None = "auto"
response_format: str | None = "url" # url | b64_json
size: str | None = "1024x1024"
style: str | None = "vivid"
background: str | None = "auto" # transparent | opaque | auto
output_format: str | None = None # png | jpeg | webp
user: str | None = None
# FastVideo extensions (SGLang-compatible)
num_inference_steps: int | None = None
guidance_scale: float | None = None
true_cfg_scale: float | None = None
seed: int | None = 1024
negative_prompt: str | None = None
enable_teacache: bool | None = False
_INT64_MIN = -(2**63)
_INT64_MAX = 2**63 - 1
class VideoGenerationStatus(str, Enum):
QUEUED = "queued"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
FAILED = "failed"
SizeStr = Annotated[str, StringConstraints(pattern=r"^\d+x\d+$")]
SecondStr = Annotated[str, StringConstraints(pattern=r"^[1-9]\d*$")]
DEFAULT_FPS = 24
class VideoParams(BaseModel):
"""Optional vLLM-Omni-compatible video parameter block."""
width: int | None = Field(default=None, ge=1, le=_INT64_MAX)
height: int | None = Field(default=None, ge=1, le=_INT64_MAX)
num_frames: int | None = Field(default=None, ge=1, le=_INT64_MAX)
fps: int | None = Field(default=None, ge=1, le=_INT64_MAX)
@property
def size(self) -> str | None:
if self.width is not None and self.height is not None:
return f"{self.width}x{self.height}"
return None
class FileImageReference(BaseModel):
model_config = ConfigDict(extra="forbid")
file_id: str
class UrlImageReference(BaseModel):
model_config = ConfigDict(extra="forbid")
image_url: str = Field(min_length=1)
ImageReference = UrlImageReference | FileImageReference
class FileVideoReference(BaseModel):
model_config = ConfigDict(extra="forbid")
file_id: str
class UrlVideoReference(BaseModel):
model_config = ConfigDict(extra="forbid")
video_url: str = Field(min_length=1)
VideoReference = UrlVideoReference | FileVideoReference
class UrlAudioReference(BaseModel):
model_config = ConfigDict(extra="forbid")
audio_url: str = Field(min_length=1)
AudioReference = UrlAudioReference
class VideoGenerationRequest(BaseModel):
"""OpenAI/vLLM-Omni-compatible video generation request.
Model-specific parameters belong in ``extra_params``. FastVideo keeps the
legacy ``input_reference`` and ``reference_url`` fields for clients that
predate vLLM-Omni's typed reference objects.
"""
model_config = ConfigDict(extra="forbid")
prompt: str = Field(min_length=1)
model: str | None = None
seconds: Annotated[int, Field(ge=1, le=_INT64_MAX)] | SecondStr | None = None
size: SizeStr | None = None
image_reference: ImageReference | list[ImageReference] | None = None
video_reference: VideoReference | list[VideoReference] | None = None
audio_reference: AudioReference | list[AudioReference] | None = None
input_reference: str | None = None
reference_url: str | None = None
# SGLang's legacy direct-video spellings.
video_path: str | None = None
video_url: str | None = None
video_params: VideoParams | None = None
user: str | None = None
task: str | None = None
width: int | None = Field(default=None, ge=1, le=_INT64_MAX)
height: int | None = Field(default=None, ge=1, le=_INT64_MAX)
fps: int | None = Field(default=None, ge=1, le=_INT64_MAX)
num_frames: int | None = Field(default=None, ge=1, le=_INT64_MAX)
aspect_ratio: str | None = None
short_edge: int | None = Field(default=None, ge=1, le=_INT64_MAX)
num_outputs_per_prompt: int = Field(default=1, ge=1, le=10)
# SGLang spelling retained as an alias-like input field.
n: int | None = Field(default=None, ge=1, le=10)
start_time_seconds: float | None = Field(default=None, ge=0.0)
quality: Literal["auto", "default", "standard", "hd"] | None = None
negative_prompt: str | None = None
num_inference_steps: int | None = Field(default=None, ge=1, le=200)
guidance_scale: float | None = Field(default=None, ge=0.0, le=20.0)
guidance_scale_2: float | None = Field(default=None, ge=0.0, le=20.0)
boundary_ratio: float | None = Field(default=None, ge=0.0, le=1.0)
flow_shift: float | None = None
true_cfg_scale: float | None = Field(default=None, ge=0.0, le=20.0)
seed: int | None = Field(default=None, ge=_INT64_MIN, le=_INT64_MAX)
generate_sound: bool = False
sound_duration: float | None = Field(default=None, gt=0.0)
enable_teacache: bool = False
max_sequence_length: int | None = Field(default=None, ge=1)
enable_frame_interpolation: bool = False
frame_interpolation_exp: int = Field(default=1, ge=1, le=_INT64_MAX)
frame_interpolation_scale: float = Field(default=1.0, gt=0.0)
frame_interpolation_model_path: str | None = None
lora: dict[str, Any] | None = None
extra_params: dict[str, Any] | None = None
@field_validator("prompt")
@classmethod
def validate_prompt(cls, value: str) -> str:
if not value.strip():
raise ValueError("prompt must not be empty")
return value
def resolve_video_params(self) -> VideoParams:
"""Resolve top-level, nested, and ``size`` dimensions like vLLM-Omni."""
params = VideoParams(
width=self.width,
height=self.height,
fps=self.fps,
num_frames=self.num_frames,
)
if self.video_params is not None:
params.width = params.width or self.video_params.width
params.height = params.height or self.video_params.height
params.fps = params.fps or self.video_params.fps
params.num_frames = params.num_frames or self.video_params.num_frames
if self.size is not None:
width, height = self.size.split("x", 1)
params.width, params.height = int(width), int(height)
if params.fps is None:
params.fps = DEFAULT_FPS
if params.num_frames is None and self.seconds is not None:
params.num_frames = int(self.seconds) * params.fps
return params
@property
def resolved_num_outputs(self) -> int:
return self.n if self.n is not None else self.num_outputs_per_prompt
# Backward-compatible spelling used by the original FastVideo/SGLang surface.
VideoGenerationsRequest = VideoGenerationRequest
class VideoError(BaseModel):
code: int | str = 500
message: str
class VideoResponse(BaseModel):
id: str
object: Literal["video"] = "video"
model: str = ""
prompt: str = ""
status: VideoGenerationStatus = VideoGenerationStatus.QUEUED
progress: int = 0
created_at: int = Field(default_factory=lambda: int(time.time()))
size: SizeStr | None = None
seconds: SecondStr = "4"
quality: str = "default"
url: str | None = None
remixed_from_video_id: str | None = None
expires_at: int | None = None
file_path: str | None = None
file_name: str | None = None
media_type: Literal["video/mp4"] = "video/mp4"
completed_at: int | None = None
error: VideoError | None = None
peak_memory_mb: float | None = None
inference_time_s: float | None = None
stage_durations: dict[str, float] = Field(default_factory=dict)
class VideoDeleteResponse(BaseModel):
id: str
deleted: bool
object: Literal["video.deleted"] = "video.deleted"
class VideoListResponse(BaseModel):
data: list[VideoResponse]
first_id: str | None = None
last_id: str | None = None
has_more: bool = False
object: Literal["list"] = "list"
def generate_request_id() -> str:
"""Generate a unique request ID"""
return uuid.uuid4().hex