aliceblue11 commited on
Commit
dab4daa
·
verified ·
1 Parent(s): 0071410

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -478
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, ImageFilter
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
- # 전역 변수로 API 키 저장
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
- # 이미지를 PIL Image로 변환 (필요시)
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
- if isinstance(processed_image, str):
178
- with open(processed_image, 'rb') as f:
179
- image_data = f.read()
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
- 전문 OCR 규칙:
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; display: flex; align-items: center;">
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; opacity: 0.9;">유사도</div>
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; opacity: 0.9;">변경된 라인</div>
319
- <div style="font-size: 1.5em; font-weight: bold; color: #ffeb3b;">{removed_lines + added_lines}</div>
320
  </div>
321
  <div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
322
- <div style="font-size: 0.9em; opacity: 0.9;">단어 변경</div>
323
- <div style="font-size: 1.5em; font-weight: bold; color: #4caf50;">{word_changes}</div>
324
  </div>
325
  <div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 8px;">
326
- <div style="font-size: 0.9em; opacity: 0.9;">분석 시간</div>
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: linear-gradient(135deg, #ff6b6b, #ee5a52); color: white; padding: 12px; border-radius: 8px 8px 0 0; font-weight: 600; display: flex; align-items: center;">
336
- <span style="margin-right: 8px;">📷</span>
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: 'Courier New', monospace; max-height: 400px; overflow-y: auto; font-size: 14px;">
340
  """
341
 
342
- # 차이점이 있는 라인 찾기 (개선된 알고리즘)
343
- diff_lines1, diff_lines2 = find_detailed_differences(lines1, lines2)
344
-
345
- # 원본 텍스트 표시 (라인별 하이라이트)
346
- for i, line in enumerate(lines1):
347
- if i in diff_lines1:
348
- html_result += f'<div style="background-color: #fecaca; border-left: 4px solid #ef4444; padding: 8px; margin: 2px 0; border-radius: 4px;"><strong>라인 {i+1}:</strong> {line}</div>'
 
 
 
 
 
349
  else:
350
- html_result += f'<div style="padding: 4px 0; color: #6b7280;">라인 {i+1}: {line}</div>'
351
 
352
- html_result += """
353
  </div>
354
  </div>
355
  <div style="flex: 1;">
356
- <div style="background: linear-gradient(135deg, #51cf66, #40c057); color: white; padding: 12px; border-radius: 8px 8px 0 0; font-weight: 600; display: flex; align-items: center;">
357
- <span style="margin-right: 8px;">🔄</span>
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: 'Courier New', monospace; max-height: 400px; overflow-y: auto; font-size: 14px;">
363
  """
364
 
365
- # 수정된 텍스트 표시 (라인별 하이라이트)
366
- for i, line in enumerate(lines2):
367
- if i in diff_lines2:
368
- html_result += f'<div style="background-color: #dcfce7; border-left: 4px solid #22c55e; padding: 8px; margin: 2px 0; border-radius: 4px;"><strong>라인 {i+1}:</strong> {line}</div>'
369
  else:
370
- html_result += f'<div style="padding: 4px 0; color: #6b7280;">라인 {i+1}: {line}</div>'
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
- 7. 오직 변경된 텍스트만 출력하고 설명이나 주석은 포함하지 마세요
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
- **품질 보증:**
505
- - 변경 전후의 텍스트 길이와 구조가 적절한지 검토
506
- - 한국어 문법과 자연스러운 표현 확인
507
- - 변경사항이 완전히 반영되었는지 검증
 
 
 
 
 
 
 
 
508
  """
509
 
510
- # 재시도 메커니즘과 품질 검증
511
- max_retries = 2
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 final_text:
536
- comparison = compare_texts(original_text, final_text)
537
  else:
538
  comparison = "텍스트 비교를 위해 원본 텍스트와 변경된 텍스트가 모두 필요합니다."
539
 
540
- return final_text, comparison
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
- # 환경 변수 상태 표시 (개선된 UI)
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; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
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; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
689
- <div style="font-weight: 600; font-size: 1.1em; margin-bottom: 10px; display: flex; align-items: center;">
690
- <span style="font-size: 1.5em; margin-right: 10px;">⚠️</span>
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: <code style="background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px;">GEMINI_API_KEY</code></div>
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