Files
2026-07-13 13:37:43 +08:00

298 lines
12 KiB
Python

import os
from openai import OpenAI
import streamlit as st
from dotenv import load_dotenv
import tempfile
import shutil
import base64
import fitz # PyMuPDF for PDF to image
load_dotenv()
st.set_page_config(page_title="Nvidia Nemotron-Nano OCR", layout="centered")
if "messages" not in st.session_state:
st.session_state.messages = []
if "docs_loaded" not in st.session_state:
st.session_state.docs_loaded = False
if "temp_dir" not in st.session_state:
st.session_state.temp_dir = None
if "current_pdf" not in st.session_state:
st.session_state.current_pdf = None
# Convert images to base64
with open("./assets/nvidia-color.png", "rb") as nvidia_file:
nvidia_base64 = base64.b64encode(nvidia_file.read()).decode()
# Create title with embedded images
title_html = f"""
<div style="display: flex; align-items: center; gap: 10px;">
<h1 style="margin: 0;">
<img src="data:image/png;base64,{nvidia_base64}" style="height: 56px; margin: 0;">
<span style="color: #74B71B;"> Nvidia Nemotron-Nano </span> OCR
</h1>
</div>
"""
st.markdown(title_html, unsafe_allow_html=True)
st.subheader("**Extract text from PDFs and images using NVIDIA Nemotron-Nano**")
# Sidebar for configuration
with st.sidebar:
st.image("./assets/Nebius.png", width=150)
# Model selection
nebius_api_key = st.text_input(
"Nebius API Key",
value=os.getenv("NEBIUS_API_KEY", ""),
type="password",
help="Your Nebius API key",
)
st.divider()
# PDF or Image file upload
st.subheader("Upload PDF or Image")
uploaded_file = st.file_uploader(
"Choose a PDF, JPG, or PNG file",
type=["pdf", "jpg", "jpeg", "png"],
accept_multiple_files=False,
)
def display_file_preview(file):
if file is None:
return
file_type = file.type
if file_type == "application/pdf":
# Display PDF preview
st.sidebar.subheader("PDF Preview")
base64_pdf = base64.b64encode(file.getvalue()).decode("utf-8")
pdf_display = f'<iframe src="data:application/pdf;base64,{base64_pdf}" width="100%" height="500" type="application/pdf"></iframe>'
st.sidebar.markdown(pdf_display, unsafe_allow_html=True)
elif file_type in ["image/png", "image/jpeg", "image/jpg"]:
st.sidebar.subheader("Image Preview")
st.sidebar.image(file, use_container_width=True)
else:
st.sidebar.info("Unsupported file type for preview.")
st.markdown("---")
st.markdown("Made with ❤️ by [Studio1](https://www.Studio1hq.com) Team")
def ocr(file, api_key):
file_type = file.type
file_bytes = file.getvalue()
client = OpenAI(
base_url="https://api.tokenfactory.nebius.com/v1",
api_key=api_key or os.environ.get("NEBIUS_API_KEY"),
)
# Improved system prompt for better OCR results
ocr_prompt = """You are an advanced OCR system designed for accurate document text extraction with smart formatting.
CORE OBJECTIVES:
1. **Extract all text accurately**: Capture every visible character, number, and symbol
2. **Preserve logical structure**: Group related content together, not as individual items
3. **Minimize headings**: Use headings only for major sections, not for every field
4. **Use tables strategically**: Convert form fields, lists of similar items, and structured data into markdown tables
5. **Maintain readability**: Clean output that's easy to scan and understand
FORMATTING RULES:
- # for main sections only (rarely needed)
- ## for key subsections (use sparingly)
- ❌ NEVER create a heading for each form field - that's excessive
- ✅ Group related form fields together using tables or key-value lists
- ✅ Use markdown tables with pipes: | Field | Value | for forms and structured data
- ✅ Use bullet lists (•) for simple lists, not individual headings
- ✅ Use horizontal lines (---) only between major sections
SMART HANDLING OF FORMS & REPEATING FIELDS:
- When you see many similar form fields (like 8a, 8b, 8c, 8d...), create a TABLE instead of individual headings
- For checkboxes and options, show: ☐ Option 1, ☐ Option 2 on same line or in a table
CRITICAL OUTPUT RULES:
- ⛔ NO triple backticks (```) for code blocks - NEVER
- ⛔ NO indented code blocks - NEVER
- ⛔ NO heading for every single field
- ✅ Group similar items into tables or lists
- ✅ Use key-value format: Label: Value when appropriate
- ✅ Use markdown tables for structured/repetitive fields
- ✅ Inline backticks only for `technical terms`
- ✅ Plain markdown text without code blocks
EXAMPLE OUTPUT STRUCTURE:
# Form Title
## Section A - Basic Information
| Field | Value |
|-------|-------|
| Name | [value] |
| ID | [value] |
| Date | [value] |
## Section B - Checkboxes
- ☐ Option 1: [status]
- ☐ Option 2: [status]
---
## Section C - Details
Description of content here...
**Key Points:**
- Point 1
- Point 2
This style is MUCH more readable than heading for every field."""
if file_type in ["image/png", "image/jpeg", "image/jpg"]:
b64_data = base64.b64encode(file_bytes).decode()
mime = file_type
with st.spinner("Extracting text from image..."):
try:
response = client.chat.completions.create(
model="nvidia/Nemotron-Nano-V2-12b",
max_tokens=512,
temperature=0.5,
top_p=0.9,
extra_body={"top_k": 50},
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": ocr_prompt,
},
{
"type": "image_url",
"image_url": {
"url": f"data:{mime};base64,{b64_data}"
},
},
],
}
],
)
return (
response.choices[0].message.content
if hasattr(response.choices[0].message, "content")
else str(response)
)
except Exception as e:
return f"OCR API call failed: {e}"
elif file_type == "application/pdf":
# Process all pages
try:
with tempfile.NamedTemporaryFile(
delete=False, suffix=".pdf"
) as tmp_pdf:
tmp_pdf.write(file_bytes)
tmp_pdf.flush()
doc = fitz.open(tmp_pdf.name)
num_pages = doc.page_count
results = []
progress = st.progress(0, text="Processing PDF pages...")
for i in range(num_pages):
page = doc.load_page(i)
pix = page.get_pixmap()
img_bytes = pix.tobytes("png")
b64_data = base64.b64encode(img_bytes).decode()
mime = "image/png"
try:
response = client.chat.completions.create(
model="nvidia/Nemotron-Nano-V2-12b",
# max_tokens=512,
temperature=0.5,
top_p=0.9,
extra_body={"top_k": 50},
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"{ocr_prompt}\n\nNote: This is page {i+1} of {num_pages}. Extract content from this page only.",
},
{
"type": "image_url",
"image_url": {
"url": f"data:{mime};base64,{b64_data}"
},
},
],
}
],
)
text = (
response.choices[0].message.content
if hasattr(response.choices[0].message, "content")
else str(response)
)
except Exception as e:
text = f"OCR API call failed on page {i+1}: {e}"
results.append(text)
progress.progress(
(i + 1) / num_pages,
text=f"Processed {i+1} of {num_pages} pages...",
)
progress.empty()
return "\n\n".join(results)
except Exception as e:
return f"PDF to image conversion failed: {e}"
else:
return "Unsupported file type for OCR."
# Handle file upload and processing
if uploaded_file is not None:
if uploaded_file != st.session_state.current_pdf:
st.session_state.current_pdf = uploaded_file
try:
if not os.getenv("NEBIUS_API_KEY"):
st.error("Missing Nebius API key")
st.stop()
# Create temporary directory for the file
if st.session_state.temp_dir:
shutil.rmtree(st.session_state.temp_dir)
st.session_state.temp_dir = tempfile.mkdtemp()
# Save uploaded file to temp directory
file_path = os.path.join(st.session_state.temp_dir, uploaded_file.name)
with open(file_path, "wb") as f:
f.write(uploaded_file.getbuffer())
st.session_state.docs_loaded = True
st.session_state.current_file = uploaded_file
if uploaded_file.type == "application/pdf":
st.success("✓ PDF loaded successfully")
else:
st.success("✓ Image loaded successfully")
except Exception as e:
st.error(f"Error: {str(e)}")
# Always show preview
display_file_preview(uploaded_file)
# OCR button
if st.button("🔍 Extract Text (OCR)"):
extracted_text = ocr(uploaded_file, nebius_api_key)
st.session_state.extracted_text = extracted_text
if "extracted_text" not in st.session_state:
# st.error("No text extracted. Please upload a PDF or image file to extract text.")
st.markdown(
"""
This application is powered by advanced OCR technology for document and image analysis:
- **Text Extraction**: Extracts text from PDFs and images with high accuracy
- **Table Recognition**: Automatically identifies and formats tables in documents
- **Multi-Format Support**: Processes PDF files and common image formats (JPG, PNG)
The app leverages NVIDIA Nemotron-Nano models via Nebius API to deliver precise, structured text extraction for seamless document processing.
"""
)
st.stop()
else:
st.success("Text extracted successfully.")
st.markdown(st.session_state.extracted_text)