diff --git a/app/services/grok/services/image.py b/app/services/grok/services/image.py index 35da0f7f2..8072f54b0 100644 --- a/app/services/grok/services/image.py +++ b/app/services/grok/services/image.py @@ -259,6 +259,48 @@ async def _fetch_batch(call_target: int): if len(all_images) >= n: break + # If upstream likely blocked/reviewed some images, run extra parallel attempts + # and only keep valid finals selected by ws_imagine classification. + if len(all_images) < n: + remaining = n - len(all_images) + extra_attempts = int(get_config("image.blocked_parallel_attempts") or 5) + extra_attempts = max(0, min(extra_attempts, 10)) + if extra_attempts > 0: + logger.warning( + f"Image finals insufficient ({len(all_images)}/{n}), running " + f"{extra_attempts} parallel recovery attempts for remaining={remaining}" + ) + extra_tasks = [ + _fetch_batch(min(expected_per_call, remaining)) + for _ in range(extra_attempts) + ] + extra_results = await asyncio.gather(*extra_tasks, return_exceptions=True) + for batch in extra_results: + if isinstance(batch, Exception): + logger.warning(f"WS recovery batch failed: {batch}") + continue + for img in batch: + if img not in seen: + seen.add(img) + all_images.append(img) + if len(all_images) >= n: + break + if len(all_images) >= n: + break + + if len(all_images) < n: + logger.error( + f"Image generation failed after recovery attempts: finals={len(all_images)}/{n}" + ) + raise UpstreamException( + "Image generation blocked or no valid final image", + details={ + "error_code": "blocked_no_final_image", + "final_images": len(all_images), + "requested": n, + }, + ) + try: await token_mgr.consume(token, self._get_effort(model_info)) except Exception as e: diff --git a/app/services/reverse/ws_imagine.py b/app/services/reverse/ws_imagine.py index 36467a725..a9bab3a01 100644 --- a/app/services/reverse/ws_imagine.py +++ b/app/services/reverse/ws_imagine.py @@ -197,6 +197,10 @@ async def _stream_once( and completed == 0 and now - medium_received_time > blocked_grace ): + logger.warning( + "Imagine stream blocked suspected: received medium preview but no valid final image " + f"within {blocked_grace:.1f}s (request_id={request_id})" + ) raise _BlockedError() if completed > 0 and now - last_activity > 10: logger.info( @@ -258,6 +262,11 @@ async def _stream_once( and completed == 0 and time.monotonic() - medium_received_time > final_timeout ): + logger.warning( + "Imagine stream final-timeout suspected review/block: " + f"no final image reached threshold in {final_timeout:.1f}s " + f"(request_id={request_id})" + ) raise _BlockedError() elif ws_msg.type in ( diff --git a/app/static/admin/js/config.js b/app/static/admin/js/config.js index 66b12ee81..578a89617 100644 --- a/app/static/admin/js/config.js +++ b/app/static/admin/js/config.js @@ -29,6 +29,7 @@ const NUMERIC_FIELDS = new Set([ 'final_timeout', 'final_min_bytes', 'medium_min_bytes', + 'blocked_parallel_attempts', 'concurrent', 'batch_size' ]); @@ -96,7 +97,16 @@ const LOCALE_MAP = { "final_timeout": { title: "最终图超时", desc: "收到中等图后等待最终图的超时秒数。" }, "nsfw": { title: "NSFW 模式", desc: "WebSocket 请求是否启用 NSFW。" }, "medium_min_bytes": { title: "中等图最小字节", desc: "判定中等质量图的最小字节数。" }, - "final_min_bytes": { title: "最终图最小字节", desc: "判定最终图的最小字节数(通常 JPG > 100KB)。" } + "final_min_bytes": { title: "最终图最小字节", desc: "判定最终图的最小字节数(通常 JPG > 100KB)。" }, + "blocked_parallel_attempts": { title: "拦截补偿并发次数", desc: "疑似审查/拦截导致无最终图时,自动并行补偿生成次数。" } + }, + + + "superimage": { + "label": "SuperImage 配置", + "n": { title: "生成数量", desc: "仅用于 grok-superimage-1.0 的服务端统一生成数量(1-10)。" }, + "size": { title: "图片尺寸", desc: "仅用于 grok-superimage-1.0 的服务端统一尺寸。" }, + "response_format": { title: "响应格式", desc: "仅用于 grok-superimage-1.0 的服务端统一返回格式。" } }, diff --git a/config.defaults.toml b/config.defaults.toml index 6f2017946..753890a2a 100644 --- a/config.defaults.toml +++ b/config.defaults.toml @@ -109,6 +109,19 @@ nsfw = true medium_min_bytes = 30000 # 判定为最终图的最小字节数 final_min_bytes = 100000 +# 遇到疑似审查/拦截时的并行补偿生成次数 +blocked_parallel_attempts = 5 + + +# ==================== SuperImage 配置 ==================== +[superimage] +# 仅对 grok-superimage-1.0 生效,由服务端统一控制,不使用客户端 image_config +n = 1 +# 图片尺寸:1280x720 / 720x1280 / 1792x1024 / 1024x1792 / 1024x1024 +size = "1024x1024" +# 响应格式:url / b64_json / base64 +response_format = "url" + # ==================== SuperImage 配置 ==================== diff --git a/readme.md b/readme.md index 9aff133b9..4e13f9557 100644 --- a/readme.md +++ b/readme.md @@ -181,6 +181,7 @@ curl http://localhost:8000/v1/chat/completions \ - `grok-superimage-1.0` 与瀑布流 imagine 生成链路一致,可直接通过 `/v1/chat/completions` 调用;其 `n/size/response_format` 由服务端 `[superimage]` 统一控制。 - `grok-superimage-1.0` 在 `/v1/chat/completions` 的流式输出仅返回最终成图,不返回中间预览图。 - `grok-superimage-1.0` 流式 URL 出图会保持原始图片名(不追加 `-final` 后缀)。 +- 当图片疑似被审查拦截导致无最终图时,服务端会按 `image.blocked_parallel_attempts` 自动并行补偿生成;若仍无满足 `image.final_min_bytes` 的最终图则返回失败。 - `grok-imagine-1.0-edit` 必须提供图片,多图默认取**最后 3 张**与最后一个文本。 - `grok-imagine-1.0-video` 支持文生视频与图生视频(通过 `image_url` 传参考图,**仅取第 1 张**)。 - 除上述外的其他参数将自动丢弃并忽略。