Spaces:
Sleeping
Sleeping
# 이미지 업로드 및 처리 섹션#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
한국어 OCR 텍스트 추출기 | |
Google Gemini AI를 활용한 고정밀 한국어 문자 인식 애플리케이션 | |
""" | |
import gradio as gr | |
import base64 | |
import requests | |
import json | |
from PIL import Image | |
import io | |
import os | |
from typing import Optional, Tuple | |
import re | |
import time | |
import random | |
# 모듈 import 확인 | |
try: | |
import gradio as gr | |
print("✅ Gradio 모듈 정상 로드됨") | |
except ImportError as e: | |
print(f"❌ Gradio 모듈 로드 실패: {e}") | |
print("pip install gradio==4.44.0 명령어로 설치해주세요") | |
exit(1) | |
try: | |
from PIL import Image | |
print("✅ Pillow 모듈 정상 로드됨") | |
except ImportError as e: | |
print(f"❌ Pillow 모듈 로드 실패: {e}") | |
print("pip install Pillow==10.4.0 명령어로 설치해주세요") | |
exit(1) | |
try: | |
import requests | |
print("✅ Requests 모듈 정상 로드됨") | |
except ImportError as e: | |
print(f"❌ Requests 모듈 로드 실패: {e}") | |
print("pip install requests==2.32.3 명령어로 설치해주세요") | |
exit(1) | |
class KoreanOCRApp: | |
def __init__(self): | |
self.api_key = None | |
self.project_id = None | |
def set_credentials(self, api_key: str, project_id: str) -> str: | |
"""API 키와 프로젝트 ID 설정 및 검증""" | |
if not api_key or not project_id: | |
return "❌ API 키와 프로젝트 ID를 모두 입력해주세요." | |
# 프로젝트 ID 검증 (영문, 숫자, 하이픈만 허용) | |
if not re.match(r'^[a-z0-9\-]+$', project_id.strip()): | |
return "❌ 유효하지 않은 프로젝트 ID 형식입니다. 영문 소문자, 숫자, 하이픈만 사용 가능합니다." | |
self.api_key = api_key.strip() | |
self.project_id = project_id.strip() | |
return "✅ 인증 정보가 설정되었습니다." | |
def optimize_image_for_api(self, image: Image.Image) -> Image.Image: | |
"""API 호출을 위한 이미지 최적화""" | |
# 이미지 크기 최적화 (토큰 사용량 감소) | |
max_dimension = 1024 # 더 작은 크기로 제한 | |
# 현재 이미지 크기 확인 | |
width, height = image.size | |
# 큰 이미지일 경우 리사이즈 | |
if width > max_dimension or height > max_dimension: | |
image.thumbnail((max_dimension, max_dimension), Image.Resampling.LANCZOS) | |
# RGBA를 RGB로 변환 (파일 크기 감소) | |
if image.mode == 'RGBA': | |
background = Image.new('RGB', image.size, (255, 255, 255)) | |
background.paste(image, mask=image.split()[-1]) | |
image = background | |
elif image.mode != 'RGB': | |
image = image.convert('RGB') | |
return image | |
def encode_image_to_base64(self, image: Image.Image) -> str: | |
"""이미지를 base64로 인코딩 (최적화된 버전)""" | |
# 이미지 최적화 | |
image = self.optimize_image_for_api(image) | |
buffer = io.BytesIO() | |
# JPEG 형식으로 저장하여 파일 크기 최적화 (품질 80으로 낮춤) | |
image.save(buffer, format='JPEG', quality=80, optimize=True) | |
image_bytes = buffer.getvalue() | |
# 파일 크기 확인 | |
size_mb = len(image_bytes) / (1024 * 1024) | |
if size_mb > 3: # 3MB 초과 시 추가 최적화 | |
buffer = io.BytesIO() | |
image.save(buffer, format='JPEG', quality=60, optimize=True) | |
image_bytes = buffer.getvalue() | |
return base64.b64encode(image_bytes).decode('utf-8') | |
def call_gemini_api_with_retry(self, image_base64: str, max_retries: int = 3, initial_delay: float = 2.0) -> str: | |
"""재시도 로직이 포함된 Gemini API 호출""" | |
if not self.api_key: | |
return "❌ 먼저 API 키를 설정해주세요." | |
# Google AI Studio API 엔드포인트 사용 | |
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key={self.api_key}" | |
headers = { | |
"Content-Type": "application/json" | |
} | |
payload = { | |
"contents": [{ | |
"parts": [ | |
{ | |
"text": """이 이미지에 포함된 모든 한국어 텍스트를 정확하게 추출해주세요. | |
다음 규칙을 따라주세요: | |
1. 이미지에서 발견되는 모든 한국어 텍스트를 순서대로 추출 | |
2. 텍스트의 위치나 레이아웃을 최대한 보존 | |
3. 줄바꿈과 문단 구분을 명확히 표시 | |
4. 특수문자, 숫자, 영어가 포함되어 있다면 그대로 유지 | |
5. 읽기 어려운 부분이 있다면 [불분명] 표시 | |
추출된 텍스트만 반환해주세요.""" | |
}, | |
{ | |
"inline_data": { | |
"mime_type": "image/jpeg", | |
"data": image_base64 | |
} | |
} | |
] | |
}], | |
"generationConfig": { | |
"temperature": 0.1, | |
"topP": 0.8, | |
"topK": 40, | |
"maxOutputTokens": 8192 | |
}, | |
"safetySettings": [ | |
{ | |
"category": "HARM_CATEGORY_HARASSMENT", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
{ | |
"category": "HARM_CATEGORY_HATE_SPEECH", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
{ | |
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
}, | |
{ | |
"category": "HARM_CATEGORY_DANGEROUS_CONTENT", | |
"threshold": "BLOCK_MEDIUM_AND_ABOVE" | |
} | |
] | |
} | |
for attempt in range(max_retries): | |
try: | |
response = requests.post(url, headers=headers, json=payload, timeout=60) | |
if response.status_code == 401: | |
return "❌ API 키가 유효하지 않습니다. Google AI Studio에서 발급받은 올바른 API 키를 입력해주세요." | |
elif response.status_code == 403: | |
return "❌ API 접근 권한이 없습니다. Gemini API가 활성화되어 있는지 확인해주세요." | |
elif response.status_code == 429: | |
# 429 에러 시 재시도 로직 | |
if attempt < max_retries - 1: | |
delay = initial_delay * (2 ** attempt) + random.uniform(0.5, 1.5) # 지수 백오프 + 랜덤 지터 | |
return f"⏳ API 호출 한도를 초과했습니다. {delay:.1f}초 후 자동으로 재시도합니다... (시도 {attempt + 1}/{max_retries})" | |
else: | |
return """❌ API 호출 한도를 초과했습니다. | |
📌 해결 방법: | |
1. 잠시 기다린 후 다시 시도 (1-2분 권장) | |
2. Google AI Studio에서 할당량 확인 | |
3. 유료 계정으로 업그레이드 고려 | |
4. 이미지 크기를 줄여서 재시도 | |
💡 팁: 높은 해상도의 이미지는 더 많은 토큰을 사용합니다.""" | |
response.raise_for_status() | |
result = response.json() | |
if "candidates" in result and len(result["candidates"]) > 0: | |
content = result["candidates"][0]["content"]["parts"][0]["text"] | |
return content.strip() | |
elif "error" in result: | |
error_msg = result['error'].get('message', '알 수 없는 오류') | |
if "quota" in error_msg.lower() or "limit" in error_msg.lower(): | |
if attempt < max_retries - 1: | |
delay = initial_delay * (2 ** attempt) + random.uniform(0.5, 1.5) | |
time.sleep(delay) | |
continue | |
return f"❌ API 오류: {error_msg}" | |
else: | |
return "❌ 텍스트를 추출할 수 없습니다. 이미지에 한국어 텍스트가 포함되어 있는지 확인해주세요." | |
except requests.exceptions.RequestException as e: | |
if "429" in str(e) and attempt < max_retries - 1: | |
delay = initial_delay * (2 ** attempt) + random.uniform(0.5, 1.5) | |
time.sleep(delay) | |
continue | |
return f"❌ API 호출 오류: {str(e)}" | |
except json.JSONDecodeError: | |
return "❌ API 응답 파싱 오류가 발생했습니다." | |
except KeyError as e: | |
return f"❌ 예상치 못한 API 응답 형식: {str(e)}" | |
except Exception as e: | |
return f"❌ 알 수 없는 오류: {str(e)}" | |
return "❌ 최대 재시도 횟수를 초과했습니다. 잠시 후 다시 시도해주세요." | |
def call_vertex_ai_api(self, image_base64: str) -> str: | |
"""Vertex AI API 호출 (서비스 계정 키 사용)""" | |
if not self.api_key or not self.project_id: | |
return "❌ 먼저 API 키와 프로젝트 ID를 설정해주세요." | |
location = "us-central1" | |
url = f"https://{location}-aiplatform.googleapis.com/v1/projects/{self.project_id}/locations/{location}/publishers/google/models/gemini-1.5-pro:generateContent" | |
headers = { | |
"Authorization": f"Bearer {self.api_key}", | |
"Content-Type": "application/json" | |
} | |
payload = { | |
"contents": [{ | |
"role": "user", | |
"parts": [ | |
{ | |
"text": """이 이미지에 포함된 모든 한국어 텍스트를 정확하게 추출해주세요. | |
다음 규칙을 따라주세요: | |
1. 이미지에서 발견되는 모든 한국어 텍스트를 순서대로 추출 | |
2. 텍스트의 위치나 레이아웃을 최대한 보존 | |
3. 줄바꿈과 문단 구분을 명확히 표시 | |
4. 특수문자, 숫자, 영어가 포함되어 있다면 그대로 유지 | |
5. 읽기 어려운 부분이 있다면 [불분명] 표시 | |
추출된 텍스트만 반환해주세요.""" | |
}, | |
{ | |
"inline_data": { | |
"mime_type": "image/jpeg", | |
"data": image_base64 | |
} | |
} | |
] | |
}], | |
"generation_config": { | |
"temperature": 0.1, | |
"top_p": 0.8, | |
"top_k": 40, | |
"max_output_tokens": 8192 | |
} | |
} | |
try: | |
response = requests.post(url, headers=headers, json=payload, timeout=60) | |
if response.status_code == 401: | |
return "❌ 인증 오류: Access Token이 유효하지 않거나 만료되었습니다." | |
elif response.status_code == 403: | |
return "❌ 권한 오류: Vertex AI API 접근 권한이 없습니다." | |
elif response.status_code == 404: | |
return "❌ 프로젝트 ID가 올바르지 않거나 Vertex AI API가 활성화되지 않았습니다." | |
response.raise_for_status() | |
result = response.json() | |
if "candidates" in result and len(result["candidates"]) > 0: | |
content = result["candidates"][0]["content"]["parts"][0]["text"] | |
return content.strip() | |
else: | |
return "❌ 텍스트를 추출할 수 없습니다. 이미지에 한국어 텍스트가 포함되어 있는지 확인해주세요." | |
except requests.exceptions.RequestException as e: | |
return f"❌ API 호출 오류: {str(e)}" | |
except Exception as e: | |
return f"❌ 알 수 없는 오류: {str(e)}" | |
def process_image(self, image: Optional[Image.Image], api_key: str, project_id: str, api_type: str) -> Tuple[Optional[Image.Image], str]: | |
"""이미지 처리 및 OCR 수행""" | |
if image is None: | |
return None, "❌ 이미지를 업로드해주세요." | |
# 인증 정보 설정 | |
if api_type == "Google AI Studio": | |
if not api_key: | |
return image, "❌ Google AI Studio API 키를 입력해주세요." | |
self.api_key = api_key.strip() | |
else: # Vertex AI | |
auth_result = self.set_credentials(api_key, project_id) | |
if "❌" in auth_result: | |
return image, auth_result | |
try: | |
# 이미지 최적화 (토큰 사용량 감소를 위해) | |
image = self.optimize_image_for_api(image) | |
# 이미지를 base64로 인코딩 | |
image_base64 = self.encode_image_to_base64(image) | |
# API 타입에 따라 호출 | |
if api_type == "Google AI Studio": | |
extracted_text = self.call_gemini_api_with_retry(image_base64) | |
else: | |
extracted_text = self.call_vertex_ai_api(image_base64) | |
# 결과 반환 | |
return image, extracted_text | |
except Exception as e: | |
return image, f"❌ 이미지 처리 중 오류가 발생했습니다: {str(e)}" | |
# 전역 앱 인스턴스 | |
ocr_app = KoreanOCRApp() | |
def create_interface(): | |
"""Gradio 인터페이스 생성""" | |
# CSS 스타일링 | |
css = """ | |
.gradio-container { | |
font-family: 'Noto Sans KR', sans-serif; | |
} | |
.main-header { | |
text-align: center; | |
color: #2c3e50; | |
margin-bottom: 20px; | |
} | |
.info-box { | |
background-color: #e8f4fd; | |
border: 1px solid #bee5eb; | |
border-radius: 8px; | |
padding: 15px; | |
margin: 10px 0; | |
} | |
.warning-box { | |
background-color: #fff3cd; | |
border: 1px solid #ffeaa7; | |
border-radius: 8px; | |
padding: 15px; | |
margin: 10px 0; | |
color: #856404; | |
} | |
""" | |
with gr.Blocks(css=css, title="한국어 OCR - Gemini AI") as interface: | |
gr.Markdown(""" | |
# 🔍 한국어 OCR 텍스트 추출기 | |
### Google Gemini AI를 활용한 고정밀 한국어 문자 인식 | |
이미지에서 한국어 텍스트를 정확하게 추출합니다. 문서, 간판, 손글씨 등 다양한 형태의 한국어를 인식할 수 있습니다. | |
""", elem_classes="main-header") | |
# API 선택 | |
gr.Markdown("## 🔧 API 설정") | |
api_type = gr.Radio( | |
choices=["Google AI Studio", "Vertex AI"], | |
value="Google AI Studio", | |
label="사용할 API 선택", | |
info="Google AI Studio는 개인 사용자용, Vertex AI는 기업용" | |
) | |
# 인증 정보 입력 섹션 | |
with gr.Row(): | |
with gr.Column(scale=2): | |
api_key_input = gr.Textbox( | |
label="API 키 / Access Token", | |
placeholder="Google AI Studio API 키 또는 Vertex AI Access Token", | |
type="password", | |
lines=1 | |
) | |
with gr.Column(scale=1): | |
project_id_input = gr.Textbox( | |
label="프로젝트 ID (Vertex AI만)", | |
placeholder="Google Cloud 프로젝트 ID", | |
lines=1 | |
) | |
# API 설정 가이드 | |
with gr.Accordion("📖 API 설정 가이드 및 할당량 정보", open=False): | |
gr.Markdown(""" | |
### Google AI Studio API (권장) | |
1. [Google AI Studio](https://aistudio.google.com/)에 접속 | |
2. "Get API Key" 클릭 | |
3. API 키 생성 및 복사 | |
4. 위의 "API 키" 필드에 붙여넣기 | |
**📊 무료 할당량 (Google AI Studio):** | |
- 분당 15회 요청 | |
- 일일 1,500회 요청 | |
- 분당 100만 토큰 | |
- 일일 5천만 토큰 | |
### Vertex AI API (고급 사용자용) | |
1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트 생성 | |
2. Vertex AI API 활성화 | |
3. 서비스 계정 생성 및 키 다운로드 | |
4. `gcloud auth application-default login` 또는 Access Token 발급 | |
5. API 키와 프로젝트 ID 입력 | |
### ⚠️ 할당량 초과 시 해결 방법 | |
1. **잠시 대기**: 1-2분 후 다시 시도 | |
2. **이미지 최적화**: 더 작은 크기의 이미지 사용 | |
3. **사용량 분산**: 여러 번 나누어서 처리 | |
4. **유료 계정**: Google Cloud 유료 계정으로 업그레이드 | |
### 💡 토큰 절약 팁 | |
- 이미지 해상도: 1024x1024 이하 권장 | |
- 파일 형식: JPEG 사용 (PNG보다 작음) | |
- 불필요한 배경 제거 | |
- 텍스트 영역만 크롭하여 업로드 | |
""", elem_classes="warning-box") | |
# 할당량 상태 표시 | |
with gr.Row(): | |
gr.Markdown(""" | |
### 📊 현재 상태 | |
**무료 할당량 (Google AI Studio):** | |
- ⏱️ 분당 15회 요청 제한 | |
- 📅 일일 1,500회 요청 제한 | |
- 🔢 고해상도 이미지는 더 많은 토큰 사용 | |
**💡 할당량 절약 팁:** | |
- 이미지 크기를 1024x1024 이하로 유지 | |
- 텍스트가 있는 부분만 크롭하여 업로드 | |
- 연속적인 요청 간 1-2초 간격 유지 | |
""", elem_classes="info-box") | |
gr.Markdown("## 📤 이미지 업로드 및 텍스트 추출") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
input_image = gr.Image( | |
label="📁 이미지 업로드", | |
type="pil", | |
sources=["upload", "clipboard"], | |
interactive=True | |
) | |
process_btn = gr.Button( | |
"🔍 텍스트 추출 시작", | |
variant="primary", | |
size="lg" | |
) | |
with gr.Column(scale=1): | |
output_image = gr.Image( | |
label="📋 업로드된 이미지 확인", | |
type="pil", | |
interactive=False | |
) | |
# 추출된 텍스트 출력 | |
gr.Markdown("## 📝 추출된 텍스트") | |
extracted_text = gr.Textbox( | |
label="인식된 한국어 텍스트", | |
placeholder="추출된 텍스트가 여기에 표시됩니다...", | |
lines=10, | |
max_lines=20, | |
interactive=True, | |
show_copy_button=True | |
) | |
# 이벤트 핸들러 | |
process_btn.click( | |
fn=ocr_app.process_image, | |
inputs=[input_image, api_key_input, project_id_input, api_type], | |
outputs=[output_image, extracted_text], | |
show_progress=True | |
) | |
# 사용 팁 | |
gr.Markdown(""" | |
### 💡 사용 팁 | |
**📸 이미지 품질:** | |
- 선명하고 해상도가 높은 이미지 사용 | |
- 충분한 조명과 대비 | |
- 텍스트가 수평으로 배치된 이미지 권장 | |
**📄 지원 형식:** | |
- **이미지 형식:** PNG, JPEG, WebP | |
- **최대 크기:** 4MB (자동 리사이즈) | |
- **인식 언어:** 한국어, 영어, 숫자, 특수문자 | |
**🔒 보안:** | |
- API 키는 세션 동안만 임시 저장 | |
- 이미지는 서버에 저장되지 않음 | |
- 개인정보가 포함된 이미지 사용 시 주의 | |
**⚡ 성능:** | |
- Google AI Studio: 빠르고 안정적 (권장) | |
- Vertex AI: 기업용 고급 기능 | |
""") | |
return interface | |
# 메인 실행 | |
if __name__ == "__main__": | |
try: | |
print("🚀 한국어 OCR 애플리케이션을 시작합니다...") | |
# 인터페이스 생성 | |
demo = create_interface() | |
print("✅ 인터페이스 생성 완료") | |
print("🌐 서버를 시작합니다...") | |
# 서버 실행 | |
demo.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=True, | |
debug=True, | |
show_error=True, | |
inbrowser=True | |
) | |
except Exception as e: | |
print(f"❌ 애플리케이션 시작 중 오류 발생: {e}") | |
print("\n🔧 문제 해결 방법:") | |
print("1. pip install gradio==4.44.0 Pillow==10.4.0 requests==2.32.3") | |
print("2. Python 버전 확인 (3.8 이상 필요)") | |
print("3. 가상환경 사용 권장") | |
print("4. 네트워크 연결 상태 확인") | |
raise |