Files
grok2api/app/services/grok/processors/image_processors.py
T

249 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
图片生成响应处理器(HTTP)
"""
import asyncio
import random
from typing import AsyncGenerator, AsyncIterable, List
import orjson
from curl_cffi.requests.errors import RequestsError
from app.core.config import get_config
from app.core.logger import logger
from app.core.exceptions import UpstreamException
from .base import (
BaseProcessor,
StreamIdleTimeoutError,
_with_idle_timeout,
_normalize_stream_line,
_collect_image_urls,
_is_http2_stream_error,
)
class ImageStreamProcessor(BaseProcessor):
"""图片生成流式响应处理器"""
def __init__(
self, model: str, token: str = "", n: int = 1, response_format: str = "b64_json"
):
super().__init__(model, token)
self.partial_index = 0
self.n = n
self.target_index = random.randint(0, 1) if n == 1 else None
self.response_format = response_format
if response_format == "url":
self.response_field = "url"
elif response_format == "base64":
self.response_field = "base64"
else:
self.response_field = "b64_json"
def _sse(self, event: str, data: dict) -> str:
"""构建 SSE 响应"""
return f"event: {event}\ndata: {orjson.dumps(data).decode()}\n\n"
async def process(
self, response: AsyncIterable[bytes]
) -> AsyncGenerator[str, None]:
"""处理流式响应"""
final_images = []
idle_timeout = get_config("timeout.stream_idle_timeout")
try:
async for line in _with_idle_timeout(response, idle_timeout, self.model):
line = _normalize_stream_line(line)
if not line:
continue
try:
data = orjson.loads(line)
except orjson.JSONDecodeError:
continue
resp = data.get("result", {}).get("response", {})
# 图片生成进度
if img := resp.get("streamingImageGenerationResponse"):
image_index = img.get("imageIndex", 0)
progress = img.get("progress", 0)
if self.n == 1 and image_index != self.target_index:
continue
out_index = 0 if self.n == 1 else image_index
yield self._sse(
"image_generation.partial_image",
{
"type": "image_generation.partial_image",
self.response_field: "",
"index": out_index,
"progress": progress,
},
)
continue
# modelResponse
if mr := resp.get("modelResponse"):
if urls := _collect_image_urls(mr):
for url in urls:
if self.response_format == "url":
processed = await self.process_url(url, "image")
if processed:
final_images.append(processed)
continue
try:
dl_service = self._get_dl()
base64_data = await dl_service.to_base64(
url, self.token, "image"
)
if base64_data:
if "," in base64_data:
b64 = base64_data.split(",", 1)[1]
else:
b64 = base64_data
final_images.append(b64)
except Exception as e:
logger.warning(
f"Failed to convert image to base64, falling back to URL: {e}"
)
processed = await self.process_url(url, "image")
if processed:
final_images.append(processed)
continue
for index, b64 in enumerate(final_images):
if self.n == 1:
if index != self.target_index:
continue
out_index = 0
else:
out_index = index
yield self._sse(
"image_generation.completed",
{
"type": "image_generation.completed",
self.response_field: b64,
"index": out_index,
"usage": {
"total_tokens": 0,
"input_tokens": 0,
"output_tokens": 0,
"input_tokens_details": {
"text_tokens": 0,
"image_tokens": 0,
},
},
},
)
except asyncio.CancelledError:
logger.debug("Image stream cancelled by client")
except StreamIdleTimeoutError as e:
raise UpstreamException(
message=f"Image stream idle timeout after {e.idle_seconds}s",
status_code=504,
details={
"error": str(e),
"type": "stream_idle_timeout",
"idle_seconds": e.idle_seconds,
},
)
except RequestsError as e:
if _is_http2_stream_error(e):
logger.warning(f"HTTP/2 stream error in image: {e}")
raise UpstreamException(
message="Upstream connection closed unexpectedly",
status_code=502,
details={"error": str(e), "type": "http2_stream_error"},
)
logger.error(f"Image stream request error: {e}")
raise UpstreamException(
message=f"Upstream request failed: {e}",
status_code=502,
details={"error": str(e)},
)
except Exception as e:
logger.error(
f"Image stream processing error: {e}",
extra={"error_type": type(e).__name__},
)
raise
finally:
await self.close()
class ImageCollectProcessor(BaseProcessor):
"""图片生成非流式响应处理器"""
def __init__(self, model: str, token: str = "", response_format: str = "b64_json"):
super().__init__(model, token)
self.response_format = response_format
async def process(self, response: AsyncIterable[bytes]) -> List[str]:
"""处理并收集图片"""
images = []
idle_timeout = get_config("timeout.stream_idle_timeout")
try:
async for line in _with_idle_timeout(response, idle_timeout, self.model):
line = _normalize_stream_line(line)
if not line:
continue
try:
data = orjson.loads(line)
except orjson.JSONDecodeError:
continue
resp = data.get("result", {}).get("response", {})
if mr := resp.get("modelResponse"):
if urls := _collect_image_urls(mr):
for url in urls:
if self.response_format == "url":
processed = await self.process_url(url, "image")
if processed:
images.append(processed)
continue
try:
dl_service = self._get_dl()
base64_data = await dl_service.to_base64(
url, self.token, "image"
)
if base64_data:
if "," in base64_data:
b64 = base64_data.split(",", 1)[1]
else:
b64 = base64_data
images.append(b64)
except Exception as e:
logger.warning(
f"Failed to convert image to base64, falling back to URL: {e}"
)
processed = await self.process_url(url, "image")
if processed:
images.append(processed)
except asyncio.CancelledError:
logger.debug("Image collect cancelled by client")
except StreamIdleTimeoutError as e:
logger.warning(f"Image collect idle timeout: {e}")
except RequestsError as e:
if _is_http2_stream_error(e):
logger.warning(f"HTTP/2 stream error in image collect: {e}")
else:
logger.error(f"Image collect request error: {e}")
except Exception as e:
logger.error(
f"Image collect processing error: {e}",
extra={"error_type": type(e).__name__},
)
finally:
await self.close()
return images
__all__ = ["ImageStreamProcessor", "ImageCollectProcessor"]