Merge pull request #116 from chenyme/video&nsfw

fix: update video configuration and format handling
This commit is contained in:
Chenyme authored and GitHub committed 2026-02-04 18:30:44 +08:00
commit 5fe2a25977
8 files changed
+66 -42

No files matched your search

+9 -9
View File
@@ -48,8 +48,8 @@ class VideoConfig(BaseModel):
aspect_ratio: Optional[str] = Field(
"3:2", description="视频比例: 3:2, 16:9, 1:1 等"
)
video_length: Optional[int] = Field(6, description="视频时长(秒): 5-15")
resolution: Optional[str] = Field("SD", description="视频分辨率: SD, HD")
video_length: Optional[int] = Field(6, description="视频时长(秒): 6 或 10")
resolution_name: Optional[str] = Field("480p", description="视频分辨率: 480p, 720p")
preset: Optional[str] = Field("custom", description="风格预设: fun, normal, spicy")
@field_validator("aspect_ratio")
@@ -68,22 +68,22 @@ class VideoConfig(BaseModel):
@classmethod
def validate_video_length(cls, v):
if v is not None:
if v < 5 or v > 15:
if v not in (6, 10):
raise ValidationException(
message="video_length must be between 5 and 15 seconds",
message="video_length must be 6 or 10 seconds",
param="video_config.video_length",
code="invalid_video_length",
)
return v
@field_validator("resolution")
@field_validator("resolution_name")
@classmethod
def validate_resolution(cls, v):
allowed = ["SD", "HD"]
allowed = ["480p", "720p"]
if v and v not in allowed:
raise ValidationException(
message=f"resolution must be one of {allowed}",
param="video_config.resolution",
message=f"resolution_name must be one of {allowed}",
param="video_config.resolution_name",
code="invalid_resolution",
)
return v
@@ -272,7 +272,7 @@ async def chat_completions(request: ChatCompletionRequest):
thinking=request.thinking,
aspect_ratio=v_conf.aspect_ratio,
video_length=v_conf.video_length,
resolution=v_conf.resolution,
resolution=v_conf.resolution_name,
preset=v_conf.preset,
)
else:
+13 -13
View File
@@ -177,7 +177,7 @@ class VideoService:
post_id: str,
aspect_ratio: str = "3:2",
video_length: int = 6,
resolution: str = "SD",
resolution_name: str = "480p",
preset: str = "normal",
) -> dict:
"""构建视频生成载荷"""
@@ -190,7 +190,12 @@ class VideoService:
mode_flag = "--mode=extremely-spicy-or-crazy"
full_prompt = f"{prompt} {mode_flag}"
video_gen_config = {
"parentPostId": post_id,
"aspectRatio": aspect_ratio,
"videoLength": video_length,
"resolutionName": resolution_name,
}
return {
"temporary": True,
"modelName": "grok-3",
@@ -201,12 +206,7 @@ class VideoService:
"experiments": [],
"modelConfigOverride": {
"modelMap": {
"videoGenModelConfig": {
"parentPostId": post_id,
"aspectRatio": aspect_ratio,
"videoLength": video_length,
"videoResolution": resolution,
}
"videoGenModelConfig": video_gen_config
}
},
},
@@ -218,7 +218,7 @@ class VideoService:
prompt: str,
aspect_ratio: str = "3:2",
video_length: int = 6,
resolution: str = "SD",
resolution_name: str = "480p",
stream: bool = True,
preset: str = "normal",
) -> AsyncGenerator[bytes, None]:
@@ -230,7 +230,7 @@ class VideoService:
prompt: 视频描述
aspect_ratio: 宽高比
video_length: 视频时长
resolution: 分辨率
resolution_name: 分辨率
stream: 是否流式
preset: 预设
@@ -249,7 +249,7 @@ class VideoService:
# Step 2: 建立连接
headers = self._build_headers(token)
payload = self._build_payload(
prompt, post_id, aspect_ratio, video_length, resolution, preset
prompt, post_id, aspect_ratio, video_length, resolution_name, preset
)
session = AsyncSession(impersonate=BROWSER)
@@ -302,7 +302,7 @@ class VideoService:
image_url: str,
aspect_ratio: str = "3:2",
video_length: int = 6,
resolution: str = "SD",
resolution: str = "480p",
stream: bool = True,
preset: str = "normal",
) -> AsyncGenerator[bytes, None]:
@@ -418,7 +418,7 @@ class VideoService:
thinking: str = None,
aspect_ratio: str = "3:2",
video_length: int = 6,
resolution: str = "SD",
resolution: str = "480p",
preset: str = "normal",
):
"""
+19 -2
View File
@@ -62,14 +62,24 @@ class NSFWService:
@staticmethod
def _build_payload() -> bytes:
"""构造请求 payload"""
# protobuf: enable_unhinged=true (field1=1, field2=1)
protobuf = bytes([0x08, 0x01, 0x10, 0x01])
# protobuf (match captured HAR):
# 0a 02 10 01 -> field 1 (len=2) with inner bool=true
# 12 1a -> field 2, length 26
# 0a 18 <name> -> nested message with name string
name = b"always_show_nsfw_content"
inner = b"\x0a" + bytes([len(name)]) + name
protobuf = b"\x0a\x02\x10\x01\x12" + bytes([len(inner)]) + inner
return encode_grpc_web_payload(protobuf)
async def enable(self, token: str) -> NSFWResult:
"""为单个 token 开启 NSFW 模式"""
headers = self._build_headers(token)
payload = self._build_payload()
logger.debug(
"NSFW payload: len={} hex={}",
len(payload),
payload.hex(),
)
proxies = {"http": self.proxy, "https": self.proxy} if self.proxy else None
try:
@@ -96,6 +106,13 @@ class NSFWService:
)
grpc_status = get_grpc_status(trailers)
logger.debug(
"NSFW response: http={} grpc={} msg={} trailers={}",
response.status_code,
grpc_status.code,
grpc_status.message,
trailers,
)
# HTTP 200 且无 grpc-status(空响应)或 grpc-status=0 都算成功
success = grpc_status.code == -1 or grpc_status.ok
+15 -9
View File
@@ -498,7 +498,7 @@ class VideoStreamProcessor(BaseProcessor):
self.response_id: Optional[str] = None
self.think_opened: bool = False
self.role_sent: bool = False
self.video_format = get_config("app.video_format", "url")
self.video_format = str(get_config("app.video_format", "html")).lower()
if think is None:
self.show_think = get_config("grok.thinking", False)
@@ -568,10 +568,13 @@ class VideoStreamProcessor(BaseProcessor):
thumbnail_url, "image"
)
video_html = self._build_video_html(
final_video_url, final_thumbnail_url
)
yield self._sse(video_html)
if self.video_format == "url":
yield self._sse(final_video_url)
else:
video_html = self._build_video_html(
final_video_url, final_thumbnail_url
)
yield self._sse(video_html)
logger.info(f"Video generated: {video_url}")
continue
@@ -626,7 +629,7 @@ class VideoCollectProcessor(BaseProcessor):
def __init__(self, model: str, token: str = ""):
super().__init__(model, token)
self.video_format = get_config("app.video_format", "url")
self.video_format = str(get_config("app.video_format", "html")).lower()
def _build_video_html(self, video_url: str, thumbnail_url: str = "") -> str:
poster_attr = f' poster="{thumbnail_url}"' if thumbnail_url else ""
@@ -666,9 +669,12 @@ class VideoCollectProcessor(BaseProcessor):
thumbnail_url, "image"
)
content = self._build_video_html(
final_video_url, final_thumbnail_url
)
if self.video_format == "url":
content = final_video_url
else:
content = self._build_video_html(
final_video_url, final_thumbnail_url
)
logger.info(f"Video generated: {video_url}")
except asyncio.CancelledError:
+3 -2
View File
@@ -35,7 +35,7 @@ const LOCALE_MAP = {
"app_key": { title: "后台密码", desc: "登录 Grok2API 服务管理后台的密码,请妥善保管。" },
"app_url": { title: "应用地址", desc: "当前 Grok2API 服务的外部访问 URL,用于文件链接访问。" },
"image_format": { title: "图片格式", desc: "生成的图片格式(url 或 base64)。" },
"video_format": { title: "视频格式", desc: "生成的视频格式(仅支持 url)。" }
"video_format": { title: "视频格式", desc: "生成的视频格式(html 或 url,url 为处理后的链接)。" }
},
"grok": {
"label": "Grok 设置",
@@ -280,7 +280,8 @@ function renderConfig(data) {
]);
}
else if (key === 'video_format') {
built = buildSelectInput(section, key, 'url', [
built = buildSelectInput(section, key, val, [
{ val: 'html', text: 'HTML' },
{ val: 'url', text: 'URL' }
]);
}
+1 -1
View File
@@ -24,7 +24,7 @@ app_url = "http://127.0.0.1:8000"
app_key = "grok2api"
api_key = ""
image_format = "url"
video_format = "url"
video_format = "html"
[token]
auto_refresh = true
+3 -3
View File
@@ -99,8 +99,8 @@ curl http://localhost:8000/v1/chat/completions \
| `thinking` | string | Thinking mode | `enabled`, `disabled`, `null` |
| `video_config` | object | **Video model only** | - |
| └─ `aspect_ratio` | string | Video aspect ratio | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` |
| └─ `video_length` | integer | Video length (seconds) | `5` - `15` |
| └─ `resolution` | string | Resolution | `SD`, `HD` |
| └─ `video_length` | integer | Video length (seconds) | `6`, `10` |
| └─ `resolution_name` | string | Resolution | `480p`, `720p` |
| └─ `preset` | string | Style preset | `fun`, `normal`, `spicy` |
Note: any other parameters will be discarded and ignored.
@@ -157,7 +157,7 @@ Config file: `data/config.toml`
| | `app_key` | Admin password | Password for the Grok2API admin panel. | `grok2api` |
| | `api_key` | API key | Bearer token required to call Grok2API. | `""` |
| | `image_format` | Image format | Output image format (`url` or `base64`). | `url` |
| | `video_format` | Video format | Output video format (url only). | `url` |
| | `video_format` | Video format | Output video format (html tag or processed url). | `html` |
| **grok** | `temporary` | Temporary chat | Enable temporary conversation mode. | `true` |
| | `stream` | Streaming | Enable streaming by default. | `true` |
| | `thinking` | Thinking chain | Enable model thinking output. | `true` |
+3 -3
View File
@@ -112,8 +112,8 @@ curl http://localhost:8000/v1/chat/completions \
| `thinking` | string | 思维链模式 | `enabled`, `disabled`, `null` |
| `video_config` | object | **视频模型专用配置对象** | - |
| └─`aspect_ratio` | string | 视频宽高比 | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` |
| └─`video_length` | integer | 视频时长 (秒) | `5` - `15` |
| └─`resolution` | string | 分辨率 | `SD`, `HD` |
| └─`video_length` | integer | 视频时长 (秒) | `6`, `10` |
| └─`resolution_name` | string | 分辨率 | `480p`, `720p` |
| └─`preset` | string | 风格预设 | `fun`, `normal`, `spicy`, `custom` |
**消息格式 (messages)**:
@@ -190,7 +190,7 @@ curl http://localhost:8000/v1/images/generations \
| | `app_key` | 后台密码 | 登录 Grok2API 服务管理后台的密码,请妥善保管。 | `grok2api` |
| | `api_key` | API 密钥 | 调用 Grok2API 服务所需的 Bearer Token,请妥善保管。 | `grok2api` |
| | `image_format` | 图片格式 | 生成的图片格式(url 或 base64)。 | `url` |
| | `video_format` | 视频格式 | 生成的视频格式(仅支持 url)。 | `url` |
| | `video_format` | 视频格式 | 生成的视频格式(html 或 url,url 为处理后的链接)。 | `html` |
| **grok** | `temporary` | 临时对话 | 是否启用临时对话模式。 | `true` |
| | `stream` | 流式响应 | 是否默认启用流式输出。 | `true` |
| | `thinking` | 思维链 | 是否启用模型思维链输出。 | `true` |