Spaces:
Sleeping
Sleeping
quocdat1964 commited on
Commit ·
08aa8c5
1
Parent(s): bddeee9
Dùng api từ gg AIstudio để check trước khi đổi openrouter
Browse files- app/config.py +3 -0
- app/llm_service.py +64 -20
- requirements.txt +1 -0
app/config.py
CHANGED
|
@@ -25,6 +25,9 @@ class Settings:
|
|
| 25 |
# Google API Key (Dùng cho Google AI / Gemini)
|
| 26 |
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY", None)
|
| 27 |
|
|
|
|
|
|
|
|
|
|
| 28 |
# Device để chạy inference: "cpu" hoặc "cuda"
|
| 29 |
DEVICE: str = os.getenv("DEVICE", "cpu")
|
| 30 |
|
|
|
|
| 25 |
# Google API Key (Dùng cho Google AI / Gemini)
|
| 26 |
GOOGLE_API_KEY: str | None = os.getenv("GOOGLE_API_KEY", None)
|
| 27 |
|
| 28 |
+
# OpenRouter API Key (Dùng để gọi các model AI qua chuẩn OpenAI)
|
| 29 |
+
OPENROUTER_API_KEY: str | None = os.getenv("OPENROUTER_API_KEY", None)
|
| 30 |
+
|
| 31 |
# Device để chạy inference: "cpu" hoặc "cuda"
|
| 32 |
DEVICE: str = os.getenv("DEVICE", "cpu")
|
| 33 |
|
app/llm_service.py
CHANGED
|
@@ -1,16 +1,26 @@
|
|
| 1 |
import io
|
| 2 |
import json
|
|
|
|
| 3 |
import traceback
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
# pyrefly: ignore [missing-import]
|
| 7 |
import PyPDF2
|
| 8 |
-
from app.config import settings
|
|
|
|
| 9 |
|
| 10 |
-
# Configure
|
| 11 |
if settings.GOOGLE_API_KEY:
|
| 12 |
genai.configure(api_key=settings.GOOGLE_API_KEY)
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
def extract_text_from_pdf(file_bytes: bytes) -> str:
|
| 15 |
"""Extract raw text from PDF bytes using PyPDF2."""
|
| 16 |
pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
|
|
@@ -23,13 +33,14 @@ def extract_text_from_pdf(file_bytes: bytes) -> str:
|
|
| 23 |
|
| 24 |
def extract_cv_info(file_bytes: bytes, filename: str) -> dict:
|
| 25 |
"""
|
| 26 |
-
Extract structured CV info using Google AI (Gemma 3 27B /
|
| 27 |
Hỗ trợ cả định dạng PDF và Hình ảnh (Image).
|
| 28 |
"""
|
| 29 |
if not settings.GOOGLE_API_KEY:
|
| 30 |
raise ValueError("Thiếu GOOGLE_API_KEY trong cấu hình.")
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
import mimetypes
|
| 33 |
filename_lower = filename.lower()
|
| 34 |
is_pdf = filename_lower.endswith('.pdf')
|
| 35 |
is_image = filename_lower.endswith(('.png', '.jpg', '.jpeg', '.webp'))
|
|
@@ -37,14 +48,6 @@ def extract_cv_info(file_bytes: bytes, filename: str) -> dict:
|
|
| 37 |
if not is_pdf and not is_image:
|
| 38 |
raise ValueError("Định dạng file không được hỗ trợ. Chỉ hỗ trợ PDF và các định dạng ảnh (PNG, JPG, JPEG, WEBP).")
|
| 39 |
|
| 40 |
-
# 1. Khởi tạo model Gemma 3 27B (Theo yêu cầu của người dùng)
|
| 41 |
-
try:
|
| 42 |
-
model = genai.GenerativeModel('gemma-3-27b-it')
|
| 43 |
-
except Exception as e:
|
| 44 |
-
print("Lỗi khởi tạo model Gemma 3:", str(e))
|
| 45 |
-
# Fallback trong trường hợp API endpoint chưa hỗ trợ gemma-3
|
| 46 |
-
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 47 |
-
|
| 48 |
base_prompt = """
|
| 49 |
Bạn là một chuyên gia phân tích nhân sự (HR) và trích xuất dữ liệu. Hãy đọc văn bản được trích xuất từ một CV hoặc hình ảnh CV dưới đây và bóc tách các thông tin thành định dạng JSON chuẩn.
|
| 50 |
CHỈ THỊ QUAN TRỌNG:
|
|
@@ -109,27 +112,68 @@ CHỈ THỊ QUAN TRỌNG:
|
|
| 109 |
"""
|
| 110 |
|
| 111 |
# 2. Xử lý Input (Dựa trên loại file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
if is_pdf:
|
| 113 |
raw_text = extract_text_from_pdf(file_bytes)
|
| 114 |
if not raw_text.strip():
|
| 115 |
raise ValueError("Không thể đọc được văn bản từ file PDF (có thể file là ảnh scan). Vui lòng chuyển sang định dạng ảnh (JPG/PNG).")
|
|
|
|
|
|
|
| 116 |
prompt_data = base_prompt + f"\n\nNội dung CV:\n---\n{raw_text[:10000]}\n---"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
else:
|
| 118 |
-
# Nếu là hình ảnh, đẩy thẳng byte của hình ảnh lên Model
|
| 119 |
mime_type, _ = mimetypes.guess_type(filename)
|
| 120 |
if not mime_type:
|
| 121 |
mime_type = "image/jpeg"
|
| 122 |
-
|
|
|
|
| 123 |
prompt_data = [
|
| 124 |
{"mime_type": mime_type, "data": file_bytes},
|
| 125 |
base_prompt
|
| 126 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
# 3. Gọi API
|
|
|
|
| 129 |
response = model.generate_content(prompt_data)
|
| 130 |
-
|
| 131 |
-
# 4. Xử lý và làm sạch JSON trả về
|
| 132 |
response_text = response.text.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
if response_text.startswith("```json"):
|
| 134 |
response_text = response_text[7:]
|
| 135 |
if response_text.startswith("```"):
|
|
|
|
| 1 |
import io
|
| 2 |
import json
|
| 3 |
+
import base64
|
| 4 |
import traceback
|
| 5 |
+
import mimetypes
|
| 6 |
+
# from openai import OpenAI
|
|
|
|
| 7 |
import PyPDF2
|
| 8 |
+
from app.config import settings
|
| 9 |
+
import google.generativeai as genai
|
| 10 |
|
| 11 |
+
# Configure Google AI Studio (Tạm thời mở lại theo yêu cầu)
|
| 12 |
if settings.GOOGLE_API_KEY:
|
| 13 |
genai.configure(api_key=settings.GOOGLE_API_KEY)
|
| 14 |
|
| 15 |
+
# Configure OpenRouter (via OpenAI SDK) - TẠM COMMENT
|
| 16 |
+
# if settings.OPENROUTER_API_KEY:
|
| 17 |
+
# client = OpenAI(
|
| 18 |
+
# base_url="https://openrouter.ai/api/v1",
|
| 19 |
+
# api_key=settings.OPENROUTER_API_KEY,
|
| 20 |
+
# )
|
| 21 |
+
# else:
|
| 22 |
+
# client = None
|
| 23 |
+
|
| 24 |
def extract_text_from_pdf(file_bytes: bytes) -> str:
|
| 25 |
"""Extract raw text from PDF bytes using PyPDF2."""
|
| 26 |
pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
|
|
|
|
| 33 |
|
| 34 |
def extract_cv_info(file_bytes: bytes, filename: str) -> dict:
|
| 35 |
"""
|
| 36 |
+
Extract structured CV info using Google AI (Gemma 3 27B / OpenAI API standard).
|
| 37 |
Hỗ trợ cả định dạng PDF và Hình ảnh (Image).
|
| 38 |
"""
|
| 39 |
if not settings.GOOGLE_API_KEY:
|
| 40 |
raise ValueError("Thiếu GOOGLE_API_KEY trong cấu hình.")
|
| 41 |
+
# if not client:
|
| 42 |
+
# raise ValueError("Thiếu OPENROUTER_API_KEY trong cấu hình.")
|
| 43 |
|
|
|
|
| 44 |
filename_lower = filename.lower()
|
| 45 |
is_pdf = filename_lower.endswith('.pdf')
|
| 46 |
is_image = filename_lower.endswith(('.png', '.jpg', '.jpeg', '.webp'))
|
|
|
|
| 48 |
if not is_pdf and not is_image:
|
| 49 |
raise ValueError("Định dạng file không được hỗ trợ. Chỉ hỗ trợ PDF và các định dạng ảnh (PNG, JPG, JPEG, WEBP).")
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
base_prompt = """
|
| 52 |
Bạn là một chuyên gia phân tích nhân sự (HR) và trích xuất dữ liệu. Hãy đọc văn bản được trích xuất từ một CV hoặc hình ảnh CV dưới đây và bóc tách các thông tin thành định dạng JSON chuẩn.
|
| 53 |
CHỈ THỊ QUAN TRỌNG:
|
|
|
|
| 112 |
"""
|
| 113 |
|
| 114 |
# 2. Xử lý Input (Dựa trên loại file)
|
| 115 |
+
messages = [
|
| 116 |
+
{
|
| 117 |
+
"role": "system",
|
| 118 |
+
"content": "Bạn là một AI phân tích CV cực kỳ chính xác. Bạn chỉ trả về định dạng JSON, tuyệt đối không bao gồm markdown hay bất kỳ text giải thích nào khác."
|
| 119 |
+
}
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
if is_pdf:
|
| 123 |
raw_text = extract_text_from_pdf(file_bytes)
|
| 124 |
if not raw_text.strip():
|
| 125 |
raise ValueError("Không thể đọc được văn bản từ file PDF (có thể file là ảnh scan). Vui lòng chuyển sang định dạng ảnh (JPG/PNG).")
|
| 126 |
+
|
| 127 |
+
# --- Code cho Google AI Studio ---
|
| 128 |
prompt_data = base_prompt + f"\n\nNội dung CV:\n---\n{raw_text[:10000]}\n---"
|
| 129 |
+
|
| 130 |
+
# --- Code cho OpenRouter (Tạm comment) ---
|
| 131 |
+
# user_content = base_prompt + f"\n\nNội dung CV:\n---\n{raw_text[:10000]}\n---"
|
| 132 |
+
# messages.append({
|
| 133 |
+
# "role": "user",
|
| 134 |
+
# "content": user_content
|
| 135 |
+
# })
|
| 136 |
else:
|
|
|
|
| 137 |
mime_type, _ = mimetypes.guess_type(filename)
|
| 138 |
if not mime_type:
|
| 139 |
mime_type = "image/jpeg"
|
| 140 |
+
|
| 141 |
+
# --- Code cho Google AI Studio ---
|
| 142 |
prompt_data = [
|
| 143 |
{"mime_type": mime_type, "data": file_bytes},
|
| 144 |
base_prompt
|
| 145 |
]
|
| 146 |
+
|
| 147 |
+
# --- Code cho OpenRouter (Tạm comment) ---
|
| 148 |
+
# base64_image = base64.b64encode(file_bytes).decode("utf-8")
|
| 149 |
+
# messages.append({
|
| 150 |
+
# "role": "user",
|
| 151 |
+
# "content": [
|
| 152 |
+
# {
|
| 153 |
+
# "type": "text",
|
| 154 |
+
# "text": base_prompt
|
| 155 |
+
# },
|
| 156 |
+
# {
|
| 157 |
+
# "type": "image_url",
|
| 158 |
+
# "image_url": {
|
| 159 |
+
# "url": f"data:{mime_type};base64,{base64_image}"
|
| 160 |
+
# }
|
| 161 |
+
# }
|
| 162 |
+
# ]
|
| 163 |
+
# })
|
| 164 |
|
| 165 |
+
# 3. Gọi Google AI API (Sử dụng gemini-2.5-flash làm test chuẩn)
|
| 166 |
+
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 167 |
response = model.generate_content(prompt_data)
|
|
|
|
|
|
|
| 168 |
response_text = response.text.strip()
|
| 169 |
+
|
| 170 |
+
# 3. Gọi OpenRouter API (Tạm comment)
|
| 171 |
+
# response = client.chat.completions.create(
|
| 172 |
+
# model="google/gemma-3-27b-it", # Tên model chính xác trên OpenRouter
|
| 173 |
+
# messages=messages,
|
| 174 |
+
# temperature=0.1, # Giảm sáng tạo, tăng độ chính xác trích xuất
|
| 175 |
+
# )
|
| 176 |
+
# response_text = response.choices[0].message.content.strip()
|
| 177 |
if response_text.startswith("```json"):
|
| 178 |
response_text = response_text[7:]
|
| 179 |
if response_text.startswith("```"):
|
requirements.txt
CHANGED
|
@@ -19,6 +19,7 @@ pydantic
|
|
| 19 |
python-dotenv
|
| 20 |
gradio
|
| 21 |
pandas
|
|
|
|
| 22 |
google-generativeai
|
| 23 |
python-multipart
|
| 24 |
PyPDF2
|
|
|
|
| 19 |
python-dotenv
|
| 20 |
gradio
|
| 21 |
pandas
|
| 22 |
+
openai
|
| 23 |
google-generativeai
|
| 24 |
python-multipart
|
| 25 |
PyPDF2
|