Spaces:
Sleeping
Sleeping
| import os # 匯入 os 模組以處理環境變數和檔案路徑 | |
| import io # 匯入 io 模組以處理二進位資料流 | |
| import PIL.Image # 匯入 PIL 的 Image 模組以處理圖片 | |
| import requests # 匯入 requests 模組以進行 HTTP 請求 | |
| from dotenv import load_dotenv # 匯入 dotenv 以載入 .env 環境變數檔案 | |
| import json # 匯入 json 庫用於序列化 | |
| from urllib.parse import urlparse | |
| # LangChain 相關匯入 | |
| from langchain.agents import create_agent | |
| from langchain.tools import tool | |
| from langchain.messages import AIMessage, HumanMessage, ToolMessage | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| from google import genai # 匯入 Google GenAI 函式庫 | |
| from google.genai import types # 匯入 GenAI 的類型定義 | |
| from services.deblur import deblur_image_tiled # 從本地服務匯入去模糊函式 | |
| # ========================== | |
| # 環境設定與工具函式 | |
| # ========================== | |
| load_dotenv() | |
| # 設置 Google AI API 金鑰 (從環境變數讀取) | |
| google_api = os.environ["GOOGLE_API_KEY"] | |
| # 初始化 Google GenAI 客戶端 | |
| genai_client = genai.Client(api_key=google_api) | |
| # ========================== | |
| # some 工具定義 | |
| # ========================== | |
| def load_image(file_url: str) -> PIL.Image.Image: | |
| """ | |
| 支援本地檔案或 HTTP(S) URL 讀取圖片 | |
| """ | |
| parsed = urlparse(file_url) | |
| if parsed.scheme in ("http", "https"): | |
| # 網路圖片 | |
| try: | |
| print(f"Agent 正在下載圖片: {file_url}") | |
| resp = requests.get(file_url, timeout=15) | |
| resp.raise_for_status() | |
| img = PIL.Image.open(io.BytesIO(resp.content)).convert("RGB") | |
| return img | |
| except Exception as e: | |
| raise ValueError(f"下載圖片失敗: {e}") | |
| else: | |
| # 本地檔案 | |
| if not os.path.exists(file_url): | |
| raise ValueError("圖片路徑無效,無法進行分析。") | |
| try: | |
| img = PIL.Image.open(file_url).convert("RGB") | |
| return img | |
| except Exception as e: | |
| raise ValueError(f"開啟本地圖片失敗: {e}") | |
| # ========================== | |
| # LangChain 工具定義 | |
| # ========================== | |
| def generate_and_upload_image(prompt: str) -> str: | |
| """ | |
| 這個工具可以根據文字提示生成圖片,並將其上傳到伺服器。 | |
| Args: | |
| prompt: 用於生成圖片的文字提示。 | |
| Returns: | |
| 一個 JSON 格式的字串,包含圖片 URL 和描述,或錯誤訊息。 | |
| """ | |
| try: | |
| # 呼叫 Google GenAI 模型生成內容 | |
| response = genai_client.models.generate_content( | |
| model="gemini-2.0-flash-preview-image-generation",#"gemini-2.5-flash-image", # 指定圖片生成模型 | |
| contents=prompt, # 傳入文字提示 | |
| config=types.GenerateContentConfig(response_modalities=['Text', 'Image']) # 指定回應類型 | |
| ) | |
| image_binary = None | |
| # 遍歷回應的 parts,找到圖片的二進位數據 | |
| for part in response.candidates[0].content.parts: | |
| if part.inline_data is not None: | |
| image_binary = part.inline_data.data | |
| break | |
| if image_binary: | |
| # 使用 PIL 將二進位數據轉換為圖片物件 | |
| image = PIL.Image.open(io.BytesIO(image_binary)) | |
| # 隨機生成一個檔案名以避免衝突,並儲存在 static 資料夾 | |
| file_name = f"static/{os.urandom(16).hex()}.jpg" | |
| image.save(file_name, format="JPEG") | |
| # 從環境變數獲取 Hugging Face Space 的 URL (或你的伺服器 URL) | |
| # 並組合完整的圖片 URL | |
| image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space | |
| # 統一回傳 JSON 成功格式 | |
| return json.dumps({ | |
| "image_url": image_url, | |
| "text_result": f"圖片已成功生成並上傳。這是根據提示 '{prompt[:50]}...' 生成的圖片。" | |
| }) | |
| # 處理圖片生成失敗但 API 未報錯的情況 | |
| return json.dumps({ | |
| "error": "圖片生成失敗。API 回應中未包含圖片數據,請嘗試修改提示詞。" | |
| }) | |
| except Exception as e: | |
| error_msg = f"圖片生成與上傳失敗: {e}" | |
| return json.dumps({ | |
| "error": error_msg | |
| }) | |
| def analyze_image_with_text(image_path: str, user_text: str) -> str: | |
| """ | |
| 這個工具可以根據圖片和文字提示來回答問題 (多模態分析)。 | |
| Args: | |
| image_path: 圖片在本地端儲存的路徑。 | |
| user_text: 針對圖片提出的文字問題。 | |
| Returns: | |
| 一個 JSON 格式的字串,包含模型回應或錯誤訊息。 | |
| """ | |
| try: | |
| # 檢查圖片路徑是否存在 | |
| #if not os.path.exists(image_path): | |
| # return json.dumps({ | |
| # "error": "圖片路徑無效,無法進行分析。" | |
| # }) | |
| # 使用 PIL 開啟圖片 | |
| #img_user = PIL.Image.open(image_path) | |
| img_user = load_image(image_path) | |
| # 呼叫 Google GenAI 模型 (gemini-2.5-flash) 進行多模態分析 | |
| response = genai_client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=[img_user, user_text] # 同時傳入圖片物件和文字 | |
| ) | |
| if (response.text != None): | |
| out = response.text | |
| else: | |
| out = "Gemini沒答案!請換個說法!" | |
| # 統一回傳 JSON 成功格式 (只有文字結果) | |
| return json.dumps({ | |
| "text_result": out | |
| }) | |
| except Exception as e: | |
| # 處理錯誤 | |
| out = f"Gemini執行出錯: {e}" | |
| # 統一回傳 JSON 錯誤格式 | |
| return json.dumps({ | |
| "error": out | |
| }) | |
| def deblur_image_from_url( | |
| file_url: str, | |
| user_text: str | |
| ) -> str: | |
| """ | |
| 這個工具可以從提供的圖片來源載入影像(支援 HTTP/HTTPS 網址與本地檔案路徑), | |
| 並使用分塊處理(Tiled Processing)進行去模糊(deblur)。處理完成後, | |
| 會將結果儲存於伺服器的 static/ 目錄,並回傳去模糊後圖片的 **絕對 URL 路徑** | |
| 以及根據 user_text 生成的額外文字結果。 | |
| Args: | |
| file_url: | |
| 圖片來源,可為: | |
| - HTTP/HTTPS 網路圖片網址(例如:https://example.com/img.png) | |
| - 本地檔案路徑(例如:/tmp/xxx.png) | |
| user_text: | |
| 使用者針對圖片提出的處理需求或描述文字。 | |
| Returns: | |
| JSON 格式的字串,包含: | |
| - "image_url": 去模糊後圖片的絕對 URL 路徑 | |
| - "text_result": 根據 user_text 產生的額外文字說明 | |
| """ | |
| try: | |
| tile_size = 512 | |
| overlap = 32 | |
| # 內容轉換為 PIL Image | |
| img_input = load_image(file_url) | |
| # 2. 執行去模糊處理 | |
| img_deblurred = deblur_image_tiled( | |
| img_input, | |
| tile_size=tile_size, | |
| overlap=overlap | |
| ) | |
| # 建立一個唯一的檔案名 | |
| ext = img_input.format if img_input.format else 'JPEG' | |
| file_name = f"static/{os.urandom(16).hex()}.jpg" | |
| img_deblurred.save(file_name, format=ext) | |
| # 4. 建構絕對 URL 路徑 (供客戶端存取) | |
| # 這裡假設 BASE_URL 已經設定好,並與 FastAPI 的 static mount 匹配 | |
| image_url = os.path.join(os.getenv("HF_SPACE"), file_name) # Embed this Space | |
| analysis_result = f"圖片已成功去模糊。用戶請求的描述為:'{user_text}'。模型已根據此要求調整參數進行處理。" | |
| # 5. 返回 JSON 字串 | |
| return json.dumps({ | |
| "image_url": image_url, | |
| "text_result": analysis_result | |
| }) | |
| except requests.exceptions.RequestException as e: | |
| return json.dumps({ | |
| "error": f"下載圖片失敗或 URL 無效: {e}" | |
| }) | |
| except Exception as e: | |
| return json.dumps({ | |
| "error": f"圖片處理失敗。錯誤訊息: {e}" | |
| }) | |
| # ------------------------------ | |
| # 1️⃣ 意圖分類工具 | |
| # ------------------------------ | |
| def classify_intent(user_input: str) -> str: | |
| """ | |
| 判斷使用者輸入意圖: | |
| - "deblur" -> 去模糊 / 修復 / 影像清晰化 | |
| - "qa" -> 一般問題或圖片分析 | |
| """ | |
| deblur_keywords = [ | |
| # 中文 | |
| "去模糊", "清晰", "清楚", "修復", "模糊", "變清楚", "提高清晰度", | |
| "還原", "去噪", "降噪", "去霧", "增強", "超解析", "超分辨", | |
| # 英文/拼音 | |
| "deblur", "restore", "restoration", "denoise", "noise", "enhance", | |
| "enhancement", "super resolution", "sr", "defog", "dehaze", | |
| "sharpen", "blurry", "blurred", "fix blur" | |
| ] | |
| text = user_input.lower() | |
| if any(k in text for k in deblur_keywords): | |
| return "deblur" | |
| else: | |
| return "qa" | |
| # ========================== | |
| # LangChain 代理人設定 | |
| # ========================== | |
| # 結合所有定義的工具 | |
| tools = [ | |
| classify_intent, # 意圖分類 | |
| generate_and_upload_image, # 生成圖片 | |
| analyze_image_with_text, # 分析圖片 | |
| deblur_image_from_url # 去模糊圖片 | |
| ] | |
| # 建立 LLM 模型實例 (使用 LangChain 的 ChatGoogleGenerativeAI) | |
| llm = ChatGoogleGenerativeAI( | |
| google_api_key=google_api, | |
| model="gemini-2.5-flash", | |
| temperature=0.2 | |
| ) | |
| # ✅ 建立 Prompt (新版語法) | |
| sys_prompt = """ | |
| 你是一個圖像生成、去模糊與圖片問答助理,請依流程使用工具。 | |
| 【可用工具】 | |
| 1. classify_intent(user_input) → 回傳 "deblur" 或 "qa" | |
| 2. deblur_image_from_url(file_url, user_text) → 圖片去模糊/修復 | |
| 3. analyze_image_with_text(image_path, user_text) → 圖片理解與問答 | |
| 4. generate_and_upload_image(prompt) → 生成圖像 | |
| 【流程】 | |
| - 先呼叫 classify_intent 判斷意圖 | |
| - 若為 "deblur" → 呼叫 deblur_image_from_url | |
| - 若為 "qa": | |
| - 若與圖片內容有關 → analyze_image_with_text | |
| - 若需生成新圖 → generate_and_upload_image | |
| 【回覆規則】 | |
| - 若工具成功輸出圖片 → 回覆必須包含: | |
| - 圖片完整 URL | |
| - 簡要說明(如:已完成去模糊/生成圖片) | |
| - 若工具失敗 → 用自然語言說明錯誤,不輸出技術錯誤碼或 traceback | |
| 【判斷原則】 | |
| - 有「去模糊、清晰、修復」等語意 → deblur | |
| - 有提問或描述圖片 → qa | |
| - 有「生成、畫、幫我做一張圖」→ generate_and_upload_image | |
| 請嚴格遵循流程,不要跳步。 | |
| """ | |
| # --- 4. 建立代理人與執行器 --- | |
| # 建立工具調用代理人 (Tool Calling Agent) | |
| agent = create_agent( | |
| model=llm, | |
| tools=tools, | |
| system_prompt=sys_prompt | |
| ) | |
| def format_agent_result(result): | |
| output = { | |
| "user": None, | |
| "tool_call": None, | |
| "tool_result": None, | |
| "final_response": None | |
| } | |
| for msg in result["messages"]: | |
| if isinstance(msg, HumanMessage): | |
| output["user"] = msg.content | |
| elif isinstance(msg, AIMessage) and msg.additional_kwargs.get("function_call"): | |
| fn = msg.additional_kwargs["function_call"] | |
| output["tool_call"] = { | |
| "name": fn["name"], | |
| "arguments": fn["arguments"] | |
| } | |
| elif isinstance(msg, ToolMessage): | |
| try: | |
| output["tool_result"] = json.loads(msg.content) | |
| except Exception: | |
| output["tool_result"] = msg.content # 若非 JSON | |
| elif isinstance(msg, AIMessage) and not msg.additional_kwargs.get("function_call"): | |
| # 如果是 list of dict(如 [{'type': 'text','text':...}]) | |
| if isinstance(msg.content, list): | |
| # 只取第一個 text | |
| if len(msg.content) > 0 and "text" in msg.content[0]: | |
| output["final_response"] = msg.content[0]["text"] | |
| else: | |
| output["final_response"] = str(msg.content) | |
| else: | |
| output["final_response"] = msg.content | |
| return output | |
| def run_agent(user_input: str): | |
| """呼叫此函式來執行 Agent""" | |
| print(f"UserInput:{user_input}") | |
| result = agent.invoke({ | |
| "messages": [{"role": "user", "content": user_input }] | |
| }) | |
| #print(f"result:{result}") | |
| output_format = format_agent_result( result ) | |
| print(f"output_format:{output_format}") | |
| return { "output": output_format } |