hoangthiencm commited on
Commit
4fbda48
·
verified ·
1 Parent(s): 848a794

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +283 -57
server.py CHANGED
@@ -1,88 +1,147 @@
 
 
 
 
 
1
  import os
2
- import random
3
- import logging
 
 
 
 
 
 
 
4
  import base64
5
- from io import BytesIO
6
- from typing import Optional, List
7
 
8
- from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Body
 
9
  from fastapi.middleware.cors import CORSMiddleware
10
- from pydantic import BaseModel
11
- import google.generativeai as genai
12
- from dotenv import load_dotenv
 
 
13
  from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- # --- LOAD BIẾN MÔI TRƯỜNG ---
16
- load_dotenv()
 
 
 
17
 
18
- # --- CẤU HÌNH SERVER ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  app = FastAPI(title="HT MATH UNIFIED SERVER")
20
 
21
- # Cấu hình CORS (Cho phép cả Web và Desktop App gọi vào)
22
  app.add_middleware(
23
  CORSMiddleware,
24
- allow_origins=["*"], # Cho phép tất cả các nguồn (Web, App Desktop)
25
  allow_credentials=True,
26
  allow_methods=["*"],
27
  allow_headers=["*"],
28
  )
29
 
30
- # --- PHẦN 1: CẤU HÌNH CHO DESKTOP APP (Key Rotation & Models) ---
 
 
 
 
31
 
32
- # 1. Lấy danh sách API KEYS từ biến môi trường
33
- # Format trên HuggingFace: Key1,Key2,Key3
34
- api_keys_env = os.getenv("GEMINI_API_KEYS", "")
35
- DESKTOP_API_KEYS = [k.strip() for k in api_keys_env.split(",") if k.strip()]
 
 
 
36
 
37
- # 2. Lấy danh sách MODELS
38
- models_env = os.getenv("GEMINI_MODELS", "gemini-2.0-flash-exp,gemini-1.5-pro,gemini-1.5-flash")
39
- DESKTOP_MODELS = [m.strip() for m in models_env.split(",") if m.strip()]
40
 
41
- # Model dữ liệu nhận từ Desktop App
42
  class DesktopGenerateRequest(BaseModel):
43
  prompt: str
44
  model: Optional[str] = "gemini-1.5-flash"
45
  image: Optional[str] = None # Base64 string
46
 
47
- @app.get("/")
48
- async def root():
49
- return {
50
- "status": "online",
51
- "server": "HT MATH UNIFIED SERVER",
52
- "desktop_keys_loaded": len(DESKTOP_API_KEYS),
53
- "web_support": "Active"
54
- }
55
-
56
- # --- API 1: Lấy danh sách Model (Cho Desktop App) ---
57
  @app.get("/api/models")
58
  async def get_models_desktop():
59
- return {"models": DESKTOP_MODELS}
 
 
 
60
 
61
- # --- API 2: Xử lý AI (Cho Desktop App - Có xoay vòng Key) ---
62
  @app.post("/api/generate")
63
  async def generate_content_desktop(req: DesktopGenerateRequest):
64
- if not DESKTOP_API_KEYS:
65
- raise HTTPException(status_code=500, detail="Server chưa cấu hình GEMINI_API_KEYS")
66
-
 
67
  try:
68
- # 1. Chọn ngẫu nhiên 1 Key (Load Balancing)
69
- current_key = random.choice(DESKTOP_API_KEYS)
70
- genai.configure(api_key=current_key)
71
 
72
  # 2. Chọn Model
73
- # Nếu model client gửi lên không có trong danh sách hỗ trợ, dùng model mặc định
74
- model_name = req.model if req.model in DESKTOP_MODELS else DESKTOP_MODELS[0]
 
 
 
75
  model = genai.GenerativeModel(model_name)
76
 
77
  # 3. Chuẩn bị nội dung gửi đi
78
  content_parts = [req.prompt]
79
 
80
- # 4. Xử lý ảnh (nếu )
81
  if req.image:
82
  try:
83
  # Desktop App gửi ảnh dạng Base64 string
 
 
 
 
84
  image_bytes = base64.b64decode(req.image)
85
- image = Image.open(BytesIO(image_bytes))
86
  content_parts.append(image)
87
  except Exception as e:
88
  raise HTTPException(status_code=400, detail=f"Lỗi xử lý ảnh base64: {str(e)}")
@@ -96,27 +155,194 @@ async def generate_content_desktop(req: DesktopGenerateRequest):
96
  raise HTTPException(status_code=500, detail="Gemini không trả về nội dung text.")
97
 
98
  except Exception as e:
99
- print(f"Lỗi Desktop API: {e}")
100
  raise HTTPException(status_code=500, detail=str(e))
101
 
102
 
103
  # ==============================================================================
104
- # --- PHẦN 2: KHU VỰC CỦA WEB APP (COPY CODE VÀO DƯỚI ĐÂY) ---
105
  # ==============================================================================
106
 
107
- # Bạn hãy dán các hàm xử lý của Web App vào đây.
108
- # Ví dụ: import supabase, các hàm convert_file, login, signup...
109
- # Đảm bảo giữ nguyên logic cũ của Web App.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- # dụ mẫu (Nếu bạn dùng Supabase):
112
- # from supabase import create_client, Client
113
- # url: str = os.environ.get("SUPABASE_URL")
114
- # key: str = os.environ.get("SUPABASE_KEY")
115
- # supabase: Client = create_client(url, key)
 
 
 
 
 
 
116
 
117
- # @app.post("/convert")
118
- # async def convert_file(...):
119
- # ... code của bạn ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  if __name__ == "__main__":
122
  import uvicorn
 
1
+ """
2
+ Backend API HỢP NHẤT (Web + Desktop) - HT MATH V6
3
+ Chạy trên Hugging Face Spaces (Docker Version)
4
+ """
5
+
6
  import os
7
+ import io
8
+ import time
9
+ import asyncio
10
+ import re
11
+ import tempfile
12
+ import hashlib
13
+ import secrets
14
+ import uuid
15
+ import math
16
  import base64
17
+ import random
18
+ from typing import List, Optional
19
 
20
+ # --- THƯ VIỆN CHÍNH ---
21
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Form, Request, Body
22
  from fastapi.middleware.cors import CORSMiddleware
23
+ from fastapi.responses import JSONResponse, FileResponse
24
+ from fastapi.staticfiles import StaticFiles
25
+ from pydantic import BaseModel # Thêm Pydantic cho Desktop App
26
+
27
+ # --- THƯ VIỆN XỬ LÝ ẢNH & AI ---
28
  from PIL import Image
29
+ import fitz # PyMuPDF
30
+ import google.generativeai as genai
31
+
32
+ # --- PANDOC IMPORT ---
33
+ try:
34
+ import pypandoc
35
+ print(f"INFO: Pandoc version detected: {pypandoc.get_pandoc_version()}")
36
+ except ImportError:
37
+ print("CRITICAL WARNING: pypandoc module not found.")
38
+ except OSError:
39
+ print("CRITICAL WARNING: pandoc binary not found in system path.")
40
+
41
+ # --- SUPABASE ---
42
+ try:
43
+ from supabase import create_client, Client
44
+ SUPABASE_AVAILABLE = True
45
+ except ImportError:
46
+ SUPABASE_AVAILABLE = False
47
+ Client = None
48
+ create_client = None
49
 
50
+ # ===== CẤU HÌNH =====
51
+ # Load biến môi trường
52
+ GEMINI_API_KEYS = os.getenv("GEMINI_API_KEYS", "").split(",")
53
+ # Lọc bỏ key rỗng
54
+ GEMINI_API_KEYS = [k.strip() for k in GEMINI_API_KEYS if k.strip()]
55
 
56
+ GEMINI_MODELS = os.getenv("GEMINI_MODELS", "gemini-2.5-flash,gemini-1.5-pro").split(",")
57
+ GEMINI_MODELS = [m.strip() for m in GEMINI_MODELS if m.strip()]
58
+
59
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
60
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY")
61
+
62
+ # Khởi tạo Supabase
63
+ supabase: Optional[Client] = None
64
+ if SUPABASE_AVAILABLE and SUPABASE_URL and SUPABASE_KEY:
65
+ try:
66
+ supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
67
+ print("INFO: Supabase connected successfully.")
68
+ except Exception as e:
69
+ print(f"ERROR: Failed to connect to Supabase: {e}")
70
+
71
+ # Khởi tạo FastAPI
72
  app = FastAPI(title="HT MATH UNIFIED SERVER")
73
 
74
+ # Cấu hình CORS (Cho phép cả Web và Desktop App)
75
  app.add_middleware(
76
  CORSMiddleware,
77
+ allow_origins=["*"],
78
  allow_credentials=True,
79
  allow_methods=["*"],
80
  allow_headers=["*"],
81
  )
82
 
83
+ # --- HELPER FUNCTIONS ---
84
+ def get_random_api_key():
85
+ if not GEMINI_API_KEYS:
86
+ raise HTTPException(status_code=500, detail="Server chưa cấu hình GEMINI_API_KEYS")
87
+ return secrets.choice(GEMINI_API_KEYS)
88
 
89
+ def clean_latex_formulas(text):
90
+ """Làm sạch chuẩn hóa LaTeX"""
91
+ text = re.sub(r'\\\(', '$', text)
92
+ text = re.sub(r'\\\)', '$', text)
93
+ text = re.sub(r'\\\[', '$$', text)
94
+ text = re.sub(r'\\\]', '$$', text)
95
+ return text
96
 
97
+ # ==============================================================================
98
+ # PHẦN 1: API DÀNH RIÊNG CHO DESKTOP APP (HT MATH V6 CLIENT)
99
+ # ==============================================================================
100
 
 
101
  class DesktopGenerateRequest(BaseModel):
102
  prompt: str
103
  model: Optional[str] = "gemini-1.5-flash"
104
  image: Optional[str] = None # Base64 string
105
 
 
 
 
 
 
 
 
 
 
 
106
  @app.get("/api/models")
107
  async def get_models_desktop():
108
+ """API trả về danh sách model cho Desktop App cập nhật vào ComboBox"""
109
+ # Nếu danh sách rỗng, trả về default để tránh lỗi app
110
+ models = GEMINI_MODELS if GEMINI_MODELS else ["gemini-1.5-flash"]
111
+ return {"models": models}
112
 
 
113
  @app.post("/api/generate")
114
  async def generate_content_desktop(req: DesktopGenerateRequest):
115
+ """
116
+ API xử AI cho Desktop App.
117
+ Khác với Web App (nhận Multipart), API này nhận JSON chứa Base64 image.
118
+ """
119
  try:
120
+ # 1. Chọn Key ngẫu nhiên (Load Balancing)
121
+ api_key = get_random_api_key()
122
+ genai.configure(api_key=api_key)
123
 
124
  # 2. Chọn Model
125
+ # Nếu model client gửi lên không có trong danh sách hỗ trợ, dùng model đầu tiên
126
+ model_name = req.model
127
+ if model_name not in GEMINI_MODELS and GEMINI_MODELS:
128
+ model_name = GEMINI_MODELS[0]
129
+
130
  model = genai.GenerativeModel(model_name)
131
 
132
  # 3. Chuẩn bị nội dung gửi đi
133
  content_parts = [req.prompt]
134
 
135
+ # 4. Xử lý ảnh (Base64 -> Image)
136
  if req.image:
137
  try:
138
  # Desktop App gửi ảnh dạng Base64 string
139
+ # Cần xử lý trường hợp có prefix data:image/...;base64,
140
+ if "," in req.image:
141
+ req.image = req.image.split(",")[1]
142
+
143
  image_bytes = base64.b64decode(req.image)
144
+ image = Image.open(io.BytesIO(image_bytes))
145
  content_parts.append(image)
146
  except Exception as e:
147
  raise HTTPException(status_code=400, detail=f"Lỗi xử lý ảnh base64: {str(e)}")
 
155
  raise HTTPException(status_code=500, detail="Gemini không trả về nội dung text.")
156
 
157
  except Exception as e:
158
+ print(f"Error Desktop API: {e}")
159
  raise HTTPException(status_code=500, detail=str(e))
160
 
161
 
162
  # ==============================================================================
163
+ # PHẦN 2: API DÀNH CHO WEB APP (HT MATH WEB V6)
164
  # ==============================================================================
165
 
166
+ # --- AUTHENTICATION (WEB) ---
167
+ @app.post("/api/auth/register")
168
+ async def register(request: Request):
169
+ if not supabase:
170
+ raise HTTPException(status_code=503, detail="Database service unavailable")
171
+
172
+ data = await request.json()
173
+ email = data.get("email")
174
+ password = data.get("password")
175
+ full_name = data.get("full_name")
176
+
177
+ if not email or not password:
178
+ raise HTTPException(status_code=400, detail="Vui lòng nhập Email và Mật khẩu")
179
+
180
+ try:
181
+ # 1. Đăng ký Auth user
182
+ auth_res = supabase.auth.sign_up({
183
+ "email": email,
184
+ "password": password,
185
+ "options": {"data": {"full_name": full_name}}
186
+ })
187
+
188
+ if not auth_res.user:
189
+ raise HTTPException(status_code=400, detail="Đăng ký thất bại (Auth)")
190
+
191
+ # 2. Lưu vào bảng users (public)
192
+ user_data = {
193
+ "id": auth_res.user.id,
194
+ "email": email,
195
+ "full_name": full_name,
196
+ "role": "user",
197
+ "created_at": "now()"
198
+ }
199
+ supabase.table("users").insert(user_data).execute()
200
+
201
+ return {"success": True, "message": "Đăng ký thành công! Vui lòng kiểm tra email xác nhận."}
202
+
203
+ except Exception as e:
204
+ print(f"Register Error: {str(e)}")
205
+ # Xử lý lỗi Supabase trả về
206
+ msg = str(e)
207
+ if "User already registered" in msg:
208
+ raise HTTPException(status_code=400, detail="Email này đã được đăng ký.")
209
+ raise HTTPException(status_code=500, detail=f"Lỗi đăng ký: {msg}")
210
+
211
+ @app.post("/api/auth/login")
212
+ async def login(request: Request):
213
+ if not supabase:
214
+ raise HTTPException(status_code=503, detail="Database service unavailable")
215
+
216
+ data = await request.json()
217
+ email = data.get("email")
218
+ password = data.get("password")
219
+
220
+ try:
221
+ res = supabase.auth.sign_in_with_password({"email": email, "password": password})
222
+ if res.user:
223
+ # Lấy thông tin role từ bảng users
224
+ user_info = supabase.table("users").select("*").eq("id", res.user.id).execute()
225
+ role = "user"
226
+ full_name = ""
227
+ if user_info.data:
228
+ role = user_info.data[0].get("role", "user")
229
+ full_name = user_info.data[0].get("full_name", "")
230
+
231
+ return {
232
+ "success": True,
233
+ "access_token": res.session.access_token,
234
+ "user": {
235
+ "id": res.user.id,
236
+ "email": res.user.email,
237
+ "role": role,
238
+ "full_name": full_name
239
+ }
240
+ }
241
+ raise HTTPException(status_code=401, detail="Email hoặc mật khẩu không đúng")
242
+ except Exception as e:
243
+ raise HTTPException(status_code=401, detail=str(e))
244
+
245
+ # --- IMAGE PROCESSING UTILS (WEB) ---
246
+ async def process_large_image(image: Image.Image, model_name: str, prompt: str, semaphore: asyncio.Semaphore) -> str:
247
+ """Xử lý ảnh lớn bằng cách cắt nhỏ (Overlap Stitching)"""
248
+ width, height = image.size
249
+
250
+ # Nếu ảnh nhỏ, xử lý trực tiếp
251
+ if height < 2000:
252
+ async with semaphore:
253
+ return await call_gemini_vision(image, model_name, prompt)
254
 
255
+ # Cấu hình cắt ảnh
256
+ segment_height = 1500
257
+ overlap = 300
258
+ segments = []
259
+
260
+ for y in range(0, height, segment_height - overlap):
261
+ box = (0, y, width, min(y + segment_height, height))
262
+ segment = image.crop(box)
263
+ segments.append(segment)
264
+ if y + segment_height >= height:
265
+ break
266
 
267
+ # Gọi API song song cho các phần
268
+ tasks = []
269
+ for seg in segments:
270
+ tasks.append(call_gemini_vision(seg, model_name, prompt))
271
+
272
+ results = await asyncio.gather(*tasks)
273
+ return "\n".join(results) # Ghép kết quả đơn giản
274
+
275
+ async def call_gemini_vision(image: Image.Image, model_name: str, prompt: str) -> str:
276
+ """Hàm wrapper gọi Gemini Vision"""
277
+ try:
278
+ api_key = get_random_api_key()
279
+ genai.configure(api_key=api_key)
280
+ model = genai.GenerativeModel(model_name)
281
+ response = await model.generate_content_async([prompt, image])
282
+ return response.text if response.text else ""
283
+ except Exception as e:
284
+ print(f"Gemini Error: {e}")
285
+ return ""
286
+
287
+ # --- MAIN API: PROCESS IMAGE (WEB) ---
288
+ @app.post("/api/process-image")
289
+ async def process_image_web(
290
+ file: UploadFile = File(...),
291
+ model: str = Form("gemini-1.5-pro"),
292
+ prompt: str = Form("Hãy chuyển đổi nội dung trong ảnh thành định dạng Markdown LaTeX.")
293
+ ):
294
+ try:
295
+ contents = await file.read()
296
+ image = Image.open(io.BytesIO(contents))
297
+
298
+ # Giới hạn số luồng xử lý đồng thời để tránh Rate Limit
299
+ global_semaphore = asyncio.Semaphore(5)
300
+
301
+ # Xử lý ảnh
302
+ result_text = await process_large_image(image, model, prompt, global_semaphore)
303
+
304
+ return {"success": True, "result": clean_latex_formulas(result_text)}
305
+
306
+ except Exception as e:
307
+ import traceback
308
+ traceback.print_exc()
309
+ raise HTTPException(status_code=500, detail=str(e))
310
+
311
+ # --- WORD EXPORT API (PANDOC NATIVE) ---
312
+ @app.post("/api/export-docx")
313
+ async def export_docx(markdown_text: str = Form(...)):
314
+ try:
315
+ with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_file:
316
+ output_filename = tmp_file.name
317
+
318
+ # Dùng Pypandoc để convert
319
+ pypandoc.convert_text(
320
+ markdown_text,
321
+ to='docx',
322
+ format='markdown',
323
+ outputfile=output_filename,
324
+ extra_args=['--standalone']
325
+ )
326
+
327
+ return FileResponse(
328
+ output_filename,
329
+ media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
330
+ filename="Ket_qua_HT_MATH.docx"
331
+ )
332
+ except Exception as e:
333
+ import traceback
334
+ traceback.print_exc()
335
+ raise HTTPException(status_code=500, detail=f"Lỗi xuất Word: {str(e)}")
336
+
337
+ # --- TEST ENDPOINT ---
338
+ @app.get("/")
339
+ def home():
340
+ return {
341
+ "server": "HT MATH UNIFIED (Web + Desktop)",
342
+ "status": "online",
343
+ "pandoc": "detected" if 'pypandoc' in globals() else "missing",
344
+ "desktop_api_ready": True
345
+ }
346
 
347
  if __name__ == "__main__":
348
  import uvicorn