Update app.py
Browse files
app.py
CHANGED
|
@@ -182,6 +182,51 @@ def decode_base64_file(data_url: str) -> tuple[str, str, str]:
|
|
| 182 |
logger.error(f"Failed to parse data URL: {e}")
|
| 183 |
return None, None, None
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
async def upload_image_to_imgbb(base64_data: str) -> str:
|
| 186 |
"""
|
| 187 |
将 base64 图片上传到 imgbb
|
|
@@ -280,6 +325,25 @@ def format_image_as_data_url(base64_data: str) -> str:
|
|
| 280 |
logger.warning(f"Failed to detect image format: {e}, using JPEG as default")
|
| 281 |
return f"data:image/jpeg;base64,{base64_data}"
|
| 282 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str], List[Dict[str, str]]]:
|
| 284 |
"""
|
| 285 |
从消息中提取文本内容、图片和文件
|
|
@@ -290,7 +354,10 @@ def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str
|
|
| 290 |
files = []
|
| 291 |
|
| 292 |
if isinstance(content, str):
|
| 293 |
-
#
|
|
|
|
|
|
|
|
|
|
| 294 |
return content, images, files
|
| 295 |
elif isinstance(content, list):
|
| 296 |
# 复合消息(文本 + 图片 + 文件)
|
|
@@ -301,7 +368,12 @@ def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str
|
|
| 301 |
item_type = item.get("type", "")
|
| 302 |
|
| 303 |
if item_type == "text":
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
elif item_type == "image_url":
|
| 307 |
image_url = item.get("image_url", {})
|
|
@@ -319,8 +391,12 @@ def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str
|
|
| 319 |
logger.warning(f"Image URL format not supported: {url[:100]}...")
|
| 320 |
except Exception as e:
|
| 321 |
logger.error(f"Error processing image: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
else:
|
| 323 |
-
logger.warning(f"
|
| 324 |
|
| 325 |
elif item_type == "file" or (item_type == "image_url" and not item.get("image_url", {}).get("url", "").startswith("data:image/")):
|
| 326 |
# 处理文件上传
|
|
@@ -349,6 +425,10 @@ def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str
|
|
| 349 |
|
| 350 |
elif isinstance(item, str):
|
| 351 |
text_parts.append(item)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
return " ".join(text_parts), images, files
|
| 354 |
|
|
@@ -450,11 +530,24 @@ async def transform_openai_to_replicate(openai_request: Dict[str, Any], model_ov
|
|
| 450 |
formatted_image = None
|
| 451 |
if has_images and primary_image:
|
| 452 |
logger.info(f"Processing image for model {model} with format {model_config.get('image_format')}")
|
| 453 |
-
formatted_image = await format_image_for_model(primary_image, model_config)
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
# 构建 Replicate 格式的输入
|
| 460 |
replicate_input = {}
|
|
@@ -661,10 +754,11 @@ async def root():
|
|
| 661 |
"status": "running",
|
| 662 |
"replicate_token_configured": bool(REPLICATE_API_TOKEN),
|
| 663 |
"imgbb_token_configured": bool(IMGBB_API_KEY),
|
| 664 |
-
"version": "1.
|
| 665 |
"supported_models": list(MODEL_CONFIGS.keys()),
|
| 666 |
"vision_support": True,
|
| 667 |
"file_support": True,
|
|
|
|
| 668 |
"supported_text_files": list(SUPPORTED_TEXT_EXTENSIONS),
|
| 669 |
"supported_image_files": list(SUPPORTED_IMAGE_EXTENSIONS),
|
| 670 |
"claude4_vision_support": "Full support via imgbb image hosting"
|
|
|
|
| 182 |
logger.error(f"Failed to parse data URL: {e}")
|
| 183 |
return None, None, None
|
| 184 |
|
| 185 |
+
async def download_image_from_url(url: str) -> str:
|
| 186 |
+
"""
|
| 187 |
+
从URL下载图片并转换为base64
|
| 188 |
+
返回base64编码的图片数据
|
| 189 |
+
"""
|
| 190 |
+
try:
|
| 191 |
+
logger.info(f"Downloading image from URL: {url}")
|
| 192 |
+
|
| 193 |
+
async with aiohttp.ClientSession() as session:
|
| 194 |
+
async with session.get(url, timeout=30) as response:
|
| 195 |
+
if response.status == 200:
|
| 196 |
+
image_bytes = await response.read()
|
| 197 |
+
|
| 198 |
+
# 检测图片格式
|
| 199 |
+
content_type = response.headers.get('content-type', '')
|
| 200 |
+
if not content_type.startswith('image/'):
|
| 201 |
+
# 尝试从文件扩展名推断
|
| 202 |
+
if url.lower().endswith(('.jpg', '.jpeg')):
|
| 203 |
+
content_type = 'image/jpeg'
|
| 204 |
+
elif url.lower().endswith('.png'):
|
| 205 |
+
content_type = 'image/png'
|
| 206 |
+
elif url.lower().endswith('.gif'):
|
| 207 |
+
content_type = 'image/gif'
|
| 208 |
+
elif url.lower().endswith('.webp'):
|
| 209 |
+
content_type = 'image/webp'
|
| 210 |
+
else:
|
| 211 |
+
content_type = 'image/jpeg' # 默认
|
| 212 |
+
|
| 213 |
+
# 转换为base64
|
| 214 |
+
base64_data = base64.b64encode(image_bytes).decode('utf-8')
|
| 215 |
+
data_url = f"data:{content_type};base64,{base64_data}"
|
| 216 |
+
|
| 217 |
+
logger.info(f"Successfully downloaded image, size: {len(image_bytes)} bytes, base64 size: {len(base64_data)} chars")
|
| 218 |
+
return data_url
|
| 219 |
+
else:
|
| 220 |
+
logger.error(f"Failed to download image: HTTP {response.status}")
|
| 221 |
+
return None
|
| 222 |
+
|
| 223 |
+
except asyncio.TimeoutError:
|
| 224 |
+
logger.error(f"Timeout downloading image from {url}")
|
| 225 |
+
return None
|
| 226 |
+
except Exception as e:
|
| 227 |
+
logger.error(f"Error downloading image from {url}: {e}")
|
| 228 |
+
return None
|
| 229 |
+
|
| 230 |
async def upload_image_to_imgbb(base64_data: str) -> str:
|
| 231 |
"""
|
| 232 |
将 base64 图片上传到 imgbb
|
|
|
|
| 325 |
logger.warning(f"Failed to detect image format: {e}, using JPEG as default")
|
| 326 |
return f"data:image/jpeg;base64,{base64_data}"
|
| 327 |
|
| 328 |
+
def extract_images_from_context(content: str) -> List[str]:
|
| 329 |
+
"""
|
| 330 |
+
从系统上下文中提取图片URL
|
| 331 |
+
"""
|
| 332 |
+
images = []
|
| 333 |
+
try:
|
| 334 |
+
# 查找类似 <image name="..." url="..."></image> 的标签
|
| 335 |
+
import re
|
| 336 |
+
pattern = r'<image[^>]+url="([^"]+)"[^>]*></image>'
|
| 337 |
+
matches = re.findall(pattern, content)
|
| 338 |
+
for url in matches:
|
| 339 |
+
if url.startswith('http'):
|
| 340 |
+
images.append(url)
|
| 341 |
+
logger.info(f"Found image URL in context: {url}")
|
| 342 |
+
except Exception as e:
|
| 343 |
+
logger.error(f"Error extracting images from context: {e}")
|
| 344 |
+
|
| 345 |
+
return images
|
| 346 |
+
|
| 347 |
def extract_content_from_message(message: Dict[str, Any]) -> tuple[str, List[str], List[Dict[str, str]]]:
|
| 348 |
"""
|
| 349 |
从消息中提取文本内容、图片和文件
|
|
|
|
| 354 |
files = []
|
| 355 |
|
| 356 |
if isinstance(content, str):
|
| 357 |
+
# 检查文本内容中是否包含系统上下文中的图片
|
| 358 |
+
context_images = extract_images_from_context(content)
|
| 359 |
+
if context_images:
|
| 360 |
+
images.extend(context_images)
|
| 361 |
return content, images, files
|
| 362 |
elif isinstance(content, list):
|
| 363 |
# 复合消息(文本 + 图片 + 文件)
|
|
|
|
| 368 |
item_type = item.get("type", "")
|
| 369 |
|
| 370 |
if item_type == "text":
|
| 371 |
+
text_content = item.get("text", "")
|
| 372 |
+
text_parts.append(text_content)
|
| 373 |
+
# 检查文本中的上下文图片
|
| 374 |
+
context_images = extract_images_from_context(text_content)
|
| 375 |
+
if context_images:
|
| 376 |
+
images.extend(context_images)
|
| 377 |
|
| 378 |
elif item_type == "image_url":
|
| 379 |
image_url = item.get("image_url", {})
|
|
|
|
| 391 |
logger.warning(f"Image URL format not supported: {url[:100]}...")
|
| 392 |
except Exception as e:
|
| 393 |
logger.error(f"Error processing image: {e}")
|
| 394 |
+
elif url.startswith("http"):
|
| 395 |
+
# 外部图片URL
|
| 396 |
+
images.append(url)
|
| 397 |
+
logger.info(f"Found external image URL: {url}")
|
| 398 |
else:
|
| 399 |
+
logger.warning(f"Unsupported image URL format: {url}")
|
| 400 |
|
| 401 |
elif item_type == "file" or (item_type == "image_url" and not item.get("image_url", {}).get("url", "").startswith("data:image/")):
|
| 402 |
# 处理文件上传
|
|
|
|
| 425 |
|
| 426 |
elif isinstance(item, str):
|
| 427 |
text_parts.append(item)
|
| 428 |
+
# 检查文本中的上下文图片
|
| 429 |
+
context_images = extract_images_from_context(item)
|
| 430 |
+
if context_images:
|
| 431 |
+
images.extend(context_images)
|
| 432 |
|
| 433 |
return " ".join(text_parts), images, files
|
| 434 |
|
|
|
|
| 530 |
formatted_image = None
|
| 531 |
if has_images and primary_image:
|
| 532 |
logger.info(f"Processing image for model {model} with format {model_config.get('image_format')}")
|
|
|
|
| 533 |
|
| 534 |
+
# 如果是外部URL,先下载转换为base64
|
| 535 |
+
if primary_image.startswith("http"):
|
| 536 |
+
logger.info(f"Downloading external image: {primary_image}")
|
| 537 |
+
downloaded_image = await download_image_from_url(primary_image)
|
| 538 |
+
if downloaded_image:
|
| 539 |
+
primary_image = downloaded_image
|
| 540 |
+
logger.info("External image downloaded and converted to base64")
|
| 541 |
+
else:
|
| 542 |
+
logger.error("Failed to download external image")
|
| 543 |
+
primary_image = None
|
| 544 |
+
|
| 545 |
+
if primary_image:
|
| 546 |
+
formatted_image = await format_image_for_model(primary_image, model_config)
|
| 547 |
+
|
| 548 |
+
if not formatted_image:
|
| 549 |
+
logger.error("Failed to format image for model")
|
| 550 |
+
raise HTTPException(status_code=500, detail="Failed to process image")
|
| 551 |
|
| 552 |
# 构建 Replicate 格式的输入
|
| 553 |
replicate_input = {}
|
|
|
|
| 754 |
"status": "running",
|
| 755 |
"replicate_token_configured": bool(REPLICATE_API_TOKEN),
|
| 756 |
"imgbb_token_configured": bool(IMGBB_API_KEY),
|
| 757 |
+
"version": "1.3.0",
|
| 758 |
"supported_models": list(MODEL_CONFIGS.keys()),
|
| 759 |
"vision_support": True,
|
| 760 |
"file_support": True,
|
| 761 |
+
"external_image_support": True,
|
| 762 |
"supported_text_files": list(SUPPORTED_TEXT_EXTENSIONS),
|
| 763 |
"supported_image_files": list(SUPPORTED_IMAGE_EXTENSIONS),
|
| 764 |
"claude4_vision_support": "Full support via imgbb image hosting"
|