From b2dff38f61d84472225b8c3796192fc794ac4c09 Mon Sep 17 00:00:00 2001 From: tangwang Date: Mon, 20 Apr 2026 22:45:07 +0800 Subject: [PATCH] embedding-image接口(POST /embed/image)支持SVG格式:先转 PNG 再走 CN-CLIP --- embeddings/server.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- requirements_embedding_service.txt | 3 +++ 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/embeddings/server.py b/embeddings/server.py index ef92bc7..dec063d 100644 --- a/embeddings/server.py +++ b/embeddings/server.py @@ -15,9 +15,11 @@ import time import uuid from collections import deque from dataclasses import dataclass +import tempfile from typing import Any, Dict, List, Optional import numpy as np +import requests from fastapi import FastAPI, HTTPException, Request, Response from fastapi.concurrency import run_in_threadpool @@ -847,19 +849,36 @@ def _embed_image_lane_impl( ) backend_t0 = time.perf_counter() - with _image_encode_lock: - if lane == "image": - vectors = _image_model.encode_image_urls( - missing_items, - batch_size=CONFIG.IMAGE_BATCH_SIZE, - normalize_embeddings=effective_normalize, - ) - else: - vectors = _image_model.encode_clip_texts( - missing_items, - batch_size=CONFIG.IMAGE_BATCH_SIZE, - normalize_embeddings=effective_normalize, - ) + tmp_png_paths: List[str] = [] + encode_inputs: List[str] = list(missing_items) + if lane == "image": + # Best-effort: rasterize SVGs into temporary PNGs so CN-CLIP can encode them. + for i, item in enumerate(missing_items): + if _looks_like_svg_image_ref(item): + png_path = _rasterize_svg_to_temp_png(item) + tmp_png_paths.append(png_path) + encode_inputs[i] = png_path + + try: + with _image_encode_lock: + if lane == "image": + vectors = _image_model.encode_image_urls( + encode_inputs, + batch_size=CONFIG.IMAGE_BATCH_SIZE, + normalize_embeddings=effective_normalize, + ) + else: + vectors = _image_model.encode_clip_texts( + missing_items, + batch_size=CONFIG.IMAGE_BATCH_SIZE, + normalize_embeddings=effective_normalize, + ) + finally: + for p in tmp_png_paths: + try: + os.remove(p) + except Exception: + pass if vectors is None or len(vectors) != len(missing_items): raise RuntimeError( f"{lane} lane length mismatch: expected {len(missing_items)}, " @@ -1284,6 +1303,57 @@ def _parse_string_inputs(raw: List[Any], *, kind: str, empty_detail: str) -> Lis return out +def _looks_like_svg_image_ref(value: str) -> bool: + """ + CN-CLIP image embedding path expects raster images (jpg/png/webp/...) that PIL can decode. + SVG is a vector format and currently not supported by the image embedding backend. + """ + v = (value or "").strip().lower() + if not v: + return False + # Drop query/fragment for URL suffix check. + for sep in ("?", "#"): + if sep in v: + v = v.split(sep, 1)[0] + return v.endswith(".svg") or v.startswith("data:image/svg+xml") + + +def _rasterize_svg_to_temp_png(svg_ref: str, *, timeout_sec: int = 10) -> str: + """ + Download/resolve an SVG ref (URL or local path) and rasterize it into a temporary PNG file. + + Returns the PNG file path (caller is responsible for cleanup). + """ + if svg_ref.startswith(("http://", "https://")): + resp = requests.get(svg_ref, timeout=timeout_sec) + if resp.status_code != 200: + raise ValueError(f"HTTP {resp.status_code} when downloading SVG") + svg_bytes = resp.content + else: + with open(svg_ref, "rb") as f: + svg_bytes = f.read() + + try: + import cairosvg # type: ignore + except Exception as exc: # pragma: no cover + raise RuntimeError( + "SVG rasterization requires optional dependency 'cairosvg'. " + "Install it in the embedding-image service environment." + ) from exc + + fd, out_path = tempfile.mkstemp(prefix="embed_svg_", suffix=".png") + os.close(fd) + try: + cairosvg.svg2png(bytestring=svg_bytes, write_to=out_path) + except Exception: + try: + os.remove(out_path) + except Exception: + pass + raise + return out_path + + async def _run_image_lane_embed( *, route: str, diff --git a/requirements_embedding_service.txt b/requirements_embedding_service.txt index 1086750..4dd4e6a 100644 --- a/requirements_embedding_service.txt +++ b/requirements_embedding_service.txt @@ -12,6 +12,9 @@ numpy>=1.24.0 pyyaml>=6.0 redis>=5.0.0 +# Optional: rasterize SVG to PNG for /embed/image +cairosvg>=2.7.0 + # Image backend via clip-as-service client setuptools<82 jina>=3.12.0 -- libgit2 0.21.2