254 lines
8.4 KiB
Python
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
|