From caec23b5cc1f8975e1224fc6d23b7760f53f53d8 Mon Sep 17 00:00:00 2001 From: Jony <951565127@qq.com> Date: Sun, 26 Apr 2026 01:13:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20GPT=20Image=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E4=BD=93=E8=B6=85=2020MB=20=E6=97=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=AD=89=E6=AF=94=E7=BC=A9=E6=94=BE=E5=9B=BE=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clients/gpt_image_client.py | 59 +++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/clients/gpt_image_client.py b/clients/gpt_image_client.py index 23fa308..de9012f 100644 --- a/clients/gpt_image_client.py +++ b/clients/gpt_image_client.py @@ -70,6 +70,41 @@ class GptImageClient: # ── 图像转换工具 ────────────────────────────────────────────────────────── + # ── 请求体大小限制 ──────────────────────────────────────────────────────── + _MAX_BODY_BYTES = 20 * 1024 * 1024 # 20 MB + + @staticmethod + def _shrink_png_to_limit(png_bytes: bytes, max_bytes: int, label: str = "") -> bytes: + """ + 若 PNG bytes 超过 max_bytes,按等比缩放反复压缩直到满足限制。 + 每次将面积缩小至约 80%(线性尺寸缩小至约 89.4%)。 + """ + if len(png_bytes) <= max_bytes: + return png_bytes + + img = Image.open(BytesIO(png_bytes)) + w, h = img.size + original_size = len(png_bytes) + step = 0 + + while len(png_bytes) > max_bytes: + scale = 0.894 # sqrt(0.8),面积缩小 20% + w = max(1, int(w * scale)) + h = max(1, int(h * scale)) + img = img.resize((w, h), Image.LANCZOS) + buf = BytesIO() + img.save(buf, format="PNG") + png_bytes = buf.getvalue() + step += 1 + + tag = f" ({label})" if label else "" + print( + f"[o1key GPT Image] 图像{tag}超出 {max_bytes // (1024*1024)}MB 限制," + f"已等比缩放 {step} 次:{original_size // 1024}KB → {len(png_bytes) // 1024}KB " + f"({w}×{h})" + ) + return png_bytes + @staticmethod def _tensor_to_png_bytes(tensor: torch.Tensor) -> bytes: """ @@ -220,8 +255,20 @@ class GptImageClient: if image_tensor is not None: pil_images = tensor_to_pil(image_tensor) data_urls = [] - for img in pil_images: - b64 = encode_image_to_base64(img, format="PNG") + for idx_img, img in enumerate(pil_images): + buf = BytesIO() + img.save(buf, format="PNG") + png_bytes = buf.getvalue() + # 单张图像预算:20MB 按图数平摊,至少保留 1MB 给其他字段 + per_image_budget = max( + 1024 * 1024, + (self._MAX_BODY_BYTES - 1024 * 1024) // len(pil_images), + ) + # base64 膨胀约 4/3,所以 PNG 目标上限 = budget * 3/4 + png_budget = int(per_image_budget * 3 / 4) + label = f"第{idx_img + 1}张" if len(pil_images) > 1 else "" + png_bytes = self._shrink_png_to_limit(png_bytes, png_budget, label) + b64 = base64.b64encode(png_bytes).decode("utf-8") data_urls.append(f"data:image/png;base64,{b64}") body["image"] = data_urls[0] if len(data_urls) == 1 else data_urls mode = f"图生图(参考图 {len(data_urls)} 张)" @@ -302,9 +349,17 @@ class GptImageClient: form.add_field("size", size if size else "auto") # 多图:用 image[] 数组字段逐张附加,支持 gpt-image-1.5 最多 16 张 + # 预算:20MB 按图数平摊,蒙版预留 1MB + mask_reserve = 1024 * 1024 if mask_tensor is not None else 0 + per_image_budget = max( + 1024 * 1024, + (self._MAX_BODY_BYTES - mask_reserve) // num_images, + ) for i in range(num_images): frame = image_tensor[i:i+1] # [1,H,W,C] img_bytes = self._tensor_to_png_bytes(frame) + label = f"第{i + 1}张" if num_images > 1 else "" + img_bytes = self._shrink_png_to_limit(img_bytes, per_image_budget, label) form.add_field( "image[]", img_bytes,