diff --git a/app/api/v1/chat.py b/app/api/v1/chat.py index 4866663..69db305 100644 --- a/app/api/v1/chat.py +++ b/app/api/v1/chat.py @@ -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: diff --git a/app/services/grok/media.py b/app/services/grok/media.py index 8c5159e..e6c6e1f 100644 --- a/app/services/grok/media.py +++ b/app/services/grok/media.py @@ -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", ): """ diff --git a/app/services/grok/nsfw.py b/app/services/grok/nsfw.py index 2f6a625..e3ac3b5 100644 --- a/app/services/grok/nsfw.py +++ b/app/services/grok/nsfw.py @@ -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 -> 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 diff --git a/app/services/grok/processor.py b/app/services/grok/processor.py index 7c47c69..8f0847b 100644 --- a/app/services/grok/processor.py +++ b/app/services/grok/processor.py @@ -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: diff --git a/app/static/config/config.js b/app/static/config/config.js index 9c8beaa..bef6667 100644 --- a/app/static/config/config.js +++ b/app/static/config/config.js @@ -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' } ]); } diff --git a/config.defaults.toml b/config.defaults.toml index a6ddbea..79a9e27 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -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 diff --git a/docs/README.en.md b/docs/README.en.md index 3505656..39d2ff3 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -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` | diff --git a/readme.md b/readme.md index 3f15261..71b00ea 100644 --- a/readme.md +++ b/readme.md @@ -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` |