Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import gradio as gr
|
2 |
import base64
|
3 |
import requests
|
4 |
import json
|
@@ -7,6 +7,8 @@ import io
|
|
7 |
import os
|
8 |
from typing import Optional, Tuple
|
9 |
import re
|
|
|
|
|
10 |
|
11 |
class KoreanOCRApp:
|
12 |
def __init__(self):
|
@@ -26,22 +28,48 @@ class KoreanOCRApp:
|
|
26 |
self.project_id = project_id.strip()
|
27 |
return "✅ 인증 정보가 설정되었습니다."
|
28 |
|
29 |
-
def
|
30 |
-
"""
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
if image.mode == 'RGBA':
|
34 |
-
# RGBA 이미지는 RGB로 변환
|
35 |
background = Image.new('RGB', image.size, (255, 255, 255))
|
36 |
background.paste(image, mask=image.split()[-1])
|
37 |
image = background
|
|
|
|
|
38 |
|
39 |
-
image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
image_bytes = buffer.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
return base64.b64encode(image_bytes).decode('utf-8')
|
42 |
|
43 |
-
def
|
44 |
-
"""
|
45 |
if not self.api_key:
|
46 |
return "❌ 먼저 API 키를 설정해주세요."
|
47 |
|
@@ -100,36 +128,62 @@ class KoreanOCRApp:
|
|
100 |
]
|
101 |
}
|
102 |
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
if response.status_code == 401:
|
107 |
-
return "❌ API 키가 유효하지 않습니다. Google AI Studio에서 발급받은 올바른 API 키를 입력해주세요."
|
108 |
-
elif response.status_code == 403:
|
109 |
-
return "❌ API 접근 권한이 없습니다. Gemini API가 활성화되어 있는지 확인해주세요."
|
110 |
-
elif response.status_code == 429:
|
111 |
-
return "❌ API 호출 한도를 초과했습니다. 잠시 후 다시 시도해주세요."
|
112 |
-
|
113 |
-
response.raise_for_status()
|
114 |
-
|
115 |
-
result = response.json()
|
116 |
-
|
117 |
-
if "candidates" in result and len(result["candidates"]) > 0:
|
118 |
-
content = result["candidates"][0]["content"]["parts"][0]["text"]
|
119 |
-
return content.strip()
|
120 |
-
elif "error" in result:
|
121 |
-
return f"❌ API 오류: {result['error'].get('message', '알 수 없는 오류')}"
|
122 |
-
else:
|
123 |
-
return "❌ 텍스트를 추출할 수 없습니다. 이미지에 한국어 텍스트가 포함되어 있는지 확인해주세요."
|
124 |
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
|
134 |
def call_vertex_ai_api(self, image_base64: str) -> str:
|
135 |
"""Vertex AI API 호출 (서비스 계정 키 사용)"""
|
@@ -216,22 +270,15 @@ class KoreanOCRApp:
|
|
216 |
return image, auth_result
|
217 |
|
218 |
try:
|
219 |
-
# 이미지
|
220 |
-
|
221 |
-
image.save(img_byte_array, format='JPEG', quality=95)
|
222 |
-
img_size_mb = len(img_byte_array.getvalue()) / (1024 * 1024)
|
223 |
-
|
224 |
-
if img_size_mb > 4: # 4MB로 제한을 낮춤
|
225 |
-
# 이미지 크기가 너무 크면 리사이즈
|
226 |
-
max_dimension = 1920
|
227 |
-
image.thumbnail((max_dimension, max_dimension), Image.Resampling.LANCZOS)
|
228 |
|
229 |
# 이미지를 base64로 인코딩
|
230 |
image_base64 = self.encode_image_to_base64(image)
|
231 |
|
232 |
# API 타입에 따라 호출
|
233 |
if api_type == "Google AI Studio":
|
234 |
-
extracted_text = self.
|
235 |
else:
|
236 |
extracted_text = self.call_vertex_ai_api(image_base64)
|
237 |
|
@@ -313,7 +360,7 @@ def create_interface():
|
|
313 |
)
|
314 |
|
315 |
# API 설정 가이드
|
316 |
-
with gr.Accordion("📖 API 설정 가이드", open=False):
|
317 |
gr.Markdown("""
|
318 |
### Google AI Studio API (권장)
|
319 |
1. [Google AI Studio](https://aistudio.google.com/)에 접속
|
@@ -321,6 +368,12 @@ def create_interface():
|
|
321 |
3. API 키 생성 및 복사
|
322 |
4. 위의 "API 키" 필드에 붙여넣기
|
323 |
|
|
|
|
|
|
|
|
|
|
|
|
|
324 |
### Vertex AI API (고급 사용자용)
|
325 |
1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트 생성
|
326 |
2. Vertex AI API 활성화
|
@@ -328,13 +381,34 @@ def create_interface():
|
|
328 |
4. `gcloud auth application-default login` 또는 Access Token 발급
|
329 |
5. API 키와 프로젝트 ID 입력
|
330 |
|
331 |
-
### ⚠️
|
332 |
-
|
333 |
-
|
334 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
""", elem_classes="warning-box")
|
336 |
|
337 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
gr.Markdown("## 📤 이미지 업로드 및 텍스트 추출")
|
339 |
|
340 |
with gr.Row():
|
|
|
1 |
+
# 이미지 업로드 및 처리 섹션import gradio as gr
|
2 |
import base64
|
3 |
import requests
|
4 |
import json
|
|
|
7 |
import os
|
8 |
from typing import Optional, Tuple
|
9 |
import re
|
10 |
+
import time
|
11 |
+
import random
|
12 |
|
13 |
class KoreanOCRApp:
|
14 |
def __init__(self):
|
|
|
28 |
self.project_id = project_id.strip()
|
29 |
return "✅ 인증 정보가 설정되었습니다."
|
30 |
|
31 |
+
def optimize_image_for_api(self, image: Image.Image) -> Image.Image:
|
32 |
+
"""API 호출을 위한 이미지 최적화"""
|
33 |
+
# 이미지 크기 최적화 (토큰 사용량 감소)
|
34 |
+
max_dimension = 1024 # 더 작은 크기로 제한
|
35 |
+
|
36 |
+
# 현재 이미지 크기 확인
|
37 |
+
width, height = image.size
|
38 |
+
|
39 |
+
# 큰 이미지일 경우 리사이즈
|
40 |
+
if width > max_dimension or height > max_dimension:
|
41 |
+
image.thumbnail((max_dimension, max_dimension), Image.Resampling.LANCZOS)
|
42 |
+
|
43 |
+
# RGBA를 RGB로 변환 (파일 크기 감소)
|
44 |
if image.mode == 'RGBA':
|
|
|
45 |
background = Image.new('RGB', image.size, (255, 255, 255))
|
46 |
background.paste(image, mask=image.split()[-1])
|
47 |
image = background
|
48 |
+
elif image.mode != 'RGB':
|
49 |
+
image = image.convert('RGB')
|
50 |
|
51 |
+
return image
|
52 |
+
def encode_image_to_base64(self, image: Image.Image) -> str:
|
53 |
+
"""이미지를 base64로 인코딩 (최적화된 버전)"""
|
54 |
+
# 이미지 최적화
|
55 |
+
image = self.optimize_image_for_api(image)
|
56 |
+
|
57 |
+
buffer = io.BytesIO()
|
58 |
+
# JPEG 형식으로 저장하여 파일 크기 최적화 (품질 80으로 낮춤)
|
59 |
+
image.save(buffer, format='JPEG', quality=80, optimize=True)
|
60 |
image_bytes = buffer.getvalue()
|
61 |
+
|
62 |
+
# 파일 크기 확인
|
63 |
+
size_mb = len(image_bytes) / (1024 * 1024)
|
64 |
+
if size_mb > 3: # 3MB 초과 시 추가 최적화
|
65 |
+
buffer = io.BytesIO()
|
66 |
+
image.save(buffer, format='JPEG', quality=60, optimize=True)
|
67 |
+
image_bytes = buffer.getvalue()
|
68 |
+
|
69 |
return base64.b64encode(image_bytes).decode('utf-8')
|
70 |
|
71 |
+
def call_gemini_api_with_retry(self, image_base64: str, max_retries: int = 3, initial_delay: float = 2.0) -> str:
|
72 |
+
"""재시도 로직이 포함된 Gemini API 호출"""
|
73 |
if not self.api_key:
|
74 |
return "❌ 먼저 API 키를 설정해주세요."
|
75 |
|
|
|
128 |
]
|
129 |
}
|
130 |
|
131 |
+
for attempt in range(max_retries):
|
132 |
+
try:
|
133 |
+
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
|
135 |
+
if response.status_code == 401:
|
136 |
+
return "❌ API 키가 유효하지 않습니다. Google AI Studio에서 발급받은 올바른 API 키를 입력해주세요."
|
137 |
+
elif response.status_code == 403:
|
138 |
+
return "❌ API 접근 권한이 없습니다. Gemini API가 활성화되어 있는지 확인해주세요."
|
139 |
+
elif response.status_code == 429:
|
140 |
+
# 429 에러 시 재시도 로직
|
141 |
+
if attempt < max_retries - 1:
|
142 |
+
delay = initial_delay * (2 ** attempt) + random.uniform(0.5, 1.5) # 지수 백오프 + 랜덤 지터
|
143 |
+
return f"⏳ API 호출 한도를 초과했습니다. {delay:.1f}초 후 자동으로 재시도합니다... (시도 {attempt + 1}/{max_retries})"
|
144 |
+
else:
|
145 |
+
return """❌ API 호출 한도를 초과했습니다.
|
146 |
+
|
147 |
+
📌 해결 방법:
|
148 |
+
1. 잠시 기다린 후 다시 시도 (1-2분 권장)
|
149 |
+
2. Google AI Studio에서 할당량 확인
|
150 |
+
3. 유료 계정으로 업그레이드 고려
|
151 |
+
4. 이미지 크기를 줄여서 재시도
|
152 |
+
|
153 |
+
💡 팁: 높은 해상도의 이미지는 더 많은 토큰을 사용합니다."""
|
154 |
+
|
155 |
+
response.raise_for_status()
|
156 |
+
|
157 |
+
result = response.json()
|
158 |
+
|
159 |
+
if "candidates" in result and len(result["candidates"]) > 0:
|
160 |
+
content = result["candidates"][0]["content"]["parts"][0]["text"]
|
161 |
+
return content.strip()
|
162 |
+
elif "error" in result:
|
163 |
+
error_msg = result['error'].get('message', '알 수 없는 오류')
|
164 |
+
if "quota" in error_msg.lower() or "limit" in error_msg.lower():
|
165 |
+
if attempt < max_retries - 1:
|
166 |
+
delay = initial_delay * (2 ** attempt) + random.uniform(0.5, 1.5)
|
167 |
+
time.sleep(delay)
|
168 |
+
continue
|
169 |
+
return f"❌ API 오류: {error_msg}"
|
170 |
+
else:
|
171 |
+
return "❌ 텍스트를 추출할 수 없습니다. 이미지에 한국어 텍스트가 포함되어 있는지 확인해주세요."
|
172 |
+
|
173 |
+
except requests.exceptions.RequestException as e:
|
174 |
+
if "429" in str(e) and attempt < max_retries - 1:
|
175 |
+
delay = initial_delay * (2 ** attempt) + random.uniform(0.5, 1.5)
|
176 |
+
time.sleep(delay)
|
177 |
+
continue
|
178 |
+
return f"❌ API 호출 오류: {str(e)}"
|
179 |
+
except json.JSONDecodeError:
|
180 |
+
return "❌ API 응답 파싱 오류가 발생했습니다."
|
181 |
+
except KeyError as e:
|
182 |
+
return f"❌ 예상치 못한 API 응답 형식: {str(e)}"
|
183 |
+
except Exception as e:
|
184 |
+
return f"❌ 알 수 없는 오류: {str(e)}"
|
185 |
+
|
186 |
+
return "❌ 최대 재시도 횟수를 초과했습니다. 잠시 후 다시 시도해주세요."
|
187 |
|
188 |
def call_vertex_ai_api(self, image_base64: str) -> str:
|
189 |
"""Vertex AI API 호출 (서비스 계정 키 사용)"""
|
|
|
270 |
return image, auth_result
|
271 |
|
272 |
try:
|
273 |
+
# 이미지 최적화 (토큰 사용량 감소를 위해)
|
274 |
+
image = self.optimize_image_for_api(image)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
|
276 |
# 이미지를 base64로 인코딩
|
277 |
image_base64 = self.encode_image_to_base64(image)
|
278 |
|
279 |
# API 타입에 따라 호출
|
280 |
if api_type == "Google AI Studio":
|
281 |
+
extracted_text = self.call_gemini_api_with_retry(image_base64)
|
282 |
else:
|
283 |
extracted_text = self.call_vertex_ai_api(image_base64)
|
284 |
|
|
|
360 |
)
|
361 |
|
362 |
# API 설정 가이드
|
363 |
+
with gr.Accordion("📖 API 설정 가이드 및 할당량 정보", open=False):
|
364 |
gr.Markdown("""
|
365 |
### Google AI Studio API (권장)
|
366 |
1. [Google AI Studio](https://aistudio.google.com/)에 접속
|
|
|
368 |
3. API 키 생성 및 복사
|
369 |
4. 위의 "API 키" 필드에 붙여넣기
|
370 |
|
371 |
+
**📊 무료 할당량 (Google AI Studio):**
|
372 |
+
- 분당 15회 요청
|
373 |
+
- 일일 1,500회 요청
|
374 |
+
- 분당 100만 토큰
|
375 |
+
- 일일 5천만 토큰
|
376 |
+
|
377 |
### Vertex AI API (고급 사용자용)
|
378 |
1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트 생성
|
379 |
2. Vertex AI API 활성화
|
|
|
381 |
4. `gcloud auth application-default login` 또는 Access Token 발급
|
382 |
5. API 키와 프로젝트 ID 입력
|
383 |
|
384 |
+
### ⚠️ 할당량 초과 시 해결 방법
|
385 |
+
1. **잠시 대기**: 1-2분 후 다시 시도
|
386 |
+
2. **이미지 최적화**: 더 작은 크기의 이미지 사용
|
387 |
+
3. **사용량 분산**: 여러 번 나누어서 처리
|
388 |
+
4. **유료 계정**: Google Cloud 유료 계정으로 업그레이드
|
389 |
+
|
390 |
+
### 💡 토큰 절약 팁
|
391 |
+
- 이미지 해상도: 1024x1024 이하 권장
|
392 |
+
- 파일 형식: JPEG 사용 (PNG보다 작음)
|
393 |
+
- 불필요한 배경 제거
|
394 |
+
- 텍스트 영역만 크롭하여 업로드
|
395 |
""", elem_classes="warning-box")
|
396 |
|
397 |
+
# 할당량 상태 표시
|
398 |
+
with gr.Row():
|
399 |
+
gr.Markdown("""
|
400 |
+
### 📊 현재 상태
|
401 |
+
|
402 |
+
**무료 할당량 (Google AI Studio):**
|
403 |
+
- ⏱️ 분당 15회 요청 제한
|
404 |
+
- 📅 일일 1,500회 요청 제한
|
405 |
+
- 🔢 고해상도 이미지는 더 많은 토큰 사용
|
406 |
+
|
407 |
+
**💡 할당량 절약 팁:**
|
408 |
+
- 이미지 크기를 1024x1024 이하로 유지
|
409 |
+
- 텍스트가 있는 부분만 크롭하여 업로드
|
410 |
+
- 연속적인 요청 간 1-2초 간격 유지
|
411 |
+
""", elem_classes="info-box")
|
412 |
gr.Markdown("## 📤 이미지 업로드 및 텍스트 추출")
|
413 |
|
414 |
with gr.Row():
|