Skip to content

Commit b86c381

Browse files
authored
Merge pull request #2 from crackerky/feature/ai-ocr-vision
Feature/ai ocr vision
2 parents 4be491e + a454601 commit b86c381

4 files changed

Lines changed: 269 additions & 4 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# AI-OCR Vision API Feature
2+
3+
This branch adds support for OpenAI's Vision API (GPT-4o) for advanced OCR capabilities.
4+
5+
## Features
6+
7+
- **AI-OCR with GPT-4o Vision**: Direct image analysis without traditional OCR
8+
- **Automatic fallback**: If Vision API fails, falls back to Tesseract OCR
9+
- **Better accuracy**: Handles handwritten text, poor quality images, and complex layouts
10+
- **Multi-language support**: Works with Japanese and English receipts
11+
12+
## Configuration
13+
14+
Add these environment variables to enable Vision API:
15+
16+
```bash
17+
# Enable/disable Vision API (default: true)
18+
USE_VISION_API=true
19+
20+
# Vision API model (options: gpt-4o, gpt-4o-mini)
21+
VISION_API_MODEL=gpt-4o
22+
23+
# Your existing OpenAI API key
24+
OPENAI_API_KEY=sk-your-api-key
25+
```
26+
27+
## How it works
28+
29+
1. **Image Upload**: User uploads a receipt image
30+
2. **Vision API Processing**:
31+
- Image is encoded to base64
32+
- Sent to GPT-4o Vision API with structured prompt
33+
- AI directly extracts receipt information
34+
3. **Fallback**: If Vision API fails, uses traditional OCR pipeline:
35+
- Tesseract OCR → Text extraction → GPT-3.5 analysis → Regex patterns
36+
37+
## Cost Considerations
38+
39+
- **GPT-4o**: $5.00 per 1M input tokens, $20.00 per 1M output tokens
40+
- **GPT-4o-mini**: $0.15 per 1M input tokens, $0.60 per 1M output tokens
41+
- Average receipt image uses 500-2000 tokens (high detail)
42+
43+
## Testing
44+
45+
1. Install dependencies:
46+
```bash
47+
pip install -r requirements.txt
48+
```
49+
50+
2. Set environment variables:
51+
```bash
52+
export USE_VISION_API=true
53+
export VISION_API_MODEL=gpt-4o
54+
export OPENAI_API_KEY=your-key
55+
```
56+
57+
3. Run the application:
58+
```bash
59+
uvicorn app.main:app --reload
60+
```
61+
62+
4. Test with various receipt images:
63+
- Clear printed receipts
64+
- Handwritten receipts
65+
- Blurry or low-quality images
66+
- Complex layouts with tables
67+
68+
## Performance
69+
70+
Vision API provides:
71+
- Better accuracy for difficult images
72+
- Direct extraction without OCR errors
73+
- Structured data output
74+
- Support for various image formats
75+
76+
## Deployment
77+
78+
For Railway deployment, add the environment variables in the Railway dashboard:
79+
- `USE_VISION_API=true`
80+
- `VISION_API_MODEL=gpt-4o` (or `gpt-4o-mini` for cost savings)
81+
82+
The feature is backward compatible - if Vision API is disabled or fails, it automatically falls back to the traditional OCR pipeline.

receipt-scanner-app/receipt-scanner-backend/app/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def __init__(self):
1717
# Required environment variables
1818
self.openai_api_key = self._get_required_env("OPENAI_API_KEY")
1919

20+
# Vision API settings
21+
self.use_vision_api = os.getenv("USE_VISION_API", "true").lower() == "true"
22+
self.vision_api_model = os.getenv("VISION_API_MODEL", "gpt-4o") # or "gpt-4o-mini"
23+
2024
# Optional environment variables with defaults
2125
self.database_url = os.getenv("DATABASE_URL", "postgresql://localhost:5432/receipt_scanner")
2226
self.secret_key = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production")
@@ -111,6 +115,11 @@ def is_development(self) -> bool:
111115
def openai_available(self) -> bool:
112116
"""Check if OpenAI API is available."""
113117
return bool(self.openai_api_key)
118+
119+
@property
120+
def vision_api_available(self) -> bool:
121+
"""Check if Vision API is available and enabled."""
122+
return bool(self.openai_api_key) and self.use_vision_api
114123

115124
# Global settings instance
116125
settings = Settings()

receipt-scanner-app/receipt-scanner-backend/app/receipt_processor.py

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import logging
44
import platform
55
import subprocess
6+
import base64
67
from datetime import datetime
78
from PIL import Image, ImageFilter, ImageEnhance
89
from typing import Dict, Any, Optional, Tuple
10+
from openai import OpenAI
911

1012
# HEIFのインポートを条件付きに
1113
try:
@@ -86,22 +88,37 @@ def setup_tesseract():
8688

8789

8890
class ReceiptProcessor:
89-
"""統合されたレシート処理クラス - AI-OCRの協調処理"""
91+
"""Secure receipt processing with AI-OCR Vision and fallback OCR functionality."""
92+
9093

9194
def __init__(self):
9295
"""Initialize the receipt processor with secure configuration."""
9396
self.openai_available = settings.openai_available
97+
self.vision_api_available = settings.vision_api_available
9498
self.tesseract_available = tesseract_available
9599
self.cv2_available = CV2_AVAILABLE
96100
self.heif_available = HEIF_AVAILABLE
97101

98-
# 処理モード設定
99-
self.processing_mode = self._determine_processing_mode()
102+
100103

101104
# OCRプロセッサーの初期化
102105
self.ocr_processor = OCRProcessor(cv2_available=self.cv2_available)
103106

104-
# AIプロセッサーの初期化
107+
# AIプロセッサーの初期化 # 処理モード設定
108+
self.processing_mode = self._determine_processing_mode()
109+
110+
# Initialize OpenAI client for Vision API
111+
if self.vision_api_available:
112+
try:
113+
self.openai_client = OpenAI(api_key=settings.openai_api_key)
114+
logger.info("OpenAI Vision API initialized successfully")
115+
except Exception as e:
116+
logger.error(f"Failed to initialize OpenAI Vision API: {e}")
117+
self.vision_api_available = False
118+
119+
if not self.tesseract_available:
120+
logger.error("Tesseract OCR is not available. Please install Tesseract OCR.")
121+
105122
self.ai_processor = None
106123
if self.openai_available:
107124
try:
@@ -157,6 +174,139 @@ def _check_tesseract_languages(self):
157174
except Exception as e:
158175
logger.error(f"Failed to get Tesseract languages: {e}")
159176

177+
def _create_prompt_template(self) -> ChatPromptTemplate:
178+
"""Create a secure prompt template for OpenAI."""
179+
return ChatPromptTemplate.from_template(
180+
"""
181+
以下は日本のレシートのテキストです。このテキストから以下の情報を抽出してください:
182+
1. 日付 (YYYY-MM-DD形式、見つからない場合はnull)
183+
2. 店名または会社名
184+
3. 合計金額 (数値のみ、見つからない場合はnull)
185+
4. 税抜き価格 (あれば、数値のみ)
186+
5. 税込み価格 (あれば、数値のみ)
187+
188+
JSONフォーマットで回答してください:
189+
{{
190+
"date": "YYYY-MM-DD" or null,
191+
"store_name": "店名",
192+
"total_amount": 数値 or null,
193+
"tax_excluded_amount": 数値 or null,
194+
"tax_included_amount": 数値 or null
195+
}}
196+
197+
レシートテキスト:
198+
{text}
199+
"""
200+
)
201+
202+
def _create_vision_prompt(self) -> str:
203+
"""Create a prompt for Vision API OCR."""
204+
return """
205+
この画像は日本のレシートです。以下の情報を正確に抽出してください:
206+
207+
1. 日付 (YYYY-MM-DD形式、見つからない場合はnull)
208+
2. 店名または会社名
209+
3. 合計金額 (数値のみ、見つからない場合はnull)
210+
4. 税抜き価格 (あれば、数値のみ)
211+
5. 税込み価格 (あれば、数値のみ)
212+
213+
以下のJSONフォーマットで回答してください:
214+
{
215+
"date": "YYYY-MM-DD" or null,
216+
"store_name": "店名",
217+
"total_amount": 数値 or null,
218+
"tax_excluded_amount": 数値 or null,
219+
"tax_included_amount": 数値 or null
220+
}
221+
222+
注意事項:
223+
- 日付は必ずYYYY-MM-DD形式に変換してください
224+
- 金額は数値のみ(カンマや円記号は除く)
225+
- 税抜き/税込み価格が明記されていない場合はnull
226+
- 不明な情報はnullとしてください
227+
"""
228+
229+
def _extract_with_vision_api(self, image_bytes: bytes) -> Dict[str, Any]:
230+
"""Extract receipt information using GPT-4o Vision API."""
231+
try:
232+
# Convert image to base64
233+
base64_image = base64.b64encode(image_bytes).decode('utf-8')
234+
235+
logger.info("Sending image to Vision API for OCR...")
236+
237+
# Call Vision API
238+
response = self.openai_client.chat.completions.create(
239+
model="gpt-4o", # or "gpt-4o-mini" for cost savings
240+
messages=[
241+
{
242+
"role": "user",
243+
"content": [
244+
{
245+
"type": "text",
246+
"text": self._create_vision_prompt()
247+
},
248+
{
249+
"type": "image_url",
250+
"image_url": {
251+
"url": f"data:image/jpeg;base64,{base64_image}",
252+
"detail": "high" # Use "high" for better OCR accuracy
253+
}
254+
}
255+
]
256+
}
257+
],
258+
max_tokens=1000,
259+
response_format={"type": "json_object"} # Ensure JSON response
260+
)
261+
262+
# Parse the response
263+
result_text = response.choices[0].message.content
264+
logger.info(f"Vision API response: {result_text}")
265+
266+
# Parse JSON response
267+
data = json.loads(result_text)
268+
269+
# Validate and process the data
270+
processed_data = {
271+
"date": data.get("date"),
272+
"store_name": data.get("store_name"),
273+
"total_amount": float(data.get("total_amount")) if data.get("total_amount") else None,
274+
"tax_excluded_amount": float(data.get("tax_excluded_amount")) if data.get("tax_excluded_amount") else None,
275+
"tax_included_amount": float(data.get("tax_included_amount")) if data.get("tax_included_amount") else None,
276+
"expense_category": None
277+
}
278+
279+
# Validate required fields
280+
if not processed_data.get("store_name"):
281+
return {
282+
"success": False,
283+
"message": "Vision APIで店名を抽出できませんでした。",
284+
"data": None
285+
}
286+
287+
logger.info("Successfully extracted receipt data with Vision API")
288+
289+
return {
290+
"success": True,
291+
"message": "AI-OCR (Vision API)でレシート情報を抽出しました。",
292+
"data": processed_data
293+
}
294+
295+
except json.JSONDecodeError as e:
296+
logger.error(f"Failed to parse JSON from Vision API response: {e}")
297+
return {
298+
"success": False,
299+
"message": "Vision APIのレスポンスが無効でした。",
300+
"data": None
301+
}
302+
except Exception as e:
303+
logger.error(f"Vision API extraction error: {e}")
304+
return {
305+
"success": False,
306+
"message": f"Vision API処理中にエラーが発生しました: {str(e)}",
307+
"data": None
308+
}
309+
160310
def _convert_heic_to_jpeg(self, image_bytes: bytes) -> bytes:
161311
"""Convert HEIC/HEIF image to JPEG format."""
162312
if not self.heif_available:
@@ -321,6 +471,29 @@ def process_image(self, image_bytes: bytes, processing_mode: Optional[str] = Non
321471
# 処理モードの決定
322472
if not processing_mode:
323473
processing_mode = "auto"
474+
475+
# Try Vision API first if available
476+
if self.vision_api_available:
477+
logger.info("Attempting AI-OCR with Vision API...")
478+
result = self._extract_with_vision_api(image_bytes)
479+
if result["success"]:
480+
# 日付が抽出できなかった場合、現在の日時を使用
481+
if result["data"] and not result["data"].get("date"):
482+
result["data"]["date"] = datetime.now().strftime("%Y-%m-%d")
483+
result["message"] += " 日付は現在の日付で補完しました。"
484+
return result
485+
else:
486+
logger.warning("Vision API failed, falling back to traditional OCR")
487+
488+
# Fall back to traditional OCR processing
489+
# Tesseractが利用できない場合のエラー
490+
if not self.tesseract_available:
491+
return {
492+
"success": False,
493+
"message": "OCRエンジンが利用できません。Tesseract OCRをインストールしてください。",
494+
"data": None
495+
}
496+
324497

325498
# Vision APIモードの場合
326499
if processing_mode == "vision" and self.openai_client:

receipt-scanner-app/receipt-scanner-backend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ alembic>=1.13.0
1919
python-jose[cryptography]>=3.3.0
2020
passlib[bcrypt]>=1.7.4
2121
email-validator>=2.0.0
22+

0 commit comments

Comments
 (0)