Spaces:
Build error
Build error
| import gradio as gr | |
| import google.generativeai as genai | |
| import PIL.Image | |
| import difflib | |
| import re | |
| from typing import List, Tuple, Optional | |
| import os | |
| from dotenv import load_dotenv | |
| # 환경변수 로드 | |
| load_dotenv() | |
| # Google AI API 설정 | |
| GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") | |
| if GOOGLE_API_KEY: | |
| genai.configure(api_key=GOOGLE_API_KEY) | |
| model = genai.GenerativeModel('gemini-1.5-flash') | |
| class TextDiffChecker: | |
| def __init__(self): | |
| self.model = model if GOOGLE_API_KEY else None | |
| def extract_text_from_image(self, image: PIL.Image.Image) -> str: | |
| """이미지에서 텍스트 추출""" | |
| if not self.model: | |
| return "API 키가 설정되지 않았습니다." | |
| try: | |
| # Gemini Vision으로 텍스트 추출 | |
| prompt = """ | |
| 이 이미지에서 모든 텍스트를 정확히 추출해주세요. | |
| - 배경은 무시하고 텍스트만 인식 | |
| - 글자 크기, 색상, 폰트는 무시 | |
| - 줄바꿈과 공백을 유지 | |
| - 특수문자와 숫자도 모두 포함 | |
| - 추출된 텍스트만 반환 (다른 설명 없이) | |
| """ | |
| response = self.model.generate_content([prompt, image]) | |
| return response.text.strip() | |
| except Exception as e: | |
| return f"텍스트 추출 오류: {str(e)}" | |
| def normalize_text(self, text: str) -> str: | |
| """텍스트 정규화 (공백, 줄바꿈 정리)""" | |
| # 연속된 공백을 하나로 | |
| text = re.sub(r'\s+', ' ', text) | |
| # 앞뒤 공백 제거 | |
| text = text.strip() | |
| return text | |
| def find_differences(self, text1: str, text2: str) -> Tuple[str, List[str]]: | |
| """두 텍스트의 차이점 찾기""" | |
| # 텍스트 정규화 | |
| norm_text1 = self.normalize_text(text1) | |
| norm_text2 = self.normalize_text(text2) | |
| # 단어 단위로 분할 | |
| words1 = norm_text1.split() | |
| words2 = norm_text2.split() | |
| # difflib를 사용한 차이점 분석 | |
| differ = difflib.unified_diff( | |
| words1, words2, | |
| fromfile='원본', tofile='비교본', | |
| lineterm='' | |
| ) | |
| diff_result = list(differ) | |
| # 차이점 요약 | |
| differences = [] | |
| added_words = [] | |
| removed_words = [] | |
| for line in diff_result: | |
| if line.startswith('+') and not line.startswith('+++'): | |
| added_words.extend(line[1:].split()) | |
| elif line.startswith('-') and not line.startswith('---'): | |
| removed_words.extend(line[1:].split()) | |
| if removed_words: | |
| differences.append(f"❌ 삭제된 단어: {', '.join(removed_words)}") | |
| if added_words: | |
| differences.append(f"✅ 추가된 단어: {', '.join(added_words)}") | |
| # HTML로 차이점 강조 표시 | |
| html_diff = self.create_html_diff(words1, words2) | |
| return html_diff, differences | |
| def create_html_diff(self, words1: List[str], words2: List[str]) -> str: | |
| """HTML로 차이점 시각화""" | |
| matcher = difflib.SequenceMatcher(None, words1, words2) | |
| html_parts = [] | |
| html_parts.append("<div style='font-family: monospace; line-height: 1.6;'>") | |
| html_parts.append("<h3>🔍 텍스트 비교 결과</h3>") | |
| # 원본 텍스트 | |
| html_parts.append("<div style='margin: 10px 0;'>") | |
| html_parts.append("<strong>📄 원본:</strong><br>") | |
| html_parts.append("<div style='background: #f0f0f0; padding: 10px; border-radius: 5px; margin: 5px 0;'>") | |
| for tag, i1, i2, j1, j2 in matcher.get_opcodes(): | |
| if tag == 'equal': | |
| html_parts.append(' '.join(words1[i1:i2])) | |
| elif tag == 'delete': | |
| html_parts.append(f"<span style='background: #ffcccc; text-decoration: line-through;'>{' '.join(words1[i1:i2])}</span>") | |
| elif tag == 'replace': | |
| html_parts.append(f"<span style='background: #ffcccc; text-decoration: line-through;'>{' '.join(words1[i1:i2])}</span>") | |
| if tag != 'equal': | |
| html_parts.append(" ") | |
| html_parts.append("</div></div>") | |
| # 비교본 텍스트 | |
| html_parts.append("<div style='margin: 10px 0;'>") | |
| html_parts.append("<strong>📝 비교본:</strong><br>") | |
| html_parts.append("<div style='background: #f0f0f0; padding: 10px; border-radius: 5px; margin: 5px 0;'>") | |
| for tag, i1, i2, j1, j2 in matcher.get_opcodes(): | |
| if tag == 'equal': | |
| html_parts.append(' '.join(words2[j1:j2])) | |
| elif tag == 'insert': | |
| html_parts.append(f"<span style='background: #ccffcc; font-weight: bold;'>{' '.join(words2[j1:j2])}</span>") | |
| elif tag == 'replace': | |
| html_parts.append(f"<span style='background: #ccffcc; font-weight: bold;'>{' '.join(words2[j1:j2])}</span>") | |
| if tag != 'equal': | |
| html_parts.append(" ") | |
| html_parts.append("</div></div>") | |
| html_parts.append("</div>") | |
| return ''.join(html_parts) | |
| # 전역 인스턴스 | |
| checker = TextDiffChecker() | |
| def process_comparison(image1, image2, text_input, comparison_mode): | |
| """메인 비교 처리 함수""" | |
| if not GOOGLE_API_KEY: | |
| return "❌ Google API 키를 설정해주세요.", "", [] | |
| try: | |
| # 첫 번째 소스에서 텍스트 추출 | |
| if image1 is not None: | |
| text1 = checker.extract_text_from_image(image1) | |
| source1_info = "📷 이미지 1에서 추출된 텍스트" | |
| else: | |
| return "❌ 첫 번째 이미지를 업로드해주세요.", "", [] | |
| # 두 번째 소스 처리 | |
| if comparison_mode == "이미지 vs 이미지": | |
| if image2 is not None: | |
| text2 = checker.extract_text_from_image(image2) | |
| source2_info = "📷 이미지 2에서 추출된 텍스트" | |
| else: | |
| return "❌ 두 번째 이미지를 업로드해주세요.", "", [] | |
| else: # 이미지 vs 텍스트 | |
| if text_input.strip(): | |
| text2 = text_input.strip() | |
| source2_info = "📝 입력된 텍스트" | |
| else: | |
| return "❌ 비교할 텍스트를 입력해주세요.", "", [] | |
| # 추출된 텍스트 표시 | |
| extracted_info = f""" | |
| ### 📋 추출된 텍스트 | |
| **{source1_info}:** | |
| ``` | |
| {text1} | |
| ``` | |
| **{source2_info}:** | |
| ``` | |
| {text2} | |
| ``` | |
| """ | |
| # 차이점 분석 | |
| html_diff, differences = checker.find_differences(text1, text2) | |
| if not differences: | |
| differences = ["✅ 두 텍스트가 동일합니다!"] | |
| return extracted_info, html_diff, differences | |
| except Exception as e: | |
| return f"❌ 처리 중 오류 발생: {str(e)}", "", [] | |
| def create_interface(): | |
| """Gradio 인터페이스 생성""" | |
| with gr.Blocks( | |
| title="📝 텍스트 비교 검수 시스템", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .gradio-container { | |
| max-width: 1200px; | |
| margin: auto; | |
| } | |
| .diff-output { | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 15px; | |
| background: white; | |
| } | |
| """ | |
| ) as app: | |
| gr.Markdown(""" | |
| # 📝 텍스트 비교 검수 시스템 | |
| 이미지의 텍스트를 인식하고 비교하여 차이점을 찾아드립니다. | |
| ⚙️ **기능:** | |
| - 🖼️ 이미지에서 텍스트 자동 추출 (Google Gemini Vision API) | |
| - 🔍 두 텍스트 간 차이점 정확한 분석 | |
| - 📊 시각적 차이점 표시 (추가/삭제/변경) | |
| - 🎯 배경 무시, 텍스트만 집중 분석 | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 📤 입력") | |
| comparison_mode = gr.Radio( | |
| choices=["이미지 vs 이미지", "이미지 vs 텍스트"], | |
| value="이미지 vs 이미지", | |
| label="비교 모드" | |
| ) | |
| image1 = gr.Image( | |
| label="📷 첫 번째 이미지 (원본)", | |
| type="pil" | |
| ) | |
| with gr.Group() as image_group: | |
| image2 = gr.Image( | |
| label="📷 두 번째 이미지 (비교본)", | |
| type="pil" | |
| ) | |
| with gr.Group(visible=False) as text_group: | |
| text_input = gr.Textbox( | |
| label="📝 비교할 텍스트", | |
| placeholder="비교하고 싶은 텍스트를 직접 입력하세요...", | |
| lines=5 | |
| ) | |
| def toggle_input_mode(mode): | |
| if mode == "이미지 vs 텍스트": | |
| return gr.update(visible=False), gr.update(visible=True) | |
| else: | |
| return gr.update(visible=True), gr.update(visible=False) | |
| comparison_mode.change( | |
| toggle_input_mode, | |
| inputs=[comparison_mode], | |
| outputs=[image_group, text_group] | |
| ) | |
| analyze_btn = gr.Button( | |
| "🔍 텍스트 비교 분석 시작", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 📊 분석 결과") | |
| with gr.Tabs(): | |
| with gr.TabItem("📋 추출된 텍스트"): | |
| extracted_text = gr.Markdown() | |
| with gr.TabItem("🔍 시각적 비교"): | |
| visual_diff = gr.HTML(elem_classes=["diff-output"]) | |
| with gr.TabItem("📝 차이점 요약"): | |
| differences_list = gr.JSON(label="발견된 차이점") | |
| # API 키 상태 표시 | |
| if not GOOGLE_API_KEY: | |
| gr.Markdown(""" | |
| ⚠️ **설정 필요:** | |
| 1. Google AI Studio에서 API 키를 발급받으세요 | |
| 2. `.env` 파일에 `GOOGLE_API_KEY=your_api_key` 추가 | |
| 3. 애플리케이션을 재시작하세요 | |
| """) | |
| # 이벤트 연결 | |
| analyze_btn.click( | |
| process_comparison, | |
| inputs=[image1, image2, text_input, comparison_mode], | |
| outputs=[extracted_text, visual_diff, differences_list] | |
| ) | |
| # 예시 및 도움말 | |
| gr.Markdown(""" | |
| ### 💡 사용 팁 | |
| - 📸 **고품질 이미지 사용**: 텍스트가 선명한 이미지일수록 정확도가 높아집니다 | |
| - 🔤 **다양한 언어 지원**: 한글, 영어, 숫자, 특수문자 모두 인식 가능 | |
| - 🎨 **배경 무시**: 복잡한 배경이 있어도 텍스트만 정확히 추출합니다 | |
| - ⚡ **실시간 비교**: 업로드와 동시에 즉시 분석 결과를 확인할 수 있습니다 | |
| """) | |
| return app | |
| if __name__ == "__main__": | |
| app = create_interface() | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True | |
| ) |