Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,7 +4,7 @@ import base64
|
|
| 4 |
import json
|
| 5 |
from io import BytesIO
|
| 6 |
from PIL import Image, ImageDraw
|
| 7 |
-
from mistralai
|
| 8 |
|
| 9 |
def process_document(file, api_key, progress=gr.Progress()):
|
| 10 |
if not api_key:
|
|
@@ -24,7 +24,6 @@ def process_document(file, api_key, progress=gr.Progress()):
|
|
| 24 |
doc = fitz.open(file.name)
|
| 25 |
for i in range(len(doc)):
|
| 26 |
page = doc.load_page(i)
|
| 27 |
-
# 設定 DPI,保持清晰度
|
| 28 |
pix = page.get_pixmap(dpi=150)
|
| 29 |
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 30 |
images.append(img)
|
|
@@ -37,56 +36,93 @@ def process_document(file, api_key, progress=gr.Progress()):
|
|
| 37 |
drawn_images = []
|
| 38 |
all_json_results = []
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
1.
|
| 48 |
-
2.
|
| 49 |
-
3.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
{
|
| 53 |
"handwriting": [
|
| 54 |
-
{"text": "
|
| 55 |
-
{"text": "第二段手寫內容", "box": [0.60, 0.10, 0.65, 0.80]}
|
| 56 |
]
|
| 57 |
}
|
|
|
|
|
|
|
|
|
|
| 58 |
"""
|
| 59 |
|
| 60 |
for page_num, img in enumerate(images):
|
| 61 |
-
progress(0.
|
| 62 |
buffered = BytesIO()
|
| 63 |
img.save(buffered, format="JPEG")
|
| 64 |
img_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 65 |
base64_image = f"data:image/jpeg;base64,{img_b64}"
|
| 66 |
|
| 67 |
try:
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
model="pixtral-large-latest",
|
| 71 |
messages=[
|
| 72 |
{
|
| 73 |
"role": "user",
|
| 74 |
"content": [
|
| 75 |
-
{"type": "text", "text":
|
| 76 |
{"type": "image_url", "image_url": base64_image}
|
| 77 |
]
|
| 78 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
],
|
| 80 |
response_format={"type": "json_object"}
|
| 81 |
)
|
| 82 |
|
| 83 |
-
|
| 84 |
-
content = response.choices[0].message.content
|
| 85 |
-
|
| 86 |
-
# 清理模型可能多加的 Markdown JSON 標記 (防錯機制)
|
| 87 |
-
clean_content = content.replace("```json", "").replace("```", "").strip()
|
| 88 |
-
data = json.loads(clean_content)
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
all_json_results.append({f"Page {page_num + 1}": data})
|
| 91 |
|
| 92 |
draw = ImageDraw.Draw(img)
|
|
@@ -106,7 +142,7 @@ def process_document(file, api_key, progress=gr.Progress()):
|
|
| 106 |
drawn_images.append(img)
|
| 107 |
|
| 108 |
except Exception as e:
|
| 109 |
-
error_msg = f"
|
| 110 |
all_json_results.append({f"Page {page_num + 1} Error": error_msg})
|
| 111 |
drawn_images.append(img)
|
| 112 |
|
|
@@ -114,19 +150,19 @@ def process_document(file, api_key, progress=gr.Progress()):
|
|
| 114 |
return drawn_images, json.dumps(all_json_results, ensure_ascii=False, indent=2)
|
| 115 |
|
| 116 |
# --- Gradio 介面設計 ---
|
| 117 |
-
with gr.Blocks(title="手寫字偵測
|
| 118 |
-
gr.Markdown("## 📝 手寫字偵測與畫框 Demo
|
| 119 |
-
gr.Markdown("
|
| 120 |
|
| 121 |
with gr.Row():
|
| 122 |
with gr.Column():
|
| 123 |
api_key_input = gr.Textbox(label="Mistral API Key", type="password")
|
| 124 |
file_input = gr.File(label="上傳 PDF 或圖片", file_types=[".pdf", ".jpg", ".png", ".jpeg"])
|
| 125 |
-
submit_btn = gr.Button("開始偵測
|
| 126 |
|
| 127 |
with gr.Column():
|
| 128 |
output_gallery = gr.Gallery(label="畫框結果預覽", columns=1, height="auto")
|
| 129 |
-
output_json = gr.JSON(label="
|
| 130 |
|
| 131 |
submit_btn.click(
|
| 132 |
fn=process_document,
|
|
|
|
| 4 |
import json
|
| 5 |
from io import BytesIO
|
| 6 |
from PIL import Image, ImageDraw
|
| 7 |
+
from mistralai import Mistral
|
| 8 |
|
| 9 |
def process_document(file, api_key, progress=gr.Progress()):
|
| 10 |
if not api_key:
|
|
|
|
| 24 |
doc = fitz.open(file.name)
|
| 25 |
for i in range(len(doc)):
|
| 26 |
page = doc.load_page(i)
|
|
|
|
| 27 |
pix = page.get_pixmap(dpi=150)
|
| 28 |
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
| 29 |
images.append(img)
|
|
|
|
| 36 |
drawn_images = []
|
| 37 |
all_json_results = []
|
| 38 |
|
| 39 |
+
# ==========================================
|
| 40 |
+
# Prompt 1: 給 Pixtral (專注看圖、免 JSON 壓力)
|
| 41 |
+
# ==========================================
|
| 42 |
+
prompt_step1_pixtral = """
|
| 43 |
+
你是一個專精於「中文手寫字跡辨識」的 AI 視覺專家。
|
| 44 |
+
這張圖片是一份學生的考卷。你的唯一任務是找出「所有學生手寫的文字」,並給出位置。
|
| 45 |
|
| 46 |
+
請嚴格遵守以下規則:
|
| 47 |
+
1. 絕對忽略所有電腦打字的印刷體!只提取手寫字。
|
| 48 |
+
2. 盡力辨識潦草字跡(包含簡繁體),若完全無法辨識請寫 [無法辨識]。
|
| 49 |
+
3. 給出每段手寫字在圖片上的相對邊界框 [ymin, xmin, ymax, xmax](範圍 0.0 到 1.0)。
|
| 50 |
+
|
| 51 |
+
【重要】請使用「純文字」輸出,絕對不要輸出 JSON 格式!請用以下格式條列:
|
| 52 |
+
內容:[手寫字]
|
| 53 |
+
座標:[ymin, xmin, ymax, xmax]
|
| 54 |
+
---
|
| 55 |
+
內容:[手寫字]
|
| 56 |
+
座標:[ymin, xmin, ymax, xmax]
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
# ==========================================
|
| 60 |
+
# Prompt 2: 給 Mistral Large (專注格式轉換)
|
| 61 |
+
# ==========================================
|
| 62 |
+
prompt_step2_large = """
|
| 63 |
+
你是一個資料格式化專家。請將以下由 OCR 模型提取出的純文字紀錄,轉換為嚴格的 JSON 格式。
|
| 64 |
+
請確保座標被轉換為包含 4 個浮點數的陣列。
|
| 65 |
|
| 66 |
+
必須輸出的 JSON 格式如下:
|
| 67 |
{
|
| 68 |
"handwriting": [
|
| 69 |
+
{"text": "提取的內容", "box": [0.12, 0.34, 0.15, 0.55]}
|
|
|
|
| 70 |
]
|
| 71 |
}
|
| 72 |
+
|
| 73 |
+
這是需要轉換的原始資料:
|
| 74 |
+
\n\n
|
| 75 |
"""
|
| 76 |
|
| 77 |
for page_num, img in enumerate(images):
|
| 78 |
+
progress(0.2, desc=f"處理第 {page_num + 1} 頁:準備圖片資料...")
|
| 79 |
buffered = BytesIO()
|
| 80 |
img.save(buffered, format="JPEG")
|
| 81 |
img_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 82 |
base64_image = f"data:image/jpeg;base64,{img_b64}"
|
| 83 |
|
| 84 |
try:
|
| 85 |
+
# ---------------------------------------------------------
|
| 86 |
+
# 步驟一:呼叫 Pixtral 進行視覺提取 (純文字輸出)
|
| 87 |
+
# ---------------------------------------------------------
|
| 88 |
+
progress(0.3, desc=f"第 {page_num + 1} 頁 [Step 1/2]:Pixtral 視覺提取中 (約 30 秒)...")
|
| 89 |
+
response_step1 = client.chat.complete(
|
| 90 |
model="pixtral-large-latest",
|
| 91 |
messages=[
|
| 92 |
{
|
| 93 |
"role": "user",
|
| 94 |
"content": [
|
| 95 |
+
{"type": "text", "text": prompt_step1_pixtral},
|
| 96 |
{"type": "image_url", "image_url": base64_image}
|
| 97 |
]
|
| 98 |
}
|
| 99 |
+
]
|
| 100 |
+
)
|
| 101 |
+
pixtral_raw_text = response_step1.choices[0].message.content
|
| 102 |
+
print(f"--- Pixtral 原始輸出 (Page {page_num+1}) ---\n{pixtral_raw_text}\n---------------------------")
|
| 103 |
+
|
| 104 |
+
# ---------------------------------------------------------
|
| 105 |
+
# 步驟二:呼叫 Mistral Large 進行 JSON 結構化
|
| 106 |
+
# ---------------------------------------------------------
|
| 107 |
+
progress(0.7, desc=f"第 {page_num + 1} 頁 [Step 2/2]:Mistral Large 格式轉換中 (約 5 秒)...")
|
| 108 |
+
response_step2 = client.chat.complete(
|
| 109 |
+
model="mistral-large-latest",
|
| 110 |
+
messages=[
|
| 111 |
+
{
|
| 112 |
+
"role": "user",
|
| 113 |
+
"content": prompt_step2_large + pixtral_raw_text
|
| 114 |
+
}
|
| 115 |
],
|
| 116 |
response_format={"type": "json_object"}
|
| 117 |
)
|
| 118 |
|
| 119 |
+
final_json_content = response_step2.choices[0].message.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
# ---------------------------------------------------------
|
| 122 |
+
# 解析並畫框
|
| 123 |
+
# ---------------------------------------------------------
|
| 124 |
+
progress(0.9, desc=f"第 {page_num + 1} 頁:正在圖片上繪製邊界框...")
|
| 125 |
+
data = json.loads(final_json_content)
|
| 126 |
all_json_results.append({f"Page {page_num + 1}": data})
|
| 127 |
|
| 128 |
draw = ImageDraw.Draw(img)
|
|
|
|
| 142 |
drawn_images.append(img)
|
| 143 |
|
| 144 |
except Exception as e:
|
| 145 |
+
error_msg = f"發生錯誤: {str(e)}"
|
| 146 |
all_json_results.append({f"Page {page_num + 1} Error": error_msg})
|
| 147 |
drawn_images.append(img)
|
| 148 |
|
|
|
|
| 150 |
return drawn_images, json.dumps(all_json_results, ensure_ascii=False, indent=2)
|
| 151 |
|
| 152 |
# --- Gradio 介面設計 ---
|
| 153 |
+
with gr.Blocks(title="雙模型協作:手寫字偵測 Demo") as demo:
|
| 154 |
+
gr.Markdown("## 📝 雙模型協作:手寫字偵測與畫框 Demo")
|
| 155 |
+
gr.Markdown("**架構:** `Pixtral-large` (視覺提取純文字) $\\rightarrow$ `Mistral-large` (轉換標準 JSON) $\\rightarrow$ 畫框")
|
| 156 |
|
| 157 |
with gr.Row():
|
| 158 |
with gr.Column():
|
| 159 |
api_key_input = gr.Textbox(label="Mistral API Key", type="password")
|
| 160 |
file_input = gr.File(label="上傳 PDF 或圖片", file_types=[".pdf", ".jpg", ".png", ".jpeg"])
|
| 161 |
+
submit_btn = gr.Button("開始雙階段偵測", variant="primary")
|
| 162 |
|
| 163 |
with gr.Column():
|
| 164 |
output_gallery = gr.Gallery(label="畫框結果預覽", columns=1, height="auto")
|
| 165 |
+
output_json = gr.JSON(label="Step 2 轉換後的標準 JSON")
|
| 166 |
|
| 167 |
submit_btn.click(
|
| 168 |
fn=process_document,
|