Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,76 +1,17 @@
|
|
1 |
-
|
2 |
-
with gr.Row():
|
3 |
-
with gr.Column():
|
4 |
-
gr.Markdown("### 🔑 임시 API 설정")
|
5 |
-
api_key_input = gr.Textbox(
|
6 |
-
label="Google Gemini API Key",
|
7 |
-
type="password",
|
8 |
-
placeholder="임시 사용을 위해 API 키를 입력하세요...",
|
9 |
-
info="⚠️ 보안을 위해 환경 변수 설정을 권장합니다.",
|
10 |
-
container=True
|
11 |
-
)def apply_changes_only(original_text, change_request_text, api_key_input):
|
12 |
-
"""원본 텍스트에 변경사항만 적용 (고도화된 버전)"""
|
13 |
-
# 환경 변수 우선 확인 및 API 설정
|
14 |
-
if not model:
|
15 |
-
config_result = configure_api(api_key_input)
|
16 |
-
if "❌" in config_result:
|
17 |
-
return config_result, ""
|
18 |
-
|
19 |
-
if not original_text.strip():
|
20 |
-
return "먼저 이미지에서 텍스트를 추출해주세요.", ""
|
21 |
-
|
22 |
-
if not change_request_text.strip():
|
23 |
-
return "변경사항 요청을 입력해주세요.", ""
|
24 |
-
|
25 |
-
try:
|
26 |
-
# 스마트한 변경사항 분석
|
27 |
-
change_type = analyze_change_request(change_request_text)
|
28 |
-
|
29 |
-
# 변경사항 적용을 위한 고도화된 프롬프트
|
30 |
-
prompt = f"""
|
31 |
-
당신은 전문 텍스트 편집 AI입니다. 다음 원본 텍스트에서 사용자가 요청한 변경사항을 정확하게 적용해주세요.
|
32 |
-
|
33 |
-
**원본 텍스트:**
|
34 |
-
{original_text}
|
35 |
-
|
36 |
-
**변경사항 요청:**
|
37 |
-
{change_request_text}
|
38 |
-
|
39 |
-
**변경 유형 분석:** {change_type}
|
40 |
-
|
41 |
-
**전문 편집 규칙:**
|
42 |
-
1. 요청된 변경사항만 정확히 적용하세요
|
43 |
-
2. 나머지 텍스트는 원본과 완전히 동일하게 유지하세요
|
44 |
-
3. 텍스트의 형식, 구조, 줄바꿈을 원본과 동일하게 보존하세요
|
45 |
-
4. 한국어 맞춤법과 띄어쓰기를 정확히 적용하세요
|
46 |
-
5. 숫자, 날짜, 고유명사는 특히 신중하게 처리하세요
|
47 |
-
6. 변경된 부분이 문맥상 자연스러운지 확인하세요
|
48 |
-
7. 오직 변경된 텍스트만 출력하고 설명이나 주석은 포함하지 마세요
|
49 |
-
|
50 |
-
**품질 보증:**
|
51 |
-
- 변경 전후의 텍스트 길이와 구조가 적절한지 검토
|
52 |
-
- 한국어 문법과 자연스러운 표현 확인
|
53 |
-
- 변경사항이 완전히 반영되었는지 검증
|
54 |
-
"""
|
55 |
-
7. 오직 변경된 텍스트만 출력하고 설명이나 주석은 포함하지 마def clear_all_tab2():
|
56 |
-
"""탭2의 모든 입력 초기화"""
|
57 |
-
return None, "", "", "", ""import gradio as gr
|
58 |
import google.generativeai as genai
|
59 |
import base64
|
60 |
import io
|
61 |
-
from PIL import Image, ImageEnhance
|
62 |
import difflib
|
63 |
-
import re
|
64 |
import os
|
65 |
-
import asyncio
|
66 |
-
import threading
|
67 |
import time
|
68 |
from datetime import datetime
|
69 |
|
70 |
-
# 전역
|
71 |
api_key = None
|
72 |
model = None
|
73 |
-
processing_cache = {}
|
74 |
|
75 |
# 환경 변수에서 API 키 확인
|
76 |
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
|
@@ -88,17 +29,38 @@ def initialize_api():
|
|
88 |
return False, f"❌ 환경 변수 API 키 설정 실패: {str(e)}"
|
89 |
return False, ""
|
90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
def preprocess_image(image):
|
92 |
"""이미지 전처리로 OCR 정확도 향상"""
|
93 |
try:
|
94 |
if image is None:
|
95 |
return None
|
96 |
|
97 |
-
#
|
98 |
-
if not isinstance(image, Image.Image):
|
99 |
-
image = Image.open(image) if isinstance(image, str) else image
|
100 |
-
|
101 |
-
# 이미지 크기 최적화 (너무 크면 축소, 너무 작으면 확대)
|
102 |
width, height = image.size
|
103 |
if width * height > 4000000: # 4MP 이상이면 축소
|
104 |
ratio = (4000000 / (width * height)) ** 0.5
|
@@ -128,34 +90,8 @@ def preprocess_image(image):
|
|
128 |
print(f"이미지 전처리 오류: {e}")
|
129 |
return image
|
130 |
|
131 |
-
def configure_api(api_key_input):
|
132 |
-
"""Gemini API 설정 (수동 입력 또는 환경 변수)"""
|
133 |
-
global api_key, model
|
134 |
-
|
135 |
-
# 환경 변수 우선 확인
|
136 |
-
if GEMINI_API_KEY and not api_key_input.strip():
|
137 |
-
is_initialized, message = initialize_api()
|
138 |
-
if is_initialized:
|
139 |
-
return message
|
140 |
-
|
141 |
-
# 수동 입력 API 키 처리
|
142 |
-
try:
|
143 |
-
if not api_key_input.strip():
|
144 |
-
if not GEMINI_API_KEY:
|
145 |
-
return "❌ API 키를 입력하거나 환경 변수 'GEMINI_API_KEY'를 설정해주세요."
|
146 |
-
else:
|
147 |
-
# 환경 변수가 있지만 실패한 경우
|
148 |
-
return "❌ 환경 변수의 API 키가 유효하지 않습니다. 직접 입력해주세요."
|
149 |
-
|
150 |
-
api_key = api_key_input.strip()
|
151 |
-
genai.configure(api_key=api_key)
|
152 |
-
model = genai.GenerativeModel('gemini-2.5-flash')
|
153 |
-
return "✅ 수동 입력된 API 키로 설정이 완료되었습니다!"
|
154 |
-
except Exception as e:
|
155 |
-
return f"❌ API 설정 실패: {str(e)}"
|
156 |
-
|
157 |
def extract_text_from_image(image):
|
158 |
-
"""이미지에서 한국어 텍스트 추출
|
159 |
global model, processing_cache
|
160 |
|
161 |
if model is None:
|
@@ -174,32 +110,21 @@ def extract_text_from_image(image):
|
|
174 |
processed_image = preprocess_image(image)
|
175 |
|
176 |
# PIL Image를 bytes로 변환
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
else:
|
181 |
-
buffer = io.BytesIO()
|
182 |
-
processed_image.save(buffer, format='PNG', optimize=True, quality=95)
|
183 |
-
image_data = buffer.getvalue()
|
184 |
|
185 |
-
#
|
186 |
prompt = """
|
187 |
이 이미지에 있는 모든 한국어 텍스트를 정확하게 추출해주세요.
|
188 |
|
189 |
-
|
190 |
1. 텍스트의 읽기 순서와 공간적 배치를 정확히 유지하세요
|
191 |
2. 줄바꿈, 들여쓰기, 공백을 원본과 동일하게 보존하세요
|
192 |
3. 한글, 영어, 숫자, 특수문자를 모두 포함하세요
|
193 |
4. 표나 목록의 구조를 유지하세요
|
194 |
5. 흐릿하거나 불분명한 글자는 [?]로 표시하세요
|
195 |
-
6.
|
196 |
-
7. 작은 글씨나 워터마크도 놓치지 마세요
|
197 |
-
8. 텍스트만 출력하고 추가 설명은 하지 마세요
|
198 |
-
|
199 |
-
품질 검증:
|
200 |
-
- 문맥상 어색하지 않은지 확인
|
201 |
-
- 일반적인 한국어 맞춤법 규칙 적용
|
202 |
-
- 누락된 글자나 중복된 글자 없는지 검토
|
203 |
"""
|
204 |
|
205 |
# 이미지를 base64로 인코딩
|
@@ -219,9 +144,6 @@ def extract_text_from_image(image):
|
|
219 |
|
220 |
extracted_text = response.text.strip()
|
221 |
|
222 |
-
# 후처리: 일반적인 OCR 오류 수정
|
223 |
-
extracted_text = post_process_text(extracted_text)
|
224 |
-
|
225 |
# 캐시에 저장
|
226 |
processing_cache[image_hash] = extracted_text
|
227 |
|
@@ -230,45 +152,13 @@ def extract_text_from_image(image):
|
|
230 |
except Exception as e:
|
231 |
if attempt == max_retries - 1:
|
232 |
raise e
|
233 |
-
time.sleep(1)
|
234 |
|
235 |
except Exception as e:
|
236 |
return f"❌ 텍스트 추출 실패: {str(e)}"
|
237 |
|
238 |
-
def post_process_text(text):
|
239 |
-
"""OCR 결과 후처리"""
|
240 |
-
if not text:
|
241 |
-
return text
|
242 |
-
|
243 |
-
# 일반적인 OCR 오류 패턴 수정
|
244 |
-
corrections = {
|
245 |
-
'ㅇ': 'o', # 한글 ㅇ와 영어 o 혼동
|
246 |
-
'ㅣ': '|', # 한글 ㅣ와 파이프 혼동
|
247 |
-
'1': '1', # 전각 숫자를 반각으로
|
248 |
-
'2': '2',
|
249 |
-
'3': '3',
|
250 |
-
'4': '4',
|
251 |
-
'5': '5',
|
252 |
-
'6': '6',
|
253 |
-
'7': '7',
|
254 |
-
'8': '8',
|
255 |
-
'9': '9',
|
256 |
-
'0': '0',
|
257 |
-
}
|
258 |
-
|
259 |
-
# 패턴별 수정 적용 (너무 공격적이지 않게)
|
260 |
-
for wrong, correct in corrections.items():
|
261 |
-
# 영어/숫자 컨텍스트에서만 수정
|
262 |
-
text = re.sub(f'(?<=[a-zA-Z0-9]){re.escape(wrong)}(?=[a-zA-Z0-9])', correct, text)
|
263 |
-
|
264 |
-
# 연속된 공백 정리
|
265 |
-
text = re.sub(r'\s+', ' ', text)
|
266 |
-
text = re.sub(r'\n\s*\n', '\n\n', text) # 빈 줄 정리
|
267 |
-
|
268 |
-
return text.strip()
|
269 |
-
|
270 |
def compare_texts(text1, text2):
|
271 |
-
"""두 텍스트를 비교하고 차이점을 HTML로 표시
|
272 |
if not text1 or not text2:
|
273 |
return "비교할 텍스트가 없습니다."
|
274 |
|
@@ -280,190 +170,86 @@ def compare_texts(text1, text2):
|
|
280 |
total_lines1 = len(lines1)
|
281 |
total_lines2 = len(lines2)
|
282 |
|
283 |
-
# 차이점 분석
|
284 |
-
differ = difflib.unified_diff(lines1, lines2, lineterm='', n=0)
|
285 |
-
added_lines = 0
|
286 |
-
removed_lines = 0
|
287 |
-
|
288 |
-
for line in differ:
|
289 |
-
if line.startswith('+') and not line.startswith('+++'):
|
290 |
-
added_lines += 1
|
291 |
-
elif line.startswith('-') and not line.startswith('---'):
|
292 |
-
removed_lines += 1
|
293 |
-
|
294 |
# 유사도 계산
|
295 |
similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
|
296 |
similarity_percent = round(similarity * 100, 1)
|
297 |
|
298 |
-
# 단어 레벨 차이점 분석
|
299 |
-
word_changes = analyze_word_changes(text1, text2)
|
300 |
-
|
301 |
# 현재 시간
|
302 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
303 |
|
304 |
html_result = f"""
|
305 |
<div style="font-family: 'Noto Sans KR', sans-serif; line-height: 1.6;">
|
306 |
-
<!-- 분석 헤더 -->
|
307 |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
|
308 |
-
<h2 style="margin: 0 0 10px 0;
|
309 |
-
<span style="font-size: 1.5em; margin-right: 10px;">📊</span>
|
310 |
-
텍스트 비교 분석 결과
|
311 |
-
</h2>
|
312 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px;">
|
313 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
314 |
-
<div style="font-size: 0.9em;
|
315 |
<div style="font-size: 1.5em; font-weight: bold;">{similarity_percent}%</div>
|
316 |
</div>
|
317 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
318 |
-
<div style="font-size: 0.9em;
|
319 |
-
<div style="font-size: 1.5em; font-weight: bold;
|
320 |
</div>
|
321 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
322 |
-
<div style="font-size: 0.9em;
|
323 |
-
<div style="font-size: 1.5em; font-weight: bold;
|
324 |
</div>
|
325 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
326 |
-
<div style="font-size: 0.9em;
|
327 |
<div style="font-size: 1em; font-weight: bold;">{timestamp}</div>
|
328 |
</div>
|
329 |
</div>
|
330 |
</div>
|
331 |
|
332 |
-
|
333 |
-
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
334 |
<div style="flex: 1;">
|
335 |
-
<div style="background:
|
336 |
-
|
337 |
-
원본 텍스트 ({total_lines1}줄)
|
338 |
</div>
|
339 |
-
<div style="background-color: #fff5f5; border: 1px solid #fed7d7; padding: 15px; border-radius: 0 0 8px 8px; white-space: pre-wrap; font-family:
|
340 |
"""
|
341 |
|
342 |
-
# 차이점이 있는 라인 찾기
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
|
|
|
|
|
|
|
|
|
|
349 |
else:
|
350 |
-
html_result += f'
|
351 |
|
352 |
-
html_result += """
|
353 |
</div>
|
354 |
</div>
|
355 |
<div style="flex: 1;">
|
356 |
-
<div style="background:
|
357 |
-
|
358 |
-
수정된 텍스트 (""" + f"{total_lines2}줄)"
|
359 |
-
|
360 |
-
html_result += """
|
361 |
</div>
|
362 |
-
<div style="background-color: #f0fdf4; border: 1px solid #bbf7d0; padding: 15px; border-radius: 0 0 8px 8px; white-space: pre-wrap; font-family:
|
363 |
"""
|
364 |
|
365 |
-
# 수정된 텍스트 표시 (
|
366 |
-
for
|
367 |
-
if
|
368 |
-
html_result += f'<
|
369 |
else:
|
370 |
-
html_result += f'
|
371 |
|
372 |
html_result += """
|
373 |
</div>
|
374 |
</div>
|
375 |
</div>
|
376 |
-
|
377 |
-
<!-- 상세 변경 사항 -->
|
378 |
-
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px;">
|
379 |
-
<h3 style="color: #1e293b; margin: 0 0 15px 0; display: flex; align-items: center;">
|
380 |
-
<span style="margin-right: 8px;">🔍</span>
|
381 |
-
상세 변경 사항
|
382 |
-
</h3>
|
383 |
-
<div style="display: grid; gap: 10px;">
|
384 |
-
"""
|
385 |
-
|
386 |
-
# 상세 변경사항 표시
|
387 |
-
detailed_changes = get_detailed_changes(lines1, lines2)
|
388 |
-
if detailed_changes:
|
389 |
-
for change in detailed_changes[:10]: # 최대 10개까지만 표시
|
390 |
-
html_result += f"""
|
391 |
-
<div style="background: white; border-radius: 8px; padding: 12px; border-left: 4px solid #3b82f6;">
|
392 |
-
<div style="font-weight: 600; color: #1e40af; margin-bottom: 4px;">{change['type']}</div>
|
393 |
-
<div style="font-size: 14px; color: #64748b;">{change['description']}</div>
|
394 |
-
</div>
|
395 |
-
"""
|
396 |
-
else:
|
397 |
-
html_result += '<div style="text-align: center; color: #64748b; padding: 20px;">변경사항이 없습니다. ✅</div>'
|
398 |
-
|
399 |
-
html_result += """
|
400 |
-
</div>
|
401 |
-
</div>
|
402 |
</div>
|
403 |
"""
|
404 |
|
405 |
return html_result
|
406 |
|
407 |
-
def find_detailed_differences(lines1, lines2):
|
408 |
-
"""라인별 상세 차이점 찾기"""
|
409 |
-
diff_lines1 = set()
|
410 |
-
diff_lines2 = set()
|
411 |
-
|
412 |
-
# difflib을 사용한 정확한 차이점 분석
|
413 |
-
opcodes = difflib.SequenceMatcher(None, lines1, lines2).get_opcodes()
|
414 |
-
|
415 |
-
for op, i1, i2, j1, j2 in opcodes:
|
416 |
-
if op == 'delete':
|
417 |
-
diff_lines1.update(range(i1, i2))
|
418 |
-
elif op == 'insert':
|
419 |
-
diff_lines2.update(range(j1, j2))
|
420 |
-
elif op == 'replace':
|
421 |
-
diff_lines1.update(range(i1, i2))
|
422 |
-
diff_lines2.update(range(j1, j2))
|
423 |
-
|
424 |
-
return diff_lines1, diff_lines2
|
425 |
-
|
426 |
-
def analyze_word_changes(text1, text2):
|
427 |
-
"""단어 레벨 변경사항 분석"""
|
428 |
-
words1 = text1.split()
|
429 |
-
words2 = text2.split()
|
430 |
-
|
431 |
-
opcodes = difflib.SequenceMatcher(None, words1, words2).get_opcodes()
|
432 |
-
changes = 0
|
433 |
-
|
434 |
-
for op, i1, i2, j1, j2 in opcodes:
|
435 |
-
if op in ['delete', 'insert', 'replace']:
|
436 |
-
changes += max(i2-i1, j2-j1)
|
437 |
-
|
438 |
-
return changes
|
439 |
-
|
440 |
-
def get_detailed_changes(lines1, lines2):
|
441 |
-
"""상세 변경사항 리스트 생성"""
|
442 |
-
changes = []
|
443 |
-
opcodes = difflib.SequenceMatcher(None, lines1, lines2).get_opcodes()
|
444 |
-
|
445 |
-
for op, i1, i2, j1, j2 in opcodes:
|
446 |
-
if op == 'delete':
|
447 |
-
for i in range(i1, i2):
|
448 |
-
changes.append({
|
449 |
-
'type': '🗑️ 삭제됨',
|
450 |
-
'description': f'라인 {i+1}: "{lines1[i][:50]}..."' if len(lines1[i]) > 50 else f'라인 {i+1}: "{lines1[i]}"'
|
451 |
-
})
|
452 |
-
elif op == 'insert':
|
453 |
-
for j in range(j1, j2):
|
454 |
-
changes.append({
|
455 |
-
'type': '➕ 추가됨',
|
456 |
-
'description': f'라인 {j+1}: "{lines2[j][:50]}..."' if len(lines2[j]) > 50 else f'라인 {j+1}: "{lines2[j]}"'
|
457 |
-
})
|
458 |
-
elif op == 'replace':
|
459 |
-
for i, j in zip(range(i1, i2), range(j1, j2)):
|
460 |
-
changes.append({
|
461 |
-
'type': '✏️ 수정됨',
|
462 |
-
'description': f'라인 {i+1}: "{lines1[i][:30]}..." → "{lines2[j][:30]}..."'
|
463 |
-
})
|
464 |
-
|
465 |
-
return changes
|
466 |
-
|
467 |
def process_images(image1, image2, api_key_input):
|
468 |
"""두 이미지를 처리하고 비교 결과 반환"""
|
469 |
# 환경 변수 우선 확인 및 API 설정
|
@@ -499,117 +285,61 @@ def extract_text_only(image, api_key_input):
|
|
499 |
|
500 |
return extracted_text
|
501 |
|
502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
503 |
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
508 |
"""
|
509 |
|
510 |
-
|
511 |
-
|
512 |
-
best_result = None
|
513 |
-
|
514 |
-
for attempt in range(max_retries):
|
515 |
-
try:
|
516 |
-
response = model.generate_content(prompt)
|
517 |
-
modified_text = response.text.strip()
|
518 |
-
|
519 |
-
# 품질 검증
|
520 |
-
quality_score = validate_text_quality(original_text, modified_text, change_request_text)
|
521 |
-
|
522 |
-
if quality_score > 0.8 or attempt == max_retries - 1:
|
523 |
-
best_result = modified_text
|
524 |
-
break
|
525 |
-
|
526 |
-
except Exception as e:
|
527 |
-
if attempt == max_retries - 1:
|
528 |
-
raise e
|
529 |
-
time.sleep(1)
|
530 |
-
|
531 |
-
# 최종 후처리
|
532 |
-
final_text = post_process_modified_text(best_result, original_text)
|
533 |
|
534 |
# 비교 결과 생성
|
535 |
-
if original_text and
|
536 |
-
comparison = compare_texts(original_text,
|
537 |
else:
|
538 |
comparison = "텍스트 비교를 위해 원본 텍스트와 변경된 텍스트가 모두 필요합니다."
|
539 |
|
540 |
-
return
|
541 |
|
542 |
except Exception as e:
|
543 |
return f"❌ 변경사항 적용 실패: {str(e)}", ""
|
544 |
|
545 |
-
def analyze_change_request(change_request):
|
546 |
-
"""변경사항 요청의 유형을 분석"""
|
547 |
-
request_lower = change_request.lower()
|
548 |
-
|
549 |
-
if any(word in request_lower for word in ['변경', '바꿔', '수정', '교체']):
|
550 |
-
return "텍스트 교체"
|
551 |
-
elif any(word in request_lower for word in ['추가', '넣어', '삽입']):
|
552 |
-
return "텍스트 추가"
|
553 |
-
elif any(word in request_lower for word in ['삭제', '제거', '빼']):
|
554 |
-
return "텍스트 삭제"
|
555 |
-
elif any(word in request_lower for word in ['번역', '영어로', '한국어로']):
|
556 |
-
return "언어 번역"
|
557 |
-
elif any(word in request_lower for word in ['형식', '포맷', '정렬']):
|
558 |
-
return "형식 변경"
|
559 |
-
else:
|
560 |
-
return "일반적인 편집"
|
561 |
-
|
562 |
-
def validate_text_quality(original, modified, request):
|
563 |
-
"""텍스트 변경 품질을 검증"""
|
564 |
-
if not modified or len(modified.strip()) < 10:
|
565 |
-
return 0.0
|
566 |
-
|
567 |
-
# 기본 품질 점수
|
568 |
-
score = 0.5
|
569 |
-
|
570 |
-
# 길이 적절성 검사
|
571 |
-
length_ratio = len(modified) / max(len(original), 1)
|
572 |
-
if 0.5 <= length_ratio <= 2.0:
|
573 |
-
score += 0.2
|
574 |
-
|
575 |
-
# 구조 유사성 검사
|
576 |
-
orig_lines = len(original.splitlines())
|
577 |
-
mod_lines = len(modified.splitlines())
|
578 |
-
if abs(orig_lines - mod_lines) <= max(2, orig_lines * 0.1):
|
579 |
-
score += 0.2
|
580 |
-
|
581 |
-
# 한국어 포함 여부
|
582 |
-
if re.search(r'[가-힣]', modified):
|
583 |
-
score += 0.1
|
584 |
-
|
585 |
-
return min(score, 1.0)
|
586 |
-
|
587 |
-
def post_process_modified_text(text, original):
|
588 |
-
"""수정된 텍스트 후처리"""
|
589 |
-
if not text:
|
590 |
-
return text
|
591 |
-
|
592 |
-
# 불필요한 설명 제거
|
593 |
-
lines = text.split('\n')
|
594 |
-
cleaned_lines = []
|
595 |
-
|
596 |
-
for line in lines:
|
597 |
-
# 설명성 문장 제거
|
598 |
-
if not any(phrase in line.lower() for phrase in ['변경했습니다', '수정했습니다', '다음과 같습니다', '결과입니다']):
|
599 |
-
cleaned_lines.append(line)
|
600 |
-
|
601 |
-
result = '\n'.join(cleaned_lines)
|
602 |
-
|
603 |
-
# 공백 정리
|
604 |
-
result = re.sub(r'\n\s*\n\s*\n', '\n\n', result)
|
605 |
-
result = result.strip()
|
606 |
-
|
607 |
-
return result
|
608 |
-
|
609 |
def clear_all():
|
610 |
"""모든 입력 초기화"""
|
611 |
return None, None, "", "", ""
|
612 |
|
|
|
|
|
|
|
|
|
613 |
# Gradio 인터페이스 생성
|
614 |
def create_interface():
|
615 |
# 앱 시작 시 환경 변수 확인
|
@@ -622,28 +352,6 @@ def create_interface():
|
|
622 |
.gradio-container {
|
623 |
font-family: 'Noto Sans KR', sans-serif !important;
|
624 |
}
|
625 |
-
.gr-button {
|
626 |
-
font-weight: 600;
|
627 |
-
}
|
628 |
-
.gr-form {
|
629 |
-
border-radius: 12px;
|
630 |
-
}
|
631 |
-
.env-status {
|
632 |
-
padding: 10px;
|
633 |
-
border-radius: 8px;
|
634 |
-
margin-bottom: 15px;
|
635 |
-
font-weight: 500;
|
636 |
-
}
|
637 |
-
.env-success {
|
638 |
-
background-color: #d1fae5;
|
639 |
-
color: #065f46;
|
640 |
-
border: 1px solid #a7f3d0;
|
641 |
-
}
|
642 |
-
.env-warning {
|
643 |
-
background-color: #fef3c7;
|
644 |
-
color: #92400e;
|
645 |
-
border: 1px solid #fcd34d;
|
646 |
-
}
|
647 |
"""
|
648 |
) as demo:
|
649 |
|
@@ -658,21 +366,12 @@ def create_interface():
|
|
658 |
- **⚡ 캐싱 시스템**: 동일 이미지 재처리 방지로 속도 향상
|
659 |
- **🔍 정밀 비교 분석**: 라인별, 단어별 상세 차이점 분석
|
660 |
- **📊 실시간 통계**: 유사도, 변경사항 수치화
|
661 |
-
- **✨ 품질 검증**: 자동 후처리 및 품질 보증 시스템
|
662 |
-
|
663 |
-
### 📋 사용 방법:
|
664 |
-
1. **API 키 설정**: 아래 방법 중 하나를 선택하세요
|
665 |
-
- **🔒 권장**: 허깅페이스 환경설정에서 `GEMINI_API_KEY` 변수 설정
|
666 |
-
- **⚡ 대안**: 아래 입력창에 직접 API 키 입력
|
667 |
-
2. **탭 선택**: 용도에 맞는 탭을 선택하세요
|
668 |
-
3. **이미지/텍스트 처리**: 고품질 결과를 위해 선명한 이미지 사용
|
669 |
-
4. **결과 분석**: 상세한 분석 리포트 확인
|
670 |
""")
|
671 |
|
672 |
-
# 환경 변수 상태 표시
|
673 |
if env_initialized:
|
674 |
gr.HTML(f"""
|
675 |
-
<div style="background: linear-gradient(135deg, #10b981, #059669); color: white; padding: 15px; border-radius: 12px; margin: 15px 0;
|
676 |
<div style="display: flex; align-items: center; font-weight: 600; font-size: 1.1em;">
|
677 |
<span style="font-size: 1.5em; margin-right: 10px;">✅</span>
|
678 |
{env_message}
|
@@ -685,25 +384,31 @@ def create_interface():
|
|
685 |
api_key_input = gr.Textbox(visible=False)
|
686 |
else:
|
687 |
gr.HTML("""
|
688 |
-
<div style="background: linear-gradient(135deg, #f59e0b, #d97706); color: white; padding: 20px; border-radius: 12px; margin: 15px 0;
|
689 |
-
<div style="font-weight: 600; font-size: 1.1em; margin-bottom: 10px;
|
690 |
-
|
691 |
-
환경 변수 설정 필요
|
692 |
</div>
|
693 |
<div style="background: rgba(255,255,255,0.2); padding: 15px; border-radius: 8px; margin: 10px 0;">
|
694 |
<div style="font-weight: 600; margin-bottom: 8px;">🔧 허깅페이스 설정 방법:</div>
|
695 |
<div style="font-size: 0.9em; line-height: 1.6;">
|
696 |
<div>1️⃣ 스페이스 설정(Settings) → Variables 탭</div>
|
697 |
-
<div>2️⃣ Name:
|
698 |
<div>3️⃣ Value: 여러분의 Google Gemini API 키</div>
|
699 |
<div>4️⃣ 스페이스 재시작으로 적용</div>
|
700 |
</div>
|
701 |
</div>
|
702 |
-
<div style="font-size: 0.9em; opacity: 0.9;">
|
703 |
-
💡 환경 변수 설정 시 보안성과 편의성이 크게 향상됩니다!
|
704 |
-
</div>
|
705 |
</div>
|
706 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
707 |
|
708 |
with gr.Tabs():
|
709 |
with gr.TabItem("📷 이미지 비교"):
|
@@ -820,43 +525,6 @@ def create_interface():
|
|
820 |
size="lg"
|
821 |
)
|
822 |
|
823 |
-
with gr.Row():
|
824 |
-
analyze_btn = gr.Button(
|
825 |
-
"🚀 분석 시작",
|
826 |
-
variant="primary",
|
827 |
-
size="lg"
|
828 |
-
)
|
829 |
-
clear_btn = gr.Button(
|
830 |
-
"🗑️ 초기화",
|
831 |
-
variant="secondary",
|
832 |
-
size="lg"
|
833 |
-
)
|
834 |
-
|
835 |
-
with gr.Row():
|
836 |
-
with gr.Column():
|
837 |
-
gr.Markdown("### 📄 추출된 텍스트")
|
838 |
-
with gr.Row():
|
839 |
-
text1_output = gr.Textbox(
|
840 |
-
label="이미지 1 텍스트",
|
841 |
-
lines=10,
|
842 |
-
max_lines=15,
|
843 |
-
show_copy_button=True
|
844 |
-
)
|
845 |
-
text2_output = gr.Textbox(
|
846 |
-
label="이미지 2 텍스트",
|
847 |
-
lines=10,
|
848 |
-
max_lines=15,
|
849 |
-
show_copy_button=True
|
850 |
-
)
|
851 |
-
|
852 |
-
with gr.Row():
|
853 |
-
with gr.Column():
|
854 |
-
gr.Markdown("### 📊 텍스트 차이점 분석")
|
855 |
-
comparison_output = gr.HTML(
|
856 |
-
label="텍스트 비교 결과",
|
857 |
-
show_label=False
|
858 |
-
)
|
859 |
-
|
860 |
# 이벤트 연결 - 탭1 (이미지 비교)
|
861 |
analyze_btn.click(
|
862 |
fn=process_images,
|
@@ -892,26 +560,7 @@ def create_interface():
|
|
892 |
- **🖼️ 이미지 최적화**: 자동 전처리로 800x600 이상 해상도 권장
|
893 |
- **🔍 정확도 향상**: 선명하고 대비가 높은 이미지 사용
|
894 |
- **⚡ 성능 최적화**: 캐싱 시스템으로 동일 이미지 재처리 방지
|
895 |
-
- **📊 상세 분석**: 라인별, 단어별 변경사항 추적
|
896 |
- **🔒 보안 강화**: 환경 변수 사용으로 API 키 보안 강화
|
897 |
-
|
898 |
-
### 🎯 고급 기능:
|
899 |
-
- **스마트 OCR**: AI 기반 이미지 품질 향상 및 오류 자동 수정
|
900 |
-
- **실시간 통계**: 유사도, 변경 라인 수, 단어 변경 수 실시간 표시
|
901 |
-
- **품질 검증**: 변경사항 적용 후 자동 품질 검증 시스템
|
902 |
-
- **상세 리포트**: 변경사항 유형별 분류 및 상세 분석
|
903 |
-
|
904 |
-
### 🔧 기술 스펙:
|
905 |
-
- **AI 모델**: Google Gemini 2.5 Flash
|
906 |
-
- **지원 형식**: PNG, JPEG, WEBP, HEIC, HEIF
|
907 |
-
- **최대 해상도**: 자동 최적화 (4MP 기준)
|
908 |
-
- **처리 속도**: 캐싱으로 50% 향상
|
909 |
-
|
910 |
-
### ⚠️ 사용 주의사항:
|
911 |
-
- 한국어 텍스트 인식에 특화 최적화
|
912 |
-
- 대용량 이미지는 자동으로 최적 크기로 조정
|
913 |
-
- API 사용량에 따른 비용 발생 가능
|
914 |
-
- 개인정보가 포함된 이미지 처리 시 주의 필요
|
915 |
""")
|
916 |
|
917 |
return demo
|
|
|
1 |
+
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import google.generativeai as genai
|
3 |
import base64
|
4 |
import io
|
5 |
+
from PIL import Image, ImageEnhance
|
6 |
import difflib
|
|
|
7 |
import os
|
|
|
|
|
8 |
import time
|
9 |
from datetime import datetime
|
10 |
|
11 |
+
# 전역 변수
|
12 |
api_key = None
|
13 |
model = None
|
14 |
+
processing_cache = {}
|
15 |
|
16 |
# 환경 변수에서 API 키 확인
|
17 |
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
|
|
|
29 |
return False, f"❌ 환경 변수 API 키 설정 실패: {str(e)}"
|
30 |
return False, ""
|
31 |
|
32 |
+
def configure_api(api_key_input):
|
33 |
+
"""Gemini API 설정"""
|
34 |
+
global api_key, model
|
35 |
+
|
36 |
+
# 환경 변수 우선 확인
|
37 |
+
if GEMINI_API_KEY and not api_key_input.strip():
|
38 |
+
is_initialized, message = initialize_api()
|
39 |
+
if is_initialized:
|
40 |
+
return message
|
41 |
+
|
42 |
+
# 수동 입력 API 키 처리
|
43 |
+
try:
|
44 |
+
if not api_key_input.strip():
|
45 |
+
if not GEMINI_API_KEY:
|
46 |
+
return "❌ API 키를 입력하거나 환경 변수 'GEMINI_API_KEY'를 설정해주세요."
|
47 |
+
else:
|
48 |
+
return "❌ 환경 변수의 API 키가 유효하지 않습니다. 직접 입력해주세요."
|
49 |
+
|
50 |
+
api_key = api_key_input.strip()
|
51 |
+
genai.configure(api_key=api_key)
|
52 |
+
model = genai.GenerativeModel('gemini-2.5-flash')
|
53 |
+
return "✅ 수동 입력된 API 키로 설정이 완료되었습니다!"
|
54 |
+
except Exception as e:
|
55 |
+
return f"❌ API 설정 실패: {str(e)}"
|
56 |
+
|
57 |
def preprocess_image(image):
|
58 |
"""이미지 전처리로 OCR 정확도 향상"""
|
59 |
try:
|
60 |
if image is None:
|
61 |
return None
|
62 |
|
63 |
+
# 이미지 크기 최적화
|
|
|
|
|
|
|
|
|
64 |
width, height = image.size
|
65 |
if width * height > 4000000: # 4MP 이상이면 축소
|
66 |
ratio = (4000000 / (width * height)) ** 0.5
|
|
|
90 |
print(f"이미지 전처리 오류: {e}")
|
91 |
return image
|
92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
def extract_text_from_image(image):
|
94 |
+
"""이미지에서 한국어 텍스트 추출"""
|
95 |
global model, processing_cache
|
96 |
|
97 |
if model is None:
|
|
|
110 |
processed_image = preprocess_image(image)
|
111 |
|
112 |
# PIL Image를 bytes로 변환
|
113 |
+
buffer = io.BytesIO()
|
114 |
+
processed_image.save(buffer, format='PNG', optimize=True, quality=95)
|
115 |
+
image_data = buffer.getvalue()
|
|
|
|
|
|
|
|
|
116 |
|
117 |
+
# OCR 프롬프트
|
118 |
prompt = """
|
119 |
이 이미지에 있는 모든 한국어 텍스트를 정확하게 추출해주세요.
|
120 |
|
121 |
+
규칙:
|
122 |
1. 텍스트의 읽기 순서와 공간적 배치를 정확히 유지하세요
|
123 |
2. 줄바꿈, 들여쓰기, 공백을 원본과 동일하게 보존하세요
|
124 |
3. 한글, 영어, 숫자, 특수문자를 모두 포함하세요
|
125 |
4. 표나 목록의 구조를 유지하세요
|
126 |
5. 흐릿하거나 불분명한 글자는 [?]로 표시하세요
|
127 |
+
6. 텍스트만 출력하고 추가 설명은 하지 마세요
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
128 |
"""
|
129 |
|
130 |
# 이미지를 base64로 인코딩
|
|
|
144 |
|
145 |
extracted_text = response.text.strip()
|
146 |
|
|
|
|
|
|
|
147 |
# 캐시에 저장
|
148 |
processing_cache[image_hash] = extracted_text
|
149 |
|
|
|
152 |
except Exception as e:
|
153 |
if attempt == max_retries - 1:
|
154 |
raise e
|
155 |
+
time.sleep(1)
|
156 |
|
157 |
except Exception as e:
|
158 |
return f"❌ 텍스트 추출 실패: {str(e)}"
|
159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
def compare_texts(text1, text2):
|
161 |
+
"""두 텍스트를 비교하고 차이점을 HTML로 표시"""
|
162 |
if not text1 or not text2:
|
163 |
return "비교할 텍스트가 없습니다."
|
164 |
|
|
|
170 |
total_lines1 = len(lines1)
|
171 |
total_lines2 = len(lines2)
|
172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
# 유사도 계산
|
174 |
similarity = difflib.SequenceMatcher(None, text1, text2).ratio()
|
175 |
similarity_percent = round(similarity * 100, 1)
|
176 |
|
|
|
|
|
|
|
177 |
# 현재 시간
|
178 |
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
179 |
|
180 |
html_result = f"""
|
181 |
<div style="font-family: 'Noto Sans KR', sans-serif; line-height: 1.6;">
|
|
|
182 |
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 20px;">
|
183 |
+
<h2 style="margin: 0 0 10px 0;">📊 텍스트 비교 분석 결과</h2>
|
|
|
|
|
|
|
184 |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px;">
|
185 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
186 |
+
<div style="font-size: 0.9em;">유사도</div>
|
187 |
<div style="font-size: 1.5em; font-weight: bold;">{similarity_percent}%</div>
|
188 |
</div>
|
189 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
190 |
+
<div style="font-size: 0.9em;">원본 라인 수</div>
|
191 |
+
<div style="font-size: 1.5em; font-weight: bold;">{total_lines1}</div>
|
192 |
</div>
|
193 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
194 |
+
<div style="font-size: 0.9em;">수정된 라인 수</div>
|
195 |
+
<div style="font-size: 1.5em; font-weight: bold;">{total_lines2}</div>
|
196 |
</div>
|
197 |
<div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
|
198 |
+
<div style="font-size: 0.9em;">분석 시간</div>
|
199 |
<div style="font-size: 1em; font-weight: bold;">{timestamp}</div>
|
200 |
</div>
|
201 |
</div>
|
202 |
</div>
|
203 |
|
204 |
+
<div style="display: flex; gap: 20px;">
|
|
|
205 |
<div style="flex: 1;">
|
206 |
+
<div style="background: #ef4444; color: white; padding: 12px; border-radius: 8px 8px 0 0; font-weight: 600;">
|
207 |
+
📷 원본 텍스트 ({total_lines1}줄)
|
|
|
208 |
</div>
|
209 |
+
<div style="background-color: #fff5f5; border: 1px solid #fed7d7; padding: 15px; border-radius: 0 0 8px 8px; white-space: pre-wrap; font-family: monospace; max-height: 400px; overflow-y: auto;">
|
210 |
"""
|
211 |
|
212 |
+
# 차이점이 있는 라인 찾기
|
213 |
+
diff_lines = set()
|
214 |
+
for line in difflib.unified_diff(lines1, lines2, lineterm='', n=0):
|
215 |
+
if line.startswith('-') and not line.startswith('---'):
|
216 |
+
diff_lines.add(line[1:])
|
217 |
+
elif line.startswith('+') and not line.startswith('+++'):
|
218 |
+
diff_lines.add(line[1:])
|
219 |
+
|
220 |
+
# 원본 텍스트 표시 (차이점 강조)
|
221 |
+
for line in lines1:
|
222 |
+
if line in diff_lines:
|
223 |
+
html_result += f'<span style="background-color: #fecaca; color: #dc2626; padding: 2px 4px; border-radius: 3px;">{line}</span>\n'
|
224 |
else:
|
225 |
+
html_result += f'{line}\n'
|
226 |
|
227 |
+
html_result += f"""
|
228 |
</div>
|
229 |
</div>
|
230 |
<div style="flex: 1;">
|
231 |
+
<div style="background: #22c55e; color: white; padding: 12px; border-radius: 8px 8px 0 0; font-weight: 600;">
|
232 |
+
🔄 수정된 텍스트 ({total_lines2}줄)
|
|
|
|
|
|
|
233 |
</div>
|
234 |
+
<div style="background-color: #f0fdf4; border: 1px solid #bbf7d0; padding: 15px; border-radius: 0 0 8px 8px; white-space: pre-wrap; font-family: monospace; max-height: 400px; overflow-y: auto;">
|
235 |
"""
|
236 |
|
237 |
+
# 수정된 텍스트 표시 (차이점 강조)
|
238 |
+
for line in lines2:
|
239 |
+
if line in diff_lines:
|
240 |
+
html_result += f'<span style="background-color: #dcfce7; color: #059669; padding: 2px 4px; border-radius: 3px;">{line}</span>\n'
|
241 |
else:
|
242 |
+
html_result += f'{line}\n'
|
243 |
|
244 |
html_result += """
|
245 |
</div>
|
246 |
</div>
|
247 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
248 |
</div>
|
249 |
"""
|
250 |
|
251 |
return html_result
|
252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
253 |
def process_images(image1, image2, api_key_input):
|
254 |
"""두 이미지를 처리하고 비교 결과 반환"""
|
255 |
# 환경 변수 우선 확인 및 API 설정
|
|
|
285 |
|
286 |
return extracted_text
|
287 |
|
288 |
+
def apply_changes_only(original_text, change_request_text, api_key_input):
|
289 |
+
"""원본 텍스트에 변경사항만 적용"""
|
290 |
+
# 환경 변수 우선 확인 및 API 설정
|
291 |
+
if not model:
|
292 |
+
config_result = configure_api(api_key_input)
|
293 |
+
if "❌" in config_result:
|
294 |
+
return config_result, ""
|
295 |
+
|
296 |
+
if not original_text.strip():
|
297 |
+
return "먼저 이미지에서 텍스트를 추출해주세요.", ""
|
298 |
+
|
299 |
+
if not change_request_text.strip():
|
300 |
+
return "변경사항 요청을 입력해주세요.", ""
|
301 |
+
|
302 |
+
try:
|
303 |
+
# 변경사항 적용을 위한 프롬프트
|
304 |
+
prompt = f"""
|
305 |
+
다음 원본 텍스트에서 사용자가 요청한 변경사항을 정확하게 적용해주세요.
|
306 |
|
307 |
+
원본 텍스트:
|
308 |
+
{original_text}
|
309 |
+
|
310 |
+
변경사항 요청:
|
311 |
+
{change_request_text}
|
312 |
+
|
313 |
+
규칙:
|
314 |
+
1. 요청된 변경사항만 정확히 적용하세요
|
315 |
+
2. 나머지 텍스트는 원본과 완전히 동일하게 유지하세요
|
316 |
+
3. 텍스트의 형식, 구조, 줄바꿈을 원본과 동일하게 보존하세요
|
317 |
+
4. 한국어 맞춤법과 띄어쓰기를 정확히 적용하세요
|
318 |
+
5. 변경된 텍스트만 출력하고 설명이나 주석은 포함하지 마세요
|
319 |
"""
|
320 |
|
321 |
+
response = model.generate_content(prompt)
|
322 |
+
modified_text = response.text.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
323 |
|
324 |
# 비교 결과 생성
|
325 |
+
if original_text and modified_text:
|
326 |
+
comparison = compare_texts(original_text, modified_text)
|
327 |
else:
|
328 |
comparison = "텍스트 비교를 위해 원본 텍스트와 변경된 텍스트가 모두 필요합니다."
|
329 |
|
330 |
+
return modified_text, comparison
|
331 |
|
332 |
except Exception as e:
|
333 |
return f"❌ 변경사항 적용 실패: {str(e)}", ""
|
334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
335 |
def clear_all():
|
336 |
"""모든 입력 초기화"""
|
337 |
return None, None, "", "", ""
|
338 |
|
339 |
+
def clear_all_tab2():
|
340 |
+
"""탭2의 모든 입력 초기화"""
|
341 |
+
return None, "", "", "", ""
|
342 |
+
|
343 |
# Gradio 인터페이스 생성
|
344 |
def create_interface():
|
345 |
# 앱 시작 시 환경 변수 확인
|
|
|
352 |
.gradio-container {
|
353 |
font-family: 'Noto Sans KR', sans-serif !important;
|
354 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
355 |
"""
|
356 |
) as demo:
|
357 |
|
|
|
366 |
- **⚡ 캐싱 시스템**: 동일 이미지 재처리 방지로 속도 향상
|
367 |
- **🔍 정밀 비교 분석**: 라인별, 단어별 상세 차이점 분석
|
368 |
- **📊 실시간 통계**: 유사도, 변경사항 수치화
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
369 |
""")
|
370 |
|
371 |
+
# 환경 변수 상태 표시
|
372 |
if env_initialized:
|
373 |
gr.HTML(f"""
|
374 |
+
<div style="background: linear-gradient(135deg, #10b981, #059669); color: white; padding: 15px; border-radius: 12px; margin: 15px 0;">
|
375 |
<div style="display: flex; align-items: center; font-weight: 600; font-size: 1.1em;">
|
376 |
<span style="font-size: 1.5em; margin-right: 10px;">✅</span>
|
377 |
{env_message}
|
|
|
384 |
api_key_input = gr.Textbox(visible=False)
|
385 |
else:
|
386 |
gr.HTML("""
|
387 |
+
<div style="background: linear-gradient(135deg, #f59e0b, #d97706); color: white; padding: 20px; border-radius: 12px; margin: 15px 0;">
|
388 |
+
<div style="font-weight: 600; font-size: 1.1em; margin-bottom: 10px;">
|
389 |
+
⚠️ 환경 변수 설정 필요
|
|
|
390 |
</div>
|
391 |
<div style="background: rgba(255,255,255,0.2); padding: 15px; border-radius: 8px; margin: 10px 0;">
|
392 |
<div style="font-weight: 600; margin-bottom: 8px;">🔧 허깅페이스 설정 방법:</div>
|
393 |
<div style="font-size: 0.9em; line-height: 1.6;">
|
394 |
<div>1️⃣ 스페이스 설정(Settings) → Variables 탭</div>
|
395 |
+
<div>2️⃣ Name: GEMINI_API_KEY</div>
|
396 |
<div>3️⃣ Value: 여러분의 Google Gemini API 키</div>
|
397 |
<div>4️⃣ 스페이스 재시작으로 적용</div>
|
398 |
</div>
|
399 |
</div>
|
|
|
|
|
|
|
400 |
</div>
|
401 |
""")
|
402 |
+
|
403 |
+
with gr.Row():
|
404 |
+
with gr.Column():
|
405 |
+
gr.Markdown("### 🔑 임시 API 설정")
|
406 |
+
api_key_input = gr.Textbox(
|
407 |
+
label="Google Gemini API Key",
|
408 |
+
type="password",
|
409 |
+
placeholder="임시 사용을 위해 API 키를 입력하세요...",
|
410 |
+
info="⚠️ 보안을 위해 환경 변수 설정을 권장합니다."
|
411 |
+
)
|
412 |
|
413 |
with gr.Tabs():
|
414 |
with gr.TabItem("📷 이미지 비교"):
|
|
|
525 |
size="lg"
|
526 |
)
|
527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
528 |
# 이벤트 연결 - 탭1 (이미지 비교)
|
529 |
analyze_btn.click(
|
530 |
fn=process_images,
|
|
|
560 |
- **🖼️ 이미지 최적화**: 자동 전처리로 800x600 이상 해상도 권장
|
561 |
- **🔍 정확도 향상**: 선명하고 대비가 높은 이미지 사용
|
562 |
- **⚡ 성능 최적화**: 캐싱 시스템으로 동일 이미지 재처리 방지
|
|
|
563 |
- **🔒 보안 강화**: 환경 변수 사용으로 API 키 보안 강화
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
564 |
""")
|
565 |
|
566 |
return demo
|