CoolFace
Apppublic

Ruby20260314/GeminiOCRResnedAPI

sourceHugging Faceotherupdated 6mo agoView on Hugging Face
0likes
app.py476 linesDownload Raw Back to root
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}")