Ruby20260314/GeminiOCRResnedAPI
0
1# ============================================================2# 安裝套件:pip install streamlit google-genai resend openpyxl3# 執行方式:streamlit run app.py4# ============================================================5!pip install streamlit google-genai resend openpyxl6import streamlit as st7import openpyxl8from openpyxl.styles import Font, Alignment, PatternFill9import re, base64, resend10from io import BytesIO11from google import genai12from google.genai import types13 14# ─────────────────────────────────────────────15# 頁面設定16# ─────────────────────────────────────────────17st.set_page_config(18 page_title="OCR 智慧辨識工具",19 page_icon="🔍",20 layout="centered"21)22 23st.markdown("""24<style>25 @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap');26 27 html, body, [class*="css"] { font-family: 'Noto Sans TC', sans-serif; }28 29 .main { background: #0f0f1a; }30 31 .title-block {32 text-align: center;33 padding: 32px 0 24px;34 }35 .title-block h1 {36 font-size: 2.4rem;37 font-weight: 800;38 background: linear-gradient(135deg, #a78bfa, #60efdf);39 -webkit-background-clip: text;40 -webkit-text-fill-color: transparent;41 margin-bottom: 8px;42 }43 .title-block p { color: #888; font-size: 15px; }44 45 .section-label {46 font-size: 11px;47 font-weight: 600;48 letter-spacing: 0.15em;49 text-transform: uppercase;50 color: #a78bfa;51 margin: 24px 0 10px;52 display: flex;53 align-items: center;54 gap: 8px;55 }56 .section-label::after {57 content: '';58 flex: 1;59 height: 1px;60 background: rgba(167,139,250,0.2);61 }62 63 div[data-testid="stFileUploader"] > label { display: none; }64 65 .log-box {66 background: #12121f;67 border: 1px solid rgba(255,255,255,0.07);68 border-radius: 10px;69 padding: 16px 20px;70 font-family: 'Courier New', monospace;71 font-size: 13px;72 line-height: 1.8;73 max-height: 260px;74 overflow-y: auto;75 margin-top: 12px;76 }77 78 .stat-row {79 display: flex;80 gap: 14px;81 margin: 16px 0;82 }83 .stat-box {84 flex: 1;85 background: rgba(167,139,250,0.07);86 border: 1px solid rgba(167,139,250,0.2);87 border-radius: 12px;88 padding: 18px 16px;89 text-align: center;90 }91 .stat-num {92 font-size: 2.2rem;93 font-weight: 800;94 color: #a78bfa;95 line-height: 1;96 margin-bottom: 6px;97 }98 .stat-lbl { font-size: 12px; color: #888; }99 100 .dl-btn {101 display: inline-flex;102 align-items: center;103 gap: 8px;104 padding: 12px 22px;105 background: linear-gradient(135deg, #a78bfa, #60efdf);106 border-radius: 10px;107 color: #000 !important;108 font-weight: 700;109 font-size: 14px;110 text-decoration: none !important;111 transition: opacity 0.2s;112 margin-top: 4px;113 }114 .dl-btn:hover { opacity: 0.85; }115 116 .success-banner {117 background: rgba(96,239,223,0.08);118 border: 1px solid rgba(96,239,223,0.3);119 border-radius: 12px;120 padding: 20px 24px;121 margin-top: 20px;122 }123 .success-banner h3 {124 color: #60efdf;125 margin-bottom: 12px;126 font-size: 18px;127 }128 129 stButton > button {130 width: 100%;131 }132</style>133""", unsafe_allow_html=True)134 135# ─────────────────────────────────────────────136# 標題137# ─────────────────────────────────────────────138st.markdown("""139<div class="title-block">140 <h1>🔍 OCR 智慧辨識工具</h1>141 <p>上傳圖片 → Gemini OCR → 匯出 Excel → 自動寄信</p>142</div>143""", unsafe_allow_html=True)144 145# ─────────────────────────────────────────────146# 工具函式147# ─────────────────────────────────────────────148def sanitize_sheet_name(name: str, index: int) -> str:149 name = re.sub(r'[\\/*?:\[\]]', '_', name)150 return name[:31] if len(name) <= 31 else f"圖片_{index+1}"151 152 153def build_excel(files, prompt_text: str, gemini_key: str):154 """155 OCR 辨識並建立 Excel。156 回傳 (excel_bytes, logs, success_count, total_lines)157 """158 client = genai.Client(api_key=gemini_key)159 160 wb = openpyxl.Workbook()161 wb.remove(wb.active)162 163 header_font = Font(name="Arial", bold=True, size=12, color="FFFFFF")164 header_fill = PatternFill("solid", start_color="2F5496")165 header_align = Alignment(horizontal="center", vertical="center")166 body_font = Font(name="Arial", size=11)167 label_font = Font(name="Arial", bold=True, size=11, color="2F5496")168 meta_fill = PatternFill("solid", start_color="D9E1F2")169 170 logs = []171 success_count = 0172 total_lines = 0173 174 for idx, f in enumerate(files):175 logs.append(f'<span style="color:#888">[{idx+1}/{len(files)}] 辨識:{f.name} ...</span>')176 img_bytes = f.read()177 mime_type = f.type178 179 try:180 response = client.models.generate_content(181 model="gemini-2.5-flash",182 contents=[183 types.Part.from_bytes(data=img_bytes, mime_type=mime_type),184 prompt_text185 ]186 )187 ocr_text = response.text188 success_count += 1189 except Exception as e:190 ocr_text = f"❌ 辨識失敗:{e}"191 logs.append(f'<span style="color:#ff6b6b"> ❌ 失敗:{e}</span>')192 193 # 建立工作表194 sheet_name = sanitize_sheet_name(f.name, idx)195 ws = wb.create_sheet(title=sheet_name)196 ws.column_dimensions["A"].width = 10197 ws.column_dimensions["B"].width = 100198 199 ws["A1"] = "行號"200 ws["B1"] = "文字內容"201 for cell in [ws["A1"], ws["B1"]]:202 cell.font = header_font203 cell.fill = header_fill204 cell.alignment = header_align205 ws.row_dimensions[1].height = 22206 207 for label, value in [("檔案名稱", f.name), ("MIME 類型", mime_type)]:208 ws.append(["", f"【{label}】{value}"])209 r = ws.max_row210 ws.cell(r, 2).font = label_font211 ws.cell(r, 2).fill = meta_fill212 ws.cell(r, 2).alignment = Alignment(vertical="center")213 ws.row_dimensions[r].height = 18214 215 ws.append(["", ""])216 217 lines = ocr_text.splitlines()218 total_lines += len(lines)219 for line_no, line in enumerate(lines, start=1):220 ws.append([line_no, line])221 r = ws.max_row222 ws.cell(r, 1).font = Font(name="Arial", size=10, color="888888")223 ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")224 ws.cell(r, 2).font = body_font225 ws.cell(r, 2).alignment = Alignment(vertical="center")226 ws.row_dimensions[r].height = 18227 228 logs.append(f'<span style="color:#60efdf"> ✅ 完成,{len(lines)} 行 → 工作表:{sheet_name}</span>')229 230 buf = BytesIO()231 wb.save(buf)232 return buf.getvalue(), logs, success_count, total_lines233 234 235def send_email(resend_key: str, to_email: str, excel_bytes: bytes, count: int):236 resend.api_key = resend_key237 params: resend.Emails.SendParams = {238 "from": "OCR Service <onboarding@resend.dev>",239 "to": [to_email],240 "subject": f"OCR 辨識結果 — 共 {count} 張圖片",241 "html": f"""242 <h2 style="color:#2F5496">📄 OCR 辨識完成</h2>243 <p>您好,OCR 任務已完成,結果請見附件 <b>ocr_results.xlsx</b>。</p>244 <ul><li>辨識張數:{count} 張</li></ul>245 <p style="color:#999;font-size:12px">此信件由系統自動發送,請勿直接回覆。</p>246 """,247 "attachments": [{248 "filename": "ocr_results.xlsx",249 "content": base64.b64encode(excel_bytes).decode()250 }]251 }252 return resend.Emails.send(params)253 254 255def make_download_link(excel_bytes: bytes) -> str:256 b64 = base64.b64encode(excel_bytes).decode()257 mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"258 return (259 f'<a class="dl-btn" href="data:{mime};base64,{b64}" download="ocr_results.xlsx">'260 f'⬇ 下載 ocr_results.xlsx</a>'261 )262 263# ─────────────────────────────────────────────264# Sidebar:API 設定265# ─────────────────────────────────────────────266with st.sidebar:267 st.markdown("## 🔑 API 設定")268 gemini_key = st.text_input("Gemini API Key", type="password", placeholder="AIzaSy...")269 resend_key = st.text_input("Resend API Key", type="password", placeholder="re_...")270 271 st.markdown("---")272 st.markdown("## 📧 收件設定")273 to_email = st.text_input("收件信箱", placeholder="your@email.com")274 275 st.markdown("---")276 st.markdown("## ℹ️ 說明")277 st.markdown("""278- Gemini Key:[Google AI Studio](https://aistudio.google.com/)279- Resend Key:[Resend 後台](https://resend.com/)280- Resend 免費版 `from` 僅限 `onboarding@resend.dev`,`to` 需為已驗證信箱281""")282 283# ─────────────────────────────────────────────284# 主區域285# ─────────────────────────────────────────────286st.markdown('<div class="section-label">🖼️ 上傳圖片</div>', unsafe_allow_html=True)287uploaded_files = st.file_uploader(288 "上傳圖片",289 type=["jpg", "jpeg", "png", "webp", "gif", "bmp"],290 accept_multiple_files=True,291 label_visibility="collapsed"292)293 294if uploaded_files:295 cols = st.columns(min(len(uploaded_files), 4))296 for i, f in enumerate(uploaded_files):297 with cols[i % 4]:298 st.image(f, caption=f.name, use_container_width=True)299 300st.markdown('<div class="section-label">✎ OCR Prompt</div>', unsafe_allow_html=True)301prompt_text = st.text_area(302 "OCR 指令",303 value="幫我進行OCR,提取圖片中的所有文字,保留原始排版結構",304 height=90,305 label_visibility="collapsed"306)307 308st.markdown("<div style='margin-top:8px'></div>", unsafe_allow_html=True)309run_btn = st.button("▶ 開始辨識、匯出並寄信", type="primary", use_container_width=True)310 311# ─────────────────────────────────────────────312# 執行流程313# ─────────────────────────────────────────────314if run_btn:315 # 驗證316 errors = []317 if not gemini_key: errors.append("請填入 Gemini API Key")318 if not resend_key: errors.append("請填入 Resend API Key")319 if not to_email or "@" not in to_email: errors.append("請填入有效收件信箱")320 if not uploaded_files: errors.append("請先上傳至少一張圖片")321 322 if errors:323 for e in errors:324 st.error(f"⚠️ {e}")325 st.stop()326 327 # ── 辨識 ─────────────────────────────────328 st.markdown('<div class="section-label">⚡ 執行進度</div>', unsafe_allow_html=True)329 progress = st.progress(0, text="準備中...")330 log_placeholder = st.empty()331 332 all_logs = ['<span style="color:#a78bfa">▶ 開始處理...</span>']333 log_placeholder.markdown(334 f'<div class="log-box">{"<br>".join(all_logs)}</div>',335 unsafe_allow_html=True336 )337 338 try:339 # 逐張更新進度340 client_tmp = genai.Client(api_key=gemini_key)341 wb = openpyxl.Workbook()342 wb.remove(wb.active)343 344 header_font = Font(name="Arial", bold=True, size=12, color="FFFFFF")345 header_fill = PatternFill("solid", start_color="2F5496")346 header_align = Alignment(horizontal="center", vertical="center")347 body_font = Font(name="Arial", size=11)348 label_font = Font(name="Arial", bold=True, size=11, color="2F5496")349 meta_fill = PatternFill("solid", start_color="D9E1F2")350 351 success_count = 0352 total_lines = 0353 total = len(uploaded_files)354 355 for idx, f in enumerate(uploaded_files):356 pct = int((idx / total) * 70)357 progress.progress(pct, text=f"辨識圖片 {idx+1} / {total}:{f.name}")358 all_logs.append(f'<span style="color:#888">[{idx+1}/{total}] 辨識:{f.name} ...</span>')359 log_placeholder.markdown(360 f'<div class="log-box">{"<br>".join(all_logs)}</div>',361 unsafe_allow_html=True362 )363 364 img_bytes = f.read()365 mime_type = f.type366 367 try:368 resp = client_tmp.models.generate_content(369 model="gemini-2.5-flash",370 contents=[371 types.Part.from_bytes(data=img_bytes, mime_type=mime_type),372 prompt_text373 ]374 )375 ocr_text = resp.text376 success_count += 1377 except Exception as e:378 ocr_text = f"❌ 辨識失敗:{e}"379 all_logs.append(f'<span style="color:#ff6b6b"> ❌ {e}</span>')380 381 sheet_name = sanitize_sheet_name(f.name, idx)382 ws = wb.create_sheet(title=sheet_name)383 ws.column_dimensions["A"].width = 10384 ws.column_dimensions["B"].width = 100385 ws["A1"] = "行號"; ws["B1"] = "文字內容"386 for cell in [ws["A1"], ws["B1"]]:387 cell.font = header_font; cell.fill = header_fill; cell.alignment = header_align388 ws.row_dimensions[1].height = 22389 for label, value in [("檔案名稱", f.name), ("MIME 類型", mime_type)]:390 ws.append(["", f"【{label}】{value}"])391 r = ws.max_row392 ws.cell(r, 2).font = label_font393 ws.cell(r, 2).fill = meta_fill394 ws.cell(r, 2).alignment = Alignment(vertical="center")395 ws.row_dimensions[r].height = 18396 ws.append(["", ""])397 lines = ocr_text.splitlines()398 total_lines += len(lines)399 for line_no, line in enumerate(lines, start=1):400 ws.append([line_no, line])401 r = ws.max_row402 ws.cell(r, 1).font = Font(name="Arial", size=10, color="888888")403 ws.cell(r, 1).alignment = Alignment(horizontal="center", vertical="center")404 ws.cell(r, 2).font = body_font405 ws.cell(r, 2).alignment = Alignment(vertical="center")406 ws.row_dimensions[r].height = 18407 all_logs.append(f'<span style="color:#60efdf"> ✅ {len(lines)} 行 → {sheet_name}</span>')408 log_placeholder.markdown(409 f'<div class="log-box">{"<br>".join(all_logs)}</div>',410 unsafe_allow_html=True411 )412 413 # ── Excel ─────────────────────────────414 progress.progress(80, text="建立 Excel 檔案...")415 all_logs.append('<span style="color:#a78bfa">📊 建立 Excel...</span>')416 buf = BytesIO()417 wb.save(buf)418 excel_bytes = buf.getvalue()419 all_logs.append(f'<span style="color:#60efdf">✅ Excel 完成,共 {total_lines} 行</span>')420 log_placeholder.markdown(421 f'<div class="log-box">{"<br>".join(all_logs)}</div>',422 unsafe_allow_html=True423 )424 425 # ── Email ─────────────────────────────426 progress.progress(90, text=f"寄送至 {to_email}...")427 all_logs.append(f'<span style="color:#a78bfa">📧 寄送至 {to_email}...</span>')428 log_placeholder.markdown(429 f'<div class="log-box">{"<br>".join(all_logs)}</div>',430 unsafe_allow_html=True431 )432 email_ok = True433 email_msg = ""434 try:435 result = send_email(resend_key, to_email, excel_bytes, success_count)436 msg_id = result.get("id", str(result))437 all_logs.append(f'<span style="color:#60efdf">✅ 信件已寄出!ID:{msg_id}</span>')438 email_msg = f"✅ 信件已寄出,Message ID:`{msg_id}`"439 except Exception as e:440 all_logs.append(f'<span style="color:#ffd06b">⚠️ 寄信失敗:{e}</span>')441 email_ok = False442 email_msg = f"⚠️ 寄信失敗:{e}"443 444 all_logs.append('<span style="color:#a78bfa">🎉 全部完成!</span>')445 log_placeholder.markdown(446 f'<div class="log-box">{"<br>".join(all_logs)}</div>',447 unsafe_allow_html=True448 )449 progress.progress(100, text="完成!")450 451 # ── 結果卡片 ──────────────────────────452 st.markdown(f"""453 <div class="success-banner">454 <h3>🎉 全部完成!</h3>455 <div class="stat-row">456 <div class="stat-box">457 <div class="stat-num">{success_count}</div>458 <div class="stat-lbl">張圖片辨識</div>459 </div>460 <div class="stat-box">461 <div class="stat-num">{total_lines}</div>462 <div class="stat-lbl">行文字擷取</div>463 </div>464 </div>465 {make_download_link(excel_bytes)}466 </div>467 """, unsafe_allow_html=True)468 469 if email_ok:470 st.success(email_msg)471 else:472 st.warning(email_msg)473 474 except Exception as e:475 progress.progress(0, text="發生錯誤")476 st.error(f"❌ 嚴重錯誤:{e}")