GeminiOCR / app.py
AdrianLi33's picture
Create app.py
92c50f9 verified
Raw
History Blame Contribute Delete
9.36 kB
# ============================================================
# OCR + Email ๅˆไฝตๅทฅๅ…ท๏ผˆGemini OCR โ†’ Excel โ†’ Resend ๅฏ„ไฟก๏ผ‰
# ============================================================
# ๅฎ‰่ฃไพ่ณด๏ผšpip install resend google-genai openpyxl ipywidgets
# ============================================================
import os
import re
import base64
import resend
import openpyxl
from openpyxl.styles import Font, Alignment, PatternFill
from google import genai
from google.genai import types
import ipywidgets as widgets
from IPython.display import display, clear_output, HTML
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# UI ๅ…ƒไปถ๏ผšAPI ้‡‘้‘ฐ & ่จญๅฎš
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
style = {'description_width': '140px'}
layout = widgets.Layout(width='600px')
gemini_key_input = widgets.Password(
description='Gemini API Key:',
placeholder='่ฒผไธŠไฝ ็š„ Gemini API Key',
style=style, layout=layout
)
resend_key_input = widgets.Password(
description='Resend API Key:',
placeholder='่ฒผไธŠไฝ ็š„ Resend API Key',
style=style, layout=layout
)
sender_input = widgets.Text(
description='ๅฏ„ไปถไบบ:',
value='Acme <onboarding@resend.dev>',
style=style, layout=layout
)
recipient_input = widgets.Text(
description='ๆ”ถไปถไบบ Email:',
placeholder='example@gmail.com',
style=style, layout=layout
)
subject_input = widgets.Text(
description='้ƒตไปถไธปๆ—จ:',
value='OCR ่พจ่ญ˜็ตๆžœ',
style=style, layout=layout
)
prompt_input = widgets.Text(
description='OCR Prompt:',
value='ๅนซๆˆ‘้€ฒ่กŒOCR๏ผŒๆๅ–ๅœ–็‰‡ไธญ็š„ๆ‰€ๆœ‰ๆ–‡ๅญ—๏ผŒไฟ็•™ๅŽŸๅง‹ๆŽ’็‰ˆ็ตๆง‹',
style=style, layout=layout
)
upload = widgets.FileUpload(accept='image/*', multiple=True)
button = widgets.Button(
description='๐Ÿš€ ้–‹ๅง‹่พจ่ญ˜ไธฆๅฏ„ไฟก',
button_style='primary',
layout=widgets.Layout(width='220px')
)
output = widgets.Output()
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๅทฅๅ…ทๅ‡ฝๅผ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def sanitize_sheet_name(name: str, index: int) -> str:
name = re.sub(r'[\\/*?:\[\]]', '_', name)
return name[:31] if len(name) <= 31 else f"ๅœ–็‰‡_{index+1}"
def build_excel(files: list, prompt: str, client) -> tuple[bytes, str]:
"""ๅŸท่กŒ OCR ไธฆๅปบ็ซ‹ Excel๏ผŒๅ›žๅ‚ณ (bytes, ๆ‘˜่ฆๆ–‡ๅญ—)"""
output_path = 'ocr_results.xlsx'
wb = openpyxl.Workbook()
wb.remove(wb.active)
header_font = Font(name='Arial', bold=True, size=12, color='FFFFFF')
header_fill = PatternFill('solid', start_color='2F5496')
header_align = Alignment(horizontal='center', vertical='center')
body_font = Font(name='Arial', size=11)
label_font = Font(name='Arial', bold=True, size=11, color='2F5496')
meta_fill = PatternFill('solid', start_color='D9E1F2')
summary_lines = []
for idx, (filename, file_info) in enumerate(files):
print(f'[{idx+1}/{len(files)}] ่พจ่ญ˜๏ผš{filename} ...')
img_bytes = bytes(file_info['content'])
mime_type = file_info['metadata']['type']
try:
response = client.models.generate_content(
model='gemini-2.5-flash',
contents=[
types.Part.from_bytes(data=img_bytes, mime_type=mime_type),
prompt
]
)
ocr_text = response.text
except Exception as e:
ocr_text = f'โŒ ่พจ่ญ˜ๅคฑๆ•—๏ผš{e}'
sheet_name = sanitize_sheet_name(filename, idx)
ws = wb.create_sheet(title=sheet_name)
ws.column_dimensions['A'].width = 10
ws.column_dimensions['B'].width = 100
ws['A1'] = '่กŒ่™Ÿ'
ws['B1'] = 'ๆ–‡ๅญ—ๅ…งๅฎน'
for cell in [ws['A1'], ws['B1']]:
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
ws.row_dimensions[1].height = 22
for label, value in [('ๆช”ๆกˆๅ็จฑ', filename), ('MIME ้กžๅž‹', mime_type)]:
ws.append(['', f'ใ€{label}ใ€‘{value}'])
row_idx = ws.max_row
cell_b = ws.cell(row=row_idx, column=2)
cell_b.font = label_font
cell_b.fill = meta_fill
cell_b.alignment = Alignment(vertical='center')
ws.row_dimensions[row_idx].height = 18
ws.append(['', ''])
lines = ocr_text.splitlines()
for line_no, line in enumerate(lines, start=1):
ws.append([line_no, line])
row_idx = ws.max_row
ws.cell(row=row_idx, column=1).font = Font(name='Arial', size=10, color='888888')
ws.cell(row=row_idx, column=1).alignment = Alignment(horizontal='center', vertical='center')
ws.cell(row=row_idx, column=2).font = body_font
ws.cell(row=row_idx, column=2).alignment = Alignment(vertical='center')
ws.row_dimensions[row_idx].height = 18
summary_lines.append(f'โ€ข {filename}๏ผšๅ…ฑ {len(lines)} ่กŒ')
print(f' โœ… ๅฎŒๆˆ๏ผŒๅ…ฑ {len(lines)} ่กŒ')
wb.save(output_path)
with open(output_path, 'rb') as f:
excel_bytes = f.read()
return excel_bytes, '\n'.join(summary_lines)
def send_email_with_attachment(
api_key: str, sender: str, recipient: str,
subject: str, excel_bytes: bytes, summary: str
):
"""้€้Ž Resend ๅฏ„้€ๅซ Excel ้™„ไปถ็š„้ƒตไปถ"""
resend.api_key = api_key
b64_content = base64.b64encode(excel_bytes).decode()
html_body = f"""
<h2>๐Ÿ“Š OCR ่พจ่ญ˜็ตๆžœ</h2>
<p>ๆ‚จๅฅฝ๏ผŒไปฅไธ‹ๆ˜ฏๆœฌๆฌก OCR ่พจ่ญ˜ๆ‘˜่ฆ๏ผš</p>
<pre style="background:#f4f4f4;padding:12px;border-radius:6px;">{summary}</pre>
<p>่ฉณ็ดฐ็ตๆžœ่ซ‹่ฆ‹้™„ไปถ <strong>ocr_results.xlsx</strong>ใ€‚</p>
<hr>
<small style="color:#888;">็”ฑ Gemini OCR ๅทฅๅ…ท่‡ชๅ‹•็”ข็”Ÿ</small>
"""
params: resend.Emails.SendParams = {
'from': sender,
'to': [recipient],
'subject': subject,
'html': html_body,
'attachments': [
{
'filename': 'ocr_results.xlsx',
'content': b64_content,
}
],
}
result = resend.Emails.send(params)
return result
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๆŒ‰้ˆ•ไบ‹ไปถ
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def on_click(b):
with output:
clear_output()
# ้ฉ—่ญ‰่ผธๅ…ฅ
if not gemini_key_input.value.strip():
print('โš ๏ธ ่ซ‹ๅกซๅ…ฅ Gemini API Key๏ผ')
return
if not resend_key_input.value.strip():
print('โš ๏ธ ่ซ‹ๅกซๅ…ฅ Resend API Key๏ผ')
return
if not recipient_input.value.strip():
print('โš ๏ธ ่ซ‹ๅกซๅ…ฅๆ”ถไปถไบบ Email๏ผ')
return
if not upload.value:
print('โš ๏ธ ่ซ‹ๅ…ˆไธŠๅ‚ณ่‡ณๅฐ‘ไธ€ๅผตๅœ–็‰‡๏ผ')
return
# ๅˆๅง‹ๅŒ– Gemini client
client = genai.Client(api_key=gemini_key_input.value.strip())
files = list(upload.value.items())
print(f'ๅ…ฑ {len(files)} ๅผตๅœ–็‰‡๏ผŒ้–‹ๅง‹่พจ่ญ˜...\n')
# OCR + ๅปบ็ซ‹ Excel
try:
excel_bytes, summary = build_excel(files, prompt_input.value, client)
except Exception as e:
print(f'โŒ OCR ๆˆ– Excel ๅปบ็ซ‹ๅคฑๆ•—๏ผš{e}')
return
# ๆไพ›ไธ‹่ผ‰้€ฃ็ต
b64 = base64.b64encode(excel_bytes).decode()
link_html = (
f'<a href="data:application/vnd.openxmlformats-officedocument'
f'.spreadsheetml.sheet;base64,{b64}" download="ocr_results.xlsx">'
f'๐Ÿ“ฅ ้ปžๆญคไธ‹่ผ‰ ocr_results.xlsx</a>'
)
display(HTML(link_html))
# ๅฏ„้€้ƒตไปถ
print(f'\n๐Ÿ“ง ๆญฃๅœจๅฏ„้€่‡ณ {recipient_input.value.strip()} ...')
try:
result = send_email_with_attachment(
api_key=resend_key_input.value.strip(),
sender=sender_input.value.strip(),
recipient=recipient_input.value.strip(),
subject=subject_input.value.strip(),
excel_bytes=excel_bytes,
summary=summary
)
print(f'โœ… ้ƒตไปถๅฏ„้€ๆˆๅŠŸ๏ผID๏ผš{result.get("id", result)}')
except Exception as e:
print(f'โŒ ้ƒตไปถๅฏ„้€ๅคฑๆ•—๏ผš{e}')
button.on_click(on_click)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ้กฏ็คบไป‹้ข
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
display(
widgets.HTML('<h3>๐Ÿ”‘ API ่จญๅฎš</h3>'),
gemini_key_input,
resend_key_input,
widgets.HTML('<h3>๐Ÿ“ง ้ƒตไปถ่จญๅฎš</h3>'),
sender_input,
recipient_input,
subject_input,
widgets.HTML('<h3>๐Ÿ–ผ๏ธ ๅœ–็‰‡่พจ่ญ˜่จญๅฎš</h3>'),
prompt_input,
upload,
button,
output
)