felixkky commited on
Commit
b3d2821
·
verified ·
1 Parent(s): 157ad3a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -30
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.client import Mistral
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
- # 2. 升級版:極度嚴格的 Prompt (防幻覺、防抓印刷體)
41
- prompt = """
42
- 你是一個專精於「中文手寫字跡辨識」的 AI 專家。
43
- 這張圖片是一份學生的考卷,上面同時包含「電腦打字的印刷體題目」以及「學生手寫的作答字跡」。
44
- 學生的字跡混合了繁體中文與簡體中文,且字體較為潦草,通常在橫線上或空白處
 
45
 
46
- 你的任務是
47
- 1. **絕對忽略印刷體**:尋找並提取手寫字絕對不要轉錄任何電腦印刷體的題目內容!
48
- 2. **精準辨識(拒絕幻覺)**:請最大努力辨識學生的手寫字(包含簡體與繁體)。如果你遇到真的看不懂的草字,請嚴格使用「[無法辨識]」四個字代替絕對不要自己發明、猜測或聯想詞彙。
49
- 3. **輸出邊界框**:給出段手寫字在圖片上的相對邊界框 [ymin, xmin, ymax, xmax](範圍 0.0 到 1.0)。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- 請嚴格以 JSON 格式輸出
52
  {
53
  "handwriting": [
54
- {"text": "第一段手寫內容", "box": [0.12, 0.34, 0.15, 0.55]},
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.3, desc=f"處理第 {page_num + 1} 頁:準備圖片資料...")
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
- progress(0.4, desc=f"第 {page_num + 1} 頁:Pixtral-large-latest 運算中 (約 30~60 秒)...")
69
- response = client.chat.complete(
 
 
 
70
  model="pixtral-large-latest",
71
  messages=[
72
  {
73
  "role": "user",
74
  "content": [
75
- {"type": "text", "text": prompt},
76
  {"type": "image_url", "image_url": base64_image}
77
  ]
78
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ],
80
  response_format={"type": "json_object"}
81
  )
82
 
83
- progress(0.8, desc=f"第 {page_num + 1} 頁:正在解析 JSON 並畫框...")
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"API 或解析發生錯誤: {str(e)}"
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="手寫字偵測與畫框 Demo") as demo:
118
- gr.Markdown("## 📝 手寫字偵測與畫框 Demo (Pixtral-large-latest)")
119
- gr.Markdown("上傳考卷 PDF 或圖片,系統會自動排除印刷體,找出所有的「手寫文字」並用紅框示出來。")
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("開始偵測畫框", variant="primary")
126
 
127
  with gr.Column():
128
  output_gallery = gr.Gallery(label="畫框結果預覽", columns=1, height="auto")
129
- output_json = gr.JSON(label="模型輸出的 JSON (文字與座)")
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,