bizyengine 1.2.49__py3-none-any.whl → 1.2.51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -201,6 +201,27 @@ class errnos:
201
201
  400, 400150, None, {"en": "Invalid prompt", "zh": "工作流错误"}
202
202
  )
203
203
 
204
+ # 聊天输入相关
205
+ MISSING_MESSAGE_OR_HISTORY = ErrorNo(
206
+ 400,
207
+ 400151,
208
+ None,
209
+ {
210
+ "en": "Missing message or conversation_history",
211
+ "zh": "缺少 message 或 conversation_history",
212
+ },
213
+ )
214
+
215
+ INVALID_CONVERSATION_HISTORY = ErrorNo(
216
+ 400,
217
+ 400152,
218
+ None,
219
+ {
220
+ "en": "Invalid conversation_history format",
221
+ "zh": "conversation_history 格式无效",
222
+ },
223
+ )
224
+
204
225
  INVALID_API_KEY = ErrorNo(
205
226
  401, 401000, None, {"en": "Invalid API key", "zh": "无效的API密钥"}
206
227
  )
@@ -1,11 +1,7 @@
1
1
  import asyncio
2
- import configparser
3
2
  import json
4
3
  import logging
5
- import os
6
- import random
7
4
  import re
8
- import shutil
9
5
  import threading
10
6
  import time
11
7
  import urllib.parse
@@ -13,11 +9,11 @@ import uuid
13
9
 
14
10
  import aiohttp
15
11
  import execution
16
- import openai
17
12
  from server import PromptServer
18
13
 
19
- from bizyengine.core.common.env_var import BIZYAIR_SERVER_ADDRESS, BIZYAIR_SERVER_MODE
20
- from bizyengine.core.configs.conf import ModelRule, config_manager
14
+ from bizyengine.bizybot.config import Config, LLMConfig, MCPServerConfig
15
+ from bizyengine.bizybot.coordinator import Coordinator
16
+ from bizyengine.core.common.env_var import BIZYAIR_DEBUG, BIZYAIR_SERVER_MODE
21
17
 
22
18
  from .api_client import APIClient
23
19
  from .errno import ErrorNo, errnos
@@ -45,6 +41,10 @@ BIZYAIR_DATA_DICT = {}
45
41
  BIZYAIR_DATA_DICT_UPDATED_AT = 0.0
46
42
 
47
43
  logging.basicConfig(level=logging.DEBUG)
44
+ # 禁用MCP与OpenAI使用的httpx等相关的DEBUG,INFO日志, 仅在BIZYAIR_DEBUG模式下启用
45
+ if not BIZYAIR_DEBUG:
46
+ logging.getLogger("mcp").setLevel(logging.WARNING)
47
+ logging.getLogger("httpx").setLevel(logging.WARNING)
48
48
 
49
49
 
50
50
  def _get_request_api_key(request_headers):
@@ -818,177 +818,137 @@ class BizyAirServer:
818
818
  return ErrResponse(err)
819
819
 
820
820
  @self.prompt_server.routes.post(f"/{MODEL_API}/chat")
821
- async def chat_completions(request):
822
- request_api_key, err = _get_request_api_key(request.headers)
823
- if err:
824
- return ErrResponse(err)
825
-
826
- response = None # 确保变量在退出前定义
827
- resp = None # 响应对象引用
828
- req_id = f"req-{id(request)}" # 为请求生成唯一ID
829
-
821
+ async def chat(request):
830
822
  try:
823
+ # 获取API密钥
824
+ request_api_key, err = _get_request_api_key(request.headers)
825
+ if err:
826
+ return ErrResponse(err)
827
+
831
828
  # 解析请求数据
832
829
  request_data = await request.json()
833
830
 
834
- # 转发请求到模型服务
835
- with self.api_client.forward_model_request(
836
- request_data, request_api_key
837
- ) as response:
838
- # 创建并准备流式响应
839
- resp = aiohttp.web.StreamResponse(
831
+ # 提取参数
832
+ current_message = request_data.get("message")
833
+ conversation_history = request_data.get("conversation_history", [])
834
+ model_config = request_data.get("model_config", {})
835
+
836
+ # 验证输入
837
+ if not current_message and not conversation_history:
838
+ return ErrResponse(errnos.MISSING_MESSAGE_OR_HISTORY)
839
+
840
+ # 验证对话历史格式
841
+ if conversation_history:
842
+ from bizyengine.bizybot.models import Conversation
843
+
844
+ if not Conversation.validate_conversation_history(
845
+ conversation_history
846
+ ):
847
+ return ErrResponse(errnos.INVALID_CONVERSATION_HISTORY)
848
+
849
+ if request_api_key:
850
+ api_key = request_api_key
851
+ if not BIZYAIR_SERVER_MODE:
852
+ from bizyengine.core.common.client import get_api_key
853
+
854
+ api_key = get_api_key()
855
+
856
+ if not api_key:
857
+ return ErrResponse(errnos.INVALID_API_KEY)
858
+
859
+ # 创建LLM配置
860
+ llm_config = LLMConfig(api_key=api_key, timeout=30.0)
861
+ config = Config(
862
+ llm=llm_config,
863
+ mcp_servers={
864
+ "bizyair-mcp": MCPServerConfig(
865
+ transport="streamable_http",
866
+ url="https://api.bizyair.cn/w/v1/mcp/32",
867
+ headers={
868
+ "Authorization": "Bearer " + api_key,
869
+ "X-BizyBot-Client": "bizybot",
870
+ },
871
+ ),
872
+ "bizyait-image-gen": MCPServerConfig(
873
+ transport="streamable_http",
874
+ url="https://api.bizyair.cn/w/v1/mcp/33",
875
+ headers={
876
+ "Authorization": "Bearer " + api_key,
877
+ "X-BizyBot-Client": "bizybot",
878
+ },
879
+ ),
880
+ "bizyait-image-edit": MCPServerConfig(
881
+ transport="streamable_http",
882
+ url="https://api.bizyair.cn/w/v1/mcp/34",
883
+ headers={
884
+ "Authorization": "Bearer " + api_key,
885
+ "X-BizyBot-Client": "bizybot",
886
+ },
887
+ ),
888
+ },
889
+ )
890
+
891
+ coordinator = Coordinator(config)
892
+ try:
893
+ await coordinator.initialize()
894
+
895
+ response = aiohttp.web.StreamResponse(
840
896
  status=200,
841
- reason="OK",
842
897
  headers={
843
898
  "Content-Type": "text/event-stream",
844
899
  "Cache-Control": "no-cache",
845
900
  "Connection": "keep-alive",
846
- "X-Accel-Buffering": "no", # 禁用Nginx缓冲
847
901
  },
848
902
  )
849
- await resp.prepare(request)
903
+ await response.prepare(request)
850
904
 
851
- # 开始流式传输
852
- any_chunk_sent = False # 跟踪是否发送了任何数据块
853
905
  try:
854
- for bytes in response.iter_bytes(1024):
855
- if bytes:
856
- await resp.write(bytes)
857
- any_chunk_sent = True
858
- await resp.drain() # 确保数据被立即发送
859
- except Exception as e:
860
- print(
861
- f"\033[31m[聊天请求-{req_id}]\033[0m 流式传输错误: {str(e)}"
906
+ import uuid
907
+
908
+ temp_conversation_id = str(uuid.uuid4())
909
+ conversation_event = {
910
+ "type": "conversation_started",
911
+ "conversation_id": temp_conversation_id,
912
+ }
913
+ await response.write(
914
+ f"data: {json.dumps(conversation_event, ensure_ascii=False)}\n\n".encode(
915
+ "utf-8"
916
+ )
862
917
  )
863
- # 如果尚未发送任何数据块,尝试发送错误信息
864
- if not any_chunk_sent and not resp.prepared:
865
- return ErrResponse(errnos.MODEL_API_ERROR)
866
- elif not any_chunk_sent:
867
- try:
868
- error_msg = json.dumps(
869
- {"error": f"流式传输错误: {str(e)}"}
870
- )
871
- await resp.write(
872
- f"data: {error_msg}\n\n".encode("utf-8")
873
- )
874
- await resp.write(b"data: [DONE]\n\n")
875
- except Exception as write_err:
876
- print(
877
- f"\033[31m[聊天请求-{req_id}]\033[0m 写入错误消息时出错: {str(write_err)}"
918
+
919
+ async for event in coordinator.process_message(
920
+ message=current_message,
921
+ conversation_history=conversation_history,
922
+ llm_config_override=model_config,
923
+ ):
924
+ formatted_event = coordinator.format_streaming_event(event)
925
+ await response.write(
926
+ f"data: {json.dumps(formatted_event, ensure_ascii=False)}\n\n".encode(
927
+ "utf-8"
878
928
  )
929
+ )
879
930
 
880
- try:
881
- await resp.write_eof()
931
+ await response.write(b"data: [DONE]\n\n")
882
932
  except Exception as e:
883
- print(
884
- f"\033[31m[聊天请求-{req_id}]\033[0m 结束响应时出错: {str(e)}"
885
- )
886
-
887
- return resp
933
+ error_event = {"type": "error", "error": str(e)}
934
+ try:
935
+ await response.write(
936
+ f"data: {json.dumps(error_event, ensure_ascii=False)}\n\n".encode(
937
+ "utf-8"
938
+ )
939
+ )
940
+ except Exception:
941
+ pass
888
942
 
889
- except openai.APIConnectionError as e:
890
- print("The server could not be reached")
891
- print(
892
- e.__cause__
893
- ) # an underlying Exception, likely raised within httpx.
894
- except openai.RateLimitError:
895
- print("A 429 status code was received; we should back off a bit.")
896
- except openai.APIStatusError as e:
897
- print("Another non-200-range status code was received")
898
- print(e.status_code)
899
- print(e.response)
900
- except Exception as e:
901
- print(
902
- f"\033[31m[聊天请求-{req_id}]\033[0m 处理请求时发生错误: {str(e)}"
903
- )
904
- # 如果响应已经准备好,尝试发送错误信息
905
- if resp and resp.prepared:
943
+ return response
944
+ finally:
906
945
  try:
907
- error_msg = json.dumps({"error": f"服务器错误: {str(e)}"})
908
- await resp.write(f"data: {error_msg}\n\n".encode("utf-8"))
909
- await resp.write(b"data: [DONE]\n\n")
910
- await resp.write_eof()
911
- except:
912
- pass
913
- return resp
914
-
915
- return ErrResponse(errnos.MODEL_API_ERROR)
946
+ await coordinator.cleanup()
947
+ except Exception:
948
+ logging.exception("Coordinator cleanup failed")
916
949
 
917
- @self.prompt_server.routes.post(f"/{MODEL_API}/image-edit")
918
- async def image_edit(request):
919
- try:
920
- request_api_key, err = _get_request_api_key(request.headers)
921
- if err:
922
- return ErrResponse(err)
923
- auth_header = request.headers.get("Authorization", "")
924
- cookie_str = request.headers.get("Cookie", "")
925
- cookie_api_key = _get_api_key_from_cookie(cookie_str)
926
- request_data = await request.json()
927
- target_url = f"{BIZYAIR_SERVER_ADDRESS}{config_manager.get_rules('comfyagent-flux1-kontext')[0].route}"
928
- forward_payload = {
929
- "prompt": request_data.get("prompt", "请编辑这张图片"),
930
- "image": request_data.get("image", ""),
931
- "seed": request_data.get("seed", random.randint(1, 9999999999)),
932
- "num_inference_steps": request_data.get("num_inference_steps", 20),
933
- "guidance_scale": request_data.get("guidance_scale", 2.5),
934
- }
935
- async with aiohttp.ClientSession() as session:
936
- headers = {
937
- "Content-Type": "application/json",
938
- "User-Agent": "BizyAir Server",
939
- "x-bizyair-client-version": "1.2.21",
940
- }
941
- if request_api_key:
942
- headers["Authorization"] = f"Bearer {request_api_key}"
943
- elif cookie_api_key:
944
- headers["Authorization"] = f"Bearer {cookie_api_key}"
945
- elif auth_header:
946
- if not auth_header.startswith("Bearer "):
947
- headers["Authorization"] = f"Bearer {auth_header}"
948
- else:
949
- headers["Authorization"] = auth_header
950
- else:
951
- from bizyengine.core.common.env_var import load_api_key
952
-
953
- has_key, file_api_key = load_api_key()
954
- if has_key and file_api_key:
955
- headers["Authorization"] = f"Bearer {file_api_key}"
956
- logging.debug("Using API key from api_key.ini file")
957
- async with session.post(
958
- target_url, headers=headers, json=forward_payload, timeout=120
959
- ) as response:
960
- response_data = await response.json()
961
- logging.debug(response_data)
962
-
963
- if response.status == 200:
964
- return OKResponse(response_data)
965
- else:
966
- return ErrResponse(errnos.MODEL_API_ERROR)
967
-
968
- except Exception:
969
- return ErrResponse(errnos.MODEL_API_ERROR)
970
-
971
- @self.prompt_server.routes.post(f"/{MODEL_API}/images")
972
- async def image_generations(request):
973
- try:
974
- request_api_key, err = _get_request_api_key(request.headers)
975
- if err:
976
- return ErrResponse(err)
977
-
978
- # 解析请求数据
979
- request_data = await request.json()
980
-
981
- # 转发图像生成请求
982
- result, err = await self.api_client.forward_image_request(
983
- request_data, request_api_key
984
- )
985
- if err is not None:
986
- return ErrResponse(err)
987
-
988
- # 返回结果
989
- return OKResponse(result)
990
-
991
- except Exception:
950
+ except Exception as e:
951
+ print(f"\033[31m[BizyAir]\033[0m Chat request failed: {str(e)}")
992
952
  return ErrResponse(errnos.MODEL_API_ERROR)
993
953
 
994
954
  # 服务器模式独占
@@ -1000,7 +960,7 @@ class BizyAirServer:
1000
960
  async def list_share_model_files(request):
1001
961
  shareId = request.match_info["shareId"]
1002
962
  if not is_string_valid(shareId):
1003
- return ErrResponse("INVALID_SHARE_ID")
963
+ return ErrResponse(errnos.INVALID_SHARE_ID)
1004
964
  payload = {}
1005
965
  query_params = ["type", "name", "ext_name"]
1006
966
  for param in query_params:
@@ -1,11 +1,15 @@
1
1
  import base64
2
2
  import io
3
3
  import json
4
+ import logging
4
5
  import re
5
6
 
6
7
  import numpy as np
7
8
  import torch
8
- from comfy_api_nodes.apinode_utils import tensor_to_base64_string
9
+ from comfy_api_nodes.apinode_utils import (
10
+ bytesio_to_image_tensor,
11
+ tensor_to_base64_string,
12
+ )
9
13
  from PIL import Image, ImageOps
10
14
 
11
15
  from bizyengine.core import BizyAirBaseNode, pop_api_key_and_prompt_id
@@ -58,6 +62,94 @@ def base64_to_image(base64_string):
58
62
  return image
59
63
 
60
64
 
65
+ def get_parts_from_response(
66
+ response: dict,
67
+ ):
68
+ return response["candidates"][0]["content"]["parts"]
69
+
70
+
71
+ def get_parts_by_type(response: dict, part_type: str):
72
+ parts = []
73
+ for part in get_parts_from_response(response):
74
+ if part_type == "text" and part.get("text", None):
75
+ parts.append(part)
76
+ elif (
77
+ part.get("inlineData", None) and part["inlineData"]["mimeType"] == part_type
78
+ ):
79
+ parts.append(part)
80
+ # Skip parts that don't match the requested type
81
+ return parts
82
+
83
+
84
+ def get_text_from_response(response: dict) -> str:
85
+ parts = get_parts_by_type(response, "text")
86
+ logging.debug(f"Text parts: {parts}")
87
+ return "\n".join([part["text"] for part in parts])
88
+
89
+
90
+ def get_image_from_response(response: dict) -> torch.Tensor:
91
+ image_tensors: list[torch.Tensor] = []
92
+ parts = get_parts_by_type(response, "image/png")
93
+ for part in parts:
94
+ b64_data = part["inlineData"]["data"]
95
+ if b64_data:
96
+ image_data = base64.b64decode(b64_data)
97
+ returned_image = bytesio_to_image_tensor(io.BytesIO(image_data))
98
+ image_tensors.append(returned_image)
99
+ if len(image_tensors) == 0:
100
+ return torch.zeros((1, 1024, 1024, 4))
101
+ return torch.cat(image_tensors, dim=0)
102
+
103
+
104
+ # FROM: https://github.com/ShmuelRonen/ComfyUI-NanoBanano/blob/9eeb8f2411fd0ff08791bdf5e24eec347456c8b8/nano_banano.py#L191
105
+ def build_prompt_for_operation(
106
+ prompt,
107
+ operation,
108
+ has_references=False,
109
+ aspect_ratio="1:1",
110
+ character_consistency=True,
111
+ ):
112
+ """Build optimized prompt based on operation type"""
113
+
114
+ aspect_instructions = {
115
+ "1:1": "square format",
116
+ "16:9": "widescreen landscape format",
117
+ "9:16": "portrait format",
118
+ "4:3": "standard landscape format",
119
+ "3:4": "standard portrait format",
120
+ }
121
+
122
+ base_quality = "Generate a high-quality, photorealistic image"
123
+ format_instruction = f"in {aspect_instructions.get(aspect_ratio, 'square format')}"
124
+
125
+ if operation == "generate":
126
+ if has_references:
127
+ final_prompt = f"{base_quality} inspired by the style and elements of the reference images. {prompt}. {format_instruction}."
128
+ else:
129
+ final_prompt = f"{base_quality} of: {prompt}. {format_instruction}."
130
+
131
+ elif operation == "edit":
132
+ if not has_references:
133
+ return "Error: Edit operation requires reference images"
134
+ # No aspect ratio for edit - preserve original image dimensions
135
+ final_prompt = f"Edit the provided reference image(s). {prompt}. Maintain the original composition and quality while making the requested changes."
136
+
137
+ elif operation == "style_transfer":
138
+ if not has_references:
139
+ return "Error: Style transfer requires reference images"
140
+ final_prompt = f"Apply the style from the reference images to create: {prompt}. Blend the stylistic elements naturally. {format_instruction}."
141
+
142
+ elif operation == "object_insertion":
143
+ if not has_references:
144
+ return "Error: Object insertion requires reference images"
145
+ final_prompt = f"Insert or blend the following into the reference image(s): {prompt}. Ensure natural lighting, shadows, and perspective. {format_instruction}."
146
+
147
+ if character_consistency and has_references:
148
+ final_prompt += " Maintain character consistency and visual identity from the reference images."
149
+
150
+ return final_prompt
151
+
152
+
61
153
  class NanoBanana(BizyAirBaseNode):
62
154
  def __init__(self):
63
155
  pass
@@ -73,6 +165,13 @@ class NanoBanana(BizyAirBaseNode):
73
165
  "default": "",
74
166
  },
75
167
  ),
168
+ "operation": (
169
+ ["generate", "edit", "style_transfer", "object_insertion"],
170
+ {
171
+ "default": "generate",
172
+ "tooltip": "Choose the type of image operation",
173
+ },
174
+ ),
76
175
  "temperature": (
77
176
  "FLOAT",
78
177
  {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05},
@@ -90,73 +189,123 @@ class NanoBanana(BizyAirBaseNode):
90
189
  "image3": ("IMAGE",),
91
190
  "image4": ("IMAGE",),
92
191
  "image5": ("IMAGE",),
192
+ "quality": (
193
+ ["standard", "high"],
194
+ {"default": "high", "tooltip": "Image generation quality"},
195
+ ),
196
+ "aspect_ratio": (
197
+ ["1:1", "16:9", "9:16", "4:3", "3:4"],
198
+ {"default": "1:1", "tooltip": "Output image aspect ratio"},
199
+ ),
200
+ "character_consistency": (
201
+ "BOOLEAN",
202
+ {
203
+ "default": True,
204
+ "tooltip": "Maintain character consistency across edits",
205
+ },
206
+ ),
93
207
  },
94
208
  }
95
209
 
96
- RETURN_TYPES = ("IMAGE",)
210
+ RETURN_TYPES = ("IMAGE", "STRING")
97
211
  FUNCTION = "execute"
98
212
  OUTPUT_NODE = False
99
213
  CATEGORY = "☁️BizyAir/External APIs/Gemini"
100
214
 
101
- def execute(self, prompt, temperature, top_p, seed, max_tokens, **kwargs):
102
- url = f"{BIZYAIR_SERVER_ADDRESS}/proxy_inference/VertexAI/gemini-2.5-flash-image-preview"
103
- extra_data = pop_api_key_and_prompt_id(kwargs)
104
- parts = [{"text": prompt}]
105
- for _, img in enumerate(
106
- [
107
- kwargs.get("image", None),
108
- kwargs.get("image2", None),
109
- kwargs.get("image3", None),
110
- kwargs.get("image4", None),
111
- kwargs.get("image5", None),
112
- ],
113
- 1,
114
- ):
115
- if img is not None:
116
- parts.append(
117
- {
118
- "inline_data": {
119
- "mime_type": "image/png",
120
- "data": tensor_to_base64_string(img),
215
+ def execute(
216
+ self,
217
+ prompt,
218
+ operation,
219
+ temperature,
220
+ top_p,
221
+ seed,
222
+ max_tokens,
223
+ quality=None,
224
+ aspect_ratio=None,
225
+ character_consistency=None,
226
+ **kwargs,
227
+ ):
228
+ try:
229
+ url = f"{BIZYAIR_SERVER_ADDRESS}/proxy_inference/VertexAI/gemini-2.5-flash-image-preview"
230
+ extra_data = pop_api_key_and_prompt_id(kwargs)
231
+
232
+ parts = []
233
+ for _, img in enumerate(
234
+ [
235
+ kwargs.get("image", None),
236
+ kwargs.get("image2", None),
237
+ kwargs.get("image3", None),
238
+ kwargs.get("image4", None),
239
+ kwargs.get("image5", None),
240
+ ],
241
+ 1,
242
+ ):
243
+ if img is not None:
244
+ parts.append(
245
+ {
246
+ "inline_data": {
247
+ "mime_type": "image/png",
248
+ "data": tensor_to_base64_string(img),
249
+ }
121
250
  }
122
- }
251
+ )
252
+
253
+ prompt = build_prompt_for_operation(
254
+ prompt,
255
+ operation,
256
+ has_references=len(parts) > 0,
257
+ aspect_ratio=aspect_ratio,
258
+ character_consistency=character_consistency,
259
+ )
260
+ if quality == "high":
261
+ prompt += " Use the highest quality settings available."
262
+ parts.append({"text": prompt})
263
+
264
+ data = {
265
+ "contents": {
266
+ "parts": parts,
267
+ "role": "user",
268
+ },
269
+ "generationConfig": {
270
+ "seed": seed,
271
+ "responseModalities": ["TEXT", "IMAGE"],
272
+ "temperature": temperature,
273
+ "topP": top_p,
274
+ "maxOutputTokens": max_tokens,
275
+ },
276
+ }
277
+ json_payload = json.dumps(data).encode("utf-8")
278
+ headers = client.headers(api_key=extra_data["api_key"])
279
+ headers["X-BIZYAIR-PROMPT-ID"] = extra_data[
280
+ "prompt_id"
281
+ ] # 额外参数vertexai会拒绝,所以用请求头传
282
+ resp = client.send_request(
283
+ url=url,
284
+ data=json_payload,
285
+ headers=headers,
286
+ )
287
+ # 解析潜在错误
288
+ prompt_feedback = resp.get("promptFeedback", None)
289
+ if prompt_feedback:
290
+ logging.error(f"Response: {resp}")
291
+ raise ValueError(f"Prompt blocked: {prompt_feedback}")
292
+ if len(resp["candidates"]) == 0:
293
+ logging.error(f"Response: {resp}")
294
+ raise ValueError("No candidates found in response")
295
+ if resp["candidates"][0]["finishReason"] != "STOP":
296
+ logging.error(f"Response: {resp}")
297
+ raise ValueError(
298
+ f"Erroneous finish reason: {resp['candidates'][0]['finishReason']}"
123
299
  )
124
- data = {
125
- "contents": {
126
- "parts": parts,
127
- "role": "user",
128
- },
129
- "generationConfig": {
130
- "seed": seed,
131
- "responseModalities": ["TEXT", "IMAGE"],
132
- "temperature": temperature,
133
- "topP": top_p,
134
- "maxOutputTokens": max_tokens,
135
- },
136
- }
137
- json_payload = json.dumps(data).encode("utf-8")
138
- headers = client.headers(api_key=extra_data["api_key"])
139
- headers["X-BIZYAIR-PROMPT-ID"] = extra_data[
140
- "prompt_id"
141
- ] # 额外参数vertexai会拒绝,所以用请求头传
142
- resp = client.send_request(
143
- url=url,
144
- data=json_payload,
145
- headers=headers,
146
- )
147
- # 解析base64图片
148
- b64_data = None
149
- for part in resp["candidates"][0]["content"]["parts"]:
150
- if part.get("inlineData", None):
151
- b64_data = part["inlineData"]["data"]
152
- break
153
- if b64_data:
154
- i = base64_to_image(b64_data)
155
- # 下面代码参考LoadImage
156
- i = ImageOps.exif_transpose(i)
157
- image = i.convert("RGB")
158
- image = np.array(image).astype(np.float32) / 255.0
159
- image = torch.from_numpy(image)[None,]
160
- return (image,)
161
- else:
162
- raise ValueError("No image found in response")
300
+
301
+ # 解析文本
302
+ text = get_text_from_response(resp)
303
+
304
+ # 解析base64图片
305
+ image = get_image_from_response(resp)
306
+
307
+ return (image, text)
308
+
309
+ except Exception as e:
310
+ logging.error(f"Gemini API error: {e}")
311
+ raise e