File size: 8,363 Bytes
dd70e33
319d52a
2f75e66
5e0a683
319d52a
5e0a683
dd70e33
6d820cb
5e0a683
 
6f25e32
57fdc5b
6f25e32
cda20d5
 
 
319d52a
5e0a683
dd70e33
cda20d5
319d52a
 
997fa91
 
 
 
319d52a
997fa91
dd70e33
 
cda20d5
 
 
 
 
2f75e66
5e0a683
 
 
 
 
 
 
 
2f75e66
cda20d5
 
 
57fdc5b
36e825c
cda20d5
36e825c
dd70e33
 
 
 
 
 
 
 
 
 
 
 
 
 
6d820cb
dd70e33
 
 
 
 
cda20d5
6f25e32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cda20d5
5e0a683
 
 
 
 
 
 
 
 
 
 
cda20d5
32728eb
319d52a
 
 
 
 
 
 
 
 
 
 
 
 
5e0a683
cda20d5
319d52a
 
 
 
 
4cfd693
319d52a
 
 
 
 
 
 
 
 
 
 
 
cda20d5
 
c7d0a9b
 
 
 
5e0a683
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
from fastapi import FastAPI, Request, Response, Form, Header, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware  # 匯入 FastAPI 的 CORS 中介軟體
import requests
from typing import Annotated # 推薦用於 Pydantic v2+
from linebot.exceptions import InvalidSignatureError  # 匯入 Line 簽章無效的例外
from services.linebot import line_handle

from services.deblur import deblur_image_tiled
from services.agents import run_agent

import json
from PIL import Image
import io
import os
from datetime import datetime
import uvicorn
from dotenv import load_dotenv # 匯入 dotenv 以載入 .env 環境變數檔案

STATIC_DIR = "static"

os.environ["TORCH_HOME"] = "./.cache"
os.environ["HF_HOME"] = "./.cache"
os.environ["TRANSFORMERS_CACHE"] = "./.cache"
os.makedirs("./.cache", exist_ok=True)
os.makedirs(STATIC_DIR, exist_ok=True)

load_dotenv()

# =====================
# 初始化 FastAPI
# =====================
app = FastAPI(title="DeblurGANv2 API")

app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# 設定 CORS (跨來源資源共用)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允許所有來源
    allow_credentials=True,  # 允許憑證
    allow_methods=["*"],  # 允許所有 HTTP 方法
    allow_headers=["*"],  # 允許所有 HTTP 標頭
)

# =====================
# API 路由
# =====================
@app.get("/")
def root():
    return {"message": "DeblurGANv2 API ready!"}

@app.post("/webhook")
async def webhook(

    request: Request,

    background_tasks: BackgroundTasks,

    x_line_signature=Header(None), # 從標頭獲取 Line 的簽章

):
    """

    Line Bot 的 Webhook 路由。

    """
    # 獲取請求的原始內容 (body)
    body = await request.body()
    try:
        # 使用背景任務來處理 Webhook,這樣可以立即回傳 200 OK 給 Line 伺服器
        background_tasks.add_task(
            line_handle, body.decode("utf-8"), x_line_signature
        )
    except InvalidSignatureError:
        # 如果簽章無效,拋出 400 錯誤
        raise HTTPException(status_code=400, detail="Invalid signature")
    return "ok"

@app.post("/ai_agent")
async def ai_agent(

    input_text: Annotated[str, Form(description="輸入文字")],

    # 將您的 form-data 欄位定義為函數參數,並使用 Form()

    file_name: Annotated[str, Form(description="檔案名稱")] = None,

    file_format: Annotated[str, Form(description="檔案格式")] = None,

    file_url: Annotated[str, Form(description="檔案下載網址")] = None, # 可選參數,有預設值

    # 對於需要轉換類型 (例如 int) 的欄位,直接在類型提示中指定

    file_width: Annotated[int, Form(description="檔案寬度")] = 0,

    file_height: Annotated[int, Form(description="檔案高度")] = 0,

    file_created_at: Annotated[str, Form(description="檔案建立時間")] = None,

):
    out_str = ""
    image_url = ""
    text_result = ""

    try:

        agent_input = input_text
        if file_url:
            agent_input = f"請根據這張圖片回答問題。圖片的路徑是 {file_url},我的問題是:{input_text}"

        # 運行 LangChain 代理人
        response = run_agent(agent_input)
        # Agent 的輸出是 JSON 字串 (無論成功或失敗)
        out_str = response["output"]
        #print(f"out_str:{out_str}")
        tool_result = out_str.get("tool_result", {})
        if tool_result is None:
            tool_result = {}
        #print(f"tool_result:{tool_result}")
        final_response = out_str.get("final_response") 
        #print(f"final_response:{final_response}")

        if "error" in tool_result:
            # 3A. 處理錯誤情況 (例如:圖片下載失敗、API 錯誤)
            error_msg = tool_result["error"]
            reply_text = f"🚫 圖片工具執行失敗:\n{error_msg}"
            return JSONResponse(
                {"status": "error", "message": reply_text},
                status_code=500
            )

        elif "image_url" in tool_result:
            # 3B. 處理成功情況 (帶有圖片 URL,例如:圖片生成或去模糊工具)
            image_url = tool_result["image_url"]
            text_result = tool_result.get("text_result", "圖片處理完成。")
            
            # Line 要求圖片 URL 必須是 HTTPS
            # 由於 HF_SPACE 預設是 https,這裡做一個保險轉換
            if image_url.startswith("http://"):
                image_url = image_url.replace("http://", "https://")
                
        elif "text_result" in tool_result:
                # 3C. 處理純文字結果 (例如:多模態分析工具)
            text_result = tool_result["text_result"]
            
        elif final_response:
            text_result = final_response

        else:
                # 3D. 處理意外的 JSON 結構
            text_result = "代理人回覆格式無法識別,請聯繫管理員。"
            
        return JSONResponse(
            {
                "status": "success", 
                "message": text_result,
                "image_url" : image_url 
            }, status_code=200
        )

    except json.JSONDecodeError:
            # 4. 處理 Agent 返回了無法解析的純文字 (非 JSON)
            # 這通常發生在 Agent 決定不使用工具,直接回覆純文字,或者推理過程出錯。
            return JSONResponse(
                {"status": "error", "message": out_str }, status_code=500
            )

    except Exception as e:
            # 處理代理人執行時的錯誤
            print(f"代理人執行出錯: {e}")
            out = f"代理人執行出錯!錯誤訊息:{e}"
            import traceback
            traceback.print_exc()
            return JSONResponse(
                {"status": "error", "message": out }, status_code=500
            )

@app.post("/predict")
async def predict(

    request: Request,

    # 將您的 form-data 欄位定義為函數參數,並使用 Form()

    file_name: Annotated[str, Form(description="檔案名稱")],

    file_format: Annotated[str, Form(description="檔案格式")],

    file_url: Annotated[str, Form(description="檔案下載網址")], # 可選參數,有預設值

    # 對於需要轉換類型 (例如 int) 的欄位,直接在類型提示中指定

    file_width: Annotated[int, Form(description="檔案寬度")] = 0,

    file_height: Annotated[int, Form(description="檔案高度")] = 0,

    file_created_at: Annotated[str, Form(description="檔案建立時間")] = None,

):
    try:
        print("### start /predict !!")

        if not file_url:
            return JSONResponse(
                {"status": "error", "message": "file_url is required"},
                status_code=400
            )

        # 2️⃣ 從 URL 下載圖片
        resp = requests.get(file_url)
        resp.raise_for_status()
        img = Image.open(io.BytesIO(resp.content)).convert("RGB")

        # 3️⃣ 去模糊
        result = deblur_image_tiled(img)

        # 4️⃣ 產生檔名
        base_name = f"{file_name}_{file_width}_{file_height}_{file_created_at}.jpg"
        file_path = os.path.join(STATIC_DIR, base_name)

        # 5️⃣ 儲存到 static
        result.save(file_path, format="JPEG")

        # 6️⃣ 回傳前端可取用的 URL
        file_url_return = str(request.base_url) + f"static/{base_name}"
        return {
            "status": "success", 
            "file_url": file_url_return,
            "file_name": base_name,
            "file_format": "jpg",
            "file_width": result.width,
            "file_height": result.height,
            "file_created_at": datetime.now().strftime("%Y%m%d%H%M%S")
        }

    except Exception as e:
        import traceback
        traceback.print_exc()
        return JSONResponse(
            {"status": "error", "message": str(e)}, status_code=500
        )
    

if __name__ == "__main__":
    uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)