133 lines
5.7 KiB
Python
133 lines
5.7 KiB
Python
"""
|
|
视频操作专用 API 路由
|
|
"""
|
|
|
|
import asyncio
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, Request
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.services.grok.services.video import VideoService
|
|
from app.core.auth import verify_api_key
|
|
from app.core.logger import logger
|
|
from app.core.exceptions import ValidationException
|
|
from app.api.v1.chat import _build_streaming_response, _chat_error_as_success_response, _video_error_message
|
|
|
|
router = APIRouter(tags=["Video"])
|
|
|
|
class VideoExtendRequest(BaseModel):
|
|
"""视频延长请求"""
|
|
model: str = Field("grok-imagine-1.0-video", description="模型名称")
|
|
post_id: str = Field(..., description="原始视频的 post_id")
|
|
prompt: Optional[str] = Field(None, description="延长描述词")
|
|
video_length: Optional[int] = Field(6, description="延长时长 (6/10/15)")
|
|
aspect_ratio: Optional[str] = Field("16:9", description="视频比例")
|
|
resolution: Optional[str] = Field("480p", description="分辨率 (480p/720p)")
|
|
stream: Optional[bool] = Field(True, description="是否流式返回")
|
|
n: Optional[int] = Field(None, ge=1, le=4, description="生成数量 (1-4,仅非流式)")
|
|
concurrent: Optional[int] = Field(None, ge=1, le=4, description="并发视频数 (1-4,仅非流式)")
|
|
video_extension_start_time: Optional[float] = Field(None, description="延长开始时间点")
|
|
stitch_with_extend: Optional[bool] = Field(True, description="是否拼接之前的视频")
|
|
|
|
@router.post("/video/extend", dependencies=[Depends(verify_api_key)])
|
|
async def extend_video(request: VideoExtendRequest, raw_request: Request):
|
|
"""专用视频延长接口"""
|
|
logger.info(f"Video extension requested via dedicated API: post_id={request.post_id}")
|
|
|
|
# 自动偏移并限制 30s 逻辑
|
|
# Grok 延长时长固定为 6s 或 10s,总长上限 30s
|
|
extension_start = request.video_extension_start_time or 0.0
|
|
requested_length = request.video_length or 6
|
|
|
|
# 如果用户请求的长超过 30s 剩余空间,则我们将开始时间“往前回拨”以容纳该长度,
|
|
# 但最高不能超过 30s。若回拨到 0 还是放不下,则由底层处理。
|
|
if extension_start + requested_length > 30.0:
|
|
new_start = max(0.0, 30.0 - requested_length)
|
|
logger.info(
|
|
f"Optimizing extension start time: {extension_start}s -> {new_start}s "
|
|
f"to fit {requested_length}s extension (total capped at 30s)"
|
|
)
|
|
extension_start = new_start
|
|
|
|
video_length = requested_length
|
|
|
|
# 构造兼容 VideoService.completions 的参数
|
|
# 如果没有传 prompt,传入 None 或空,VideoService 会处理为默认 animate 指令
|
|
messages = []
|
|
if request.prompt:
|
|
messages.append({"role": "user", "content": request.prompt})
|
|
else:
|
|
# 即使留空,VideoService 也会处理,这里确保至少有一个 user 消息体以便提取
|
|
messages.append({"role": "user", "content": ""})
|
|
|
|
video_concurrent = int(request.n or request.concurrent or 1)
|
|
if request.stream and video_concurrent > 1:
|
|
raise ValidationException(
|
|
message="Streaming is only supported when n=1",
|
|
param="n",
|
|
code="invalid_stream_concurrent",
|
|
)
|
|
|
|
try:
|
|
async def _run_single(single_stream: bool):
|
|
return await VideoService.completions(
|
|
model=request.model,
|
|
messages=messages,
|
|
stream=single_stream,
|
|
aspect_ratio=request.aspect_ratio or "16:9",
|
|
video_length=video_length,
|
|
resolution=request.resolution or "480p",
|
|
extend_post_id=request.post_id,
|
|
video_extension_start_time=extension_start,
|
|
stitch_with_extend=request.stitch_with_extend,
|
|
)
|
|
|
|
if video_concurrent <= 1:
|
|
result = await _run_single(bool(request.stream))
|
|
else:
|
|
results = await asyncio.gather(*[_run_single(False) for _ in range(video_concurrent)])
|
|
merged = dict(results[0]) if results else {
|
|
"id": "",
|
|
"object": "chat.completion",
|
|
"created": 0,
|
|
"model": request.model,
|
|
"choices": [],
|
|
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
|
}
|
|
|
|
choices = []
|
|
usage_prompt = 0
|
|
usage_completion = 0
|
|
usage_total = 0
|
|
for idx, item in enumerate(results):
|
|
raw_choice = (
|
|
item.get("choices", [{}])[0]
|
|
if isinstance(item.get("choices"), list) and item.get("choices")
|
|
else {}
|
|
)
|
|
choice = dict(raw_choice)
|
|
choice["index"] = idx
|
|
choices.append(choice)
|
|
|
|
usage = item.get("usage", {}) if isinstance(item, dict) else {}
|
|
usage_prompt += int(usage.get("prompt_tokens", 0) or 0)
|
|
usage_completion += int(usage.get("completion_tokens", 0) or 0)
|
|
usage_total += int(usage.get("total_tokens", 0) or 0)
|
|
|
|
merged["choices"] = choices
|
|
merged["usage"] = {
|
|
"prompt_tokens": usage_prompt,
|
|
"completion_tokens": usage_completion,
|
|
"total_tokens": usage_total,
|
|
}
|
|
result = merged
|
|
|
|
if request.stream:
|
|
return _build_streaming_response(result, raw_request, request.model)
|
|
else:
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Video extension API error: {e}")
|
|
return _chat_error_as_success_response(request.model, _video_error_message(e))
|