|
11 | 11 | → resolved to "gemini-3.0-pro-image-landscape-2k" |
12 | 12 | """ |
13 | 13 |
|
| 14 | +import re |
14 | 15 | from typing import Optional, Dict, Any, Tuple |
15 | 16 | from ..core.logger import debug_logger |
16 | 17 |
|
|
96 | 97 | # 默认 aspectRatio |
97 | 98 | DEFAULT_ASPECT = "landscape" |
98 | 99 |
|
| 100 | +OPENAI_IMAGE_SIZE_RE = re.compile(r"^(?P<w>\d{2,5})\s*[xX]\s*(?P<h>\d{2,5})$") |
| 101 | + |
| 102 | +# OpenAI 常见 quality → imageSize 映射 |
| 103 | +# - 这里的 imageSize 是 flow2api 的“放大档位”,并不等价于 OpenAI 的像素尺寸; |
| 104 | +# 但可用作“画质/清晰度”的近似映射。 |
| 105 | +OPENAI_QUALITY_MAP = { |
| 106 | + "low": None, |
| 107 | + "standard": None, |
| 108 | + "medium": "2k", |
| 109 | + "high": "4k", |
| 110 | + "hd": "4k", |
| 111 | + "ultra": "4k", |
| 112 | +} |
| 113 | + |
| 114 | +# 用于把 OpenAI size(如 1024x1792)映射到最接近的 flow2api aspect 选项 |
| 115 | +ASPECT_RATIO_FLOAT_MAP = { |
| 116 | + "landscape": 16 / 9, |
| 117 | + "portrait": 9 / 16, |
| 118 | + "square": 1.0, |
| 119 | + "four-three": 4 / 3, |
| 120 | + "three-four": 3 / 4, |
| 121 | +} |
| 122 | + |
99 | 123 |
|
100 | 124 | # ────────────────────────────────────────────── |
101 | 125 | # 视频模型简化名映射 |
@@ -173,47 +197,214 @@ def _extract_generation_params(request) -> Tuple[Optional[str], Optional[str]]: |
173 | 197 | 优先级: |
174 | 198 | 1. request.generationConfig.imageConfig (顶层 Gemini 参数) |
175 | 199 | 2. extra fields 中的 generationConfig (extra_body 透传) |
| 200 | + 3. OpenAI 风格字段(size/quality)兼容:可在 generationConfig/imageConfig 或顶层 extra 中出现 |
176 | 201 |
|
177 | 202 | Returns: |
178 | 203 | (aspect_ratio, image_size) 归一化后的值 |
179 | 204 | """ |
180 | | - aspect_ratio = None |
181 | | - image_size = None |
| 205 | + def _normalize_str(value: Any) -> Optional[str]: |
| 206 | + if not isinstance(value, str): |
| 207 | + return None |
| 208 | + text = value.strip() |
| 209 | + return text if text else None |
| 210 | + |
| 211 | + def _read_value(obj: Any, *keys: str) -> Any: |
| 212 | + if obj is None: |
| 213 | + return None |
| 214 | + if isinstance(obj, dict): |
| 215 | + for key in keys: |
| 216 | + if key in obj: |
| 217 | + return obj.get(key) |
| 218 | + return None |
| 219 | + |
| 220 | + for key in keys: |
| 221 | + if hasattr(obj, key): |
| 222 | + value = getattr(obj, key, None) |
| 223 | + if value is not None: |
| 224 | + return value |
| 225 | + |
| 226 | + extra = getattr(obj, "__pydantic_extra__", None) or {} |
| 227 | + for key in keys: |
| 228 | + if key in extra: |
| 229 | + return extra.get(key) |
| 230 | + return None |
| 231 | + |
| 232 | + def _normalize_aspect_ratio(value: Any) -> Optional[str]: |
| 233 | + raw = _normalize_str(value) |
| 234 | + if not raw: |
| 235 | + return None |
| 236 | + |
| 237 | + token = ( |
| 238 | + raw.replace(":", ":") |
| 239 | + .replace("/", ":") |
| 240 | + .replace("x", ":") |
| 241 | + .replace("X", ":") |
| 242 | + .replace(" ", "") |
| 243 | + .strip() |
| 244 | + ) |
| 245 | + |
| 246 | + mapped = ASPECT_RATIO_MAP.get(token) |
| 247 | + if mapped: |
| 248 | + return mapped |
| 249 | + mapped = ASPECT_RATIO_MAP.get(token.lower()) |
| 250 | + if mapped: |
| 251 | + return mapped |
| 252 | + mapped = ASPECT_RATIO_MAP.get(token.upper()) |
| 253 | + if mapped: |
| 254 | + return mapped |
| 255 | + return token |
| 256 | + |
| 257 | + def _normalize_image_size(value: Any) -> Optional[str]: |
| 258 | + raw = _normalize_str(value) |
| 259 | + if not raw: |
| 260 | + return None |
| 261 | + |
| 262 | + token = raw.replace(" ", "").strip() |
| 263 | + mapped = IMAGE_SIZE_MAP.get(token) |
| 264 | + if mapped is not None: |
| 265 | + return mapped or None |
| 266 | + mapped = IMAGE_SIZE_MAP.get(token.lower()) |
| 267 | + if mapped is not None: |
| 268 | + return mapped or None |
| 269 | + mapped = IMAGE_SIZE_MAP.get(token.upper()) |
| 270 | + if mapped is not None: |
| 271 | + return mapped or None |
| 272 | + return token.lower() |
| 273 | + |
| 274 | + def _aspect_from_openai_size(value: Any) -> Optional[str]: |
| 275 | + raw = _normalize_str(value) |
| 276 | + if not raw: |
| 277 | + return None |
| 278 | + |
| 279 | + match = OPENAI_IMAGE_SIZE_RE.match(raw) |
| 280 | + if not match: |
| 281 | + return None |
| 282 | + |
| 283 | + try: |
| 284 | + width = int(match.group("w")) |
| 285 | + height = int(match.group("h")) |
| 286 | + except Exception: |
| 287 | + return None |
| 288 | + |
| 289 | + if width <= 0 or height <= 0: |
| 290 | + return None |
| 291 | + |
| 292 | + ratio = width / height |
| 293 | + best = min( |
| 294 | + ASPECT_RATIO_FLOAT_MAP.items(), |
| 295 | + key=lambda item: abs(ratio - item[1]), |
| 296 | + )[0] |
| 297 | + return best |
| 298 | + |
| 299 | + def _image_size_from_openai_quality(value: Any) -> Optional[str]: |
| 300 | + raw = _normalize_str(value) |
| 301 | + if not raw: |
| 302 | + return None |
| 303 | + |
| 304 | + token = raw.strip().lower() |
| 305 | + if token in IMAGE_SIZE_MAP: |
| 306 | + return _normalize_image_size(token) |
| 307 | + |
| 308 | + mapped = OPENAI_QUALITY_MAP.get(token) |
| 309 | + if mapped: |
| 310 | + return mapped |
| 311 | + return None |
| 312 | + |
| 313 | + def _apply_image_config(image_config: Any, aspect_ratio: Optional[str], image_size: Optional[str]) -> Tuple[Optional[str], Optional[str]]: |
| 314 | + # 显式 aspectRatio/imageSize |
| 315 | + if not aspect_ratio: |
| 316 | + aspect_ratio = _normalize_aspect_ratio( |
| 317 | + _read_value(image_config, "aspectRatio", "aspect_ratio", "aspect") |
| 318 | + ) |
| 319 | + if not image_size: |
| 320 | + image_size = _normalize_image_size( |
| 321 | + _read_value(image_config, "imageSize", "image_size", "resolution") |
| 322 | + ) |
| 323 | + |
| 324 | + # OpenAI size/quality |
| 325 | + if not aspect_ratio: |
| 326 | + aspect_ratio = _aspect_from_openai_size(_read_value(image_config, "size")) |
| 327 | + if not image_size: |
| 328 | + image_size = _image_size_from_openai_quality( |
| 329 | + _read_value(image_config, "quality", "imageQuality", "image_quality") |
| 330 | + ) |
182 | 331 |
|
183 | | - # 尝试从 generationConfig 提取 |
| 332 | + return aspect_ratio, image_size |
| 333 | + |
| 334 | + aspect_ratio: Optional[str] = None |
| 335 | + image_size: Optional[str] = None |
| 336 | + |
| 337 | + # 1) 优先从 request.generationConfig 解析 |
184 | 338 | gen_config = getattr(request, "generationConfig", None) |
| 339 | + if gen_config is not None: |
| 340 | + image_config = _read_value(gen_config, "imageConfig", "image_config") |
| 341 | + if image_config is not None: |
| 342 | + aspect_ratio, image_size = _apply_image_config( |
| 343 | + image_config, aspect_ratio, image_size |
| 344 | + ) |
| 345 | + |
| 346 | + # 有些上游会把字段放在 generationConfig 顶层 |
| 347 | + if not aspect_ratio: |
| 348 | + aspect_ratio = _normalize_aspect_ratio( |
| 349 | + _read_value(gen_config, "aspectRatio", "aspect_ratio") |
| 350 | + ) |
| 351 | + if not image_size: |
| 352 | + image_size = _normalize_image_size( |
| 353 | + _read_value(gen_config, "imageSize", "image_size") |
| 354 | + ) |
| 355 | + |
| 356 | + if not aspect_ratio: |
| 357 | + aspect_ratio = _aspect_from_openai_size(_read_value(gen_config, "size")) |
| 358 | + if not image_size: |
| 359 | + image_size = _image_size_from_openai_quality(_read_value(gen_config, "quality")) |
185 | 360 |
|
186 | | - # 如果顶层没有,尝试从 extra fields (Pydantic extra="allow") |
187 | | - if gen_config is None and hasattr(request, "__pydantic_extra__"): |
| 361 | + # 2) 顶层没有时,再尝试从 extra fields (Pydantic extra="allow") 中透传的 generationConfig |
| 362 | + if (aspect_ratio is None or image_size is None) and hasattr(request, "__pydantic_extra__"): |
188 | 363 | extra = request.__pydantic_extra__ or {} |
189 | 364 | gen_config_raw = extra.get("generationConfig") |
190 | 365 | if not isinstance(gen_config_raw, dict): |
191 | 366 | extra_body = extra.get("extra_body") or extra.get("extraBody") |
192 | 367 | if isinstance(extra_body, dict): |
193 | 368 | gen_config_raw = extra_body.get("generationConfig") |
| 369 | + |
194 | 370 | if isinstance(gen_config_raw, dict): |
195 | | - image_config_raw = gen_config_raw.get("imageConfig", {}) |
196 | | - if isinstance(image_config_raw, dict): |
197 | | - aspect_ratio = image_config_raw.get("aspectRatio") |
198 | | - image_size = image_config_raw.get("imageSize") |
199 | | - return ( |
200 | | - ASPECT_RATIO_MAP.get(aspect_ratio, aspect_ratio) |
201 | | - if aspect_ratio |
202 | | - else None, |
203 | | - IMAGE_SIZE_MAP.get(image_size, image_size) if image_size else None, |
| 371 | + image_config_raw = ( |
| 372 | + gen_config_raw.get("imageConfig") |
| 373 | + or gen_config_raw.get("image_config") |
| 374 | + or {} |
204 | 375 | ) |
| 376 | + if image_config_raw: |
| 377 | + aspect_ratio, image_size = _apply_image_config( |
| 378 | + image_config_raw, aspect_ratio, image_size |
| 379 | + ) |
205 | 380 |
|
206 | | - if gen_config is not None: |
207 | | - image_config = getattr(gen_config, "imageConfig", None) |
208 | | - if image_config is not None: |
209 | | - aspect_ratio = getattr(image_config, "aspectRatio", None) |
210 | | - image_size = getattr(image_config, "imageSize", None) |
211 | | - |
212 | | - # 归一化 |
213 | | - if aspect_ratio: |
214 | | - aspect_ratio = ASPECT_RATIO_MAP.get(aspect_ratio, aspect_ratio) |
215 | | - if image_size: |
216 | | - image_size = IMAGE_SIZE_MAP.get(image_size, image_size) |
| 381 | + if aspect_ratio is None: |
| 382 | + aspect_ratio = _normalize_aspect_ratio( |
| 383 | + gen_config_raw.get("aspectRatio") or gen_config_raw.get("aspect_ratio") |
| 384 | + ) |
| 385 | + if image_size is None: |
| 386 | + image_size = _normalize_image_size( |
| 387 | + gen_config_raw.get("imageSize") or gen_config_raw.get("image_size") |
| 388 | + ) |
| 389 | + |
| 390 | + if aspect_ratio is None: |
| 391 | + aspect_ratio = _aspect_from_openai_size(gen_config_raw.get("size")) |
| 392 | + if image_size is None: |
| 393 | + image_size = _image_size_from_openai_quality(gen_config_raw.get("quality")) |
| 394 | + |
| 395 | + # 3) OpenAI 风格 size/quality(顶层 extra)兼容 |
| 396 | + if (aspect_ratio is None or image_size is None) and hasattr(request, "__pydantic_extra__"): |
| 397 | + extra = request.__pydantic_extra__ or {} |
| 398 | + if aspect_ratio is None: |
| 399 | + aspect_ratio = _aspect_from_openai_size(extra.get("size")) |
| 400 | + if image_size is None: |
| 401 | + image_size = _image_size_from_openai_quality(extra.get("quality")) |
| 402 | + |
| 403 | + # 一些上游可能直接传 aspect_ratio/image_size |
| 404 | + if aspect_ratio is None: |
| 405 | + aspect_ratio = _normalize_aspect_ratio(extra.get("aspect_ratio") or extra.get("aspectRatio")) |
| 406 | + if image_size is None: |
| 407 | + image_size = _normalize_image_size(extra.get("image_size") or extra.get("imageSize")) |
217 | 408 |
|
218 | 409 | return aspect_ratio, image_size |
219 | 410 |
|
|
0 commit comments