import gradio as gr from backend import process_request, get_pdf_files, save_result_to_file, extract_text_with_fitz, extract_text_with_docling, preview_image_processing, load_system_prompt, load_user_prompt, load_postprocess_prompt, process_request_preprocessing_only, process_request_postprocessing_only def create_ui(): """Create and configure the Gradio UI interface.""" with gr.Blocks( title="이력서 분석 시스템", css=""" .main-container { max-width: 1400px; margin: 0 auto; } .section-header { margin-bottom: 15px; } .input-group { margin-bottom: 20px; } #batch_result_area, #result_area { min-height: 200px !important; } #batch_info { font-size: 0.85em; color: #666; margin-bottom: 10px; } /* 편집 영역 스타일 */ .edit-area { border: 2px dashed #ccc; border-radius: 5px; background-color: #f9f9f9; } """, theme=gr.themes.Soft() ) as app: with gr.Column(elem_classes="main-container"): gr.Markdown("# 📋 이력서 분석 시스템", elem_classes="section-header") # 상단 영역: 파일 선택 + 로그 정보 with gr.Row(equal_height=True): # 파일 선택 영역 (왼쪽, 컴팩트) with gr.Column(scale=2): with gr.Group(): gr.Markdown("### 📁 파일 선택") pdf_files = get_pdf_files() default_pdf = "./resume_samples/pdf/text/리멤버-S3.pdf" if "./resume_samples/pdf/text/리멤버-S3.pdf" in pdf_files else (pdf_files[0] if pdf_files else None) pdf_dropdown = gr.Dropdown( label="PDF 파일 선택", choices=pdf_files, value=default_pdf, interactive=True ) file_upload = gr.File( label="또는 새 PDF 파일 업로드", file_types=[".pdf"], type="filepath" ) # 실시간 상태 정보 (오른쪽) with gr.Column(scale=3): with gr.Group(): gr.Markdown("### 📊 실시간 상태 정보") status_log_output = gr.Textbox( label="처리 상태", lines=6, max_lines=10, value="시스템 준비 완료 - 파일을 선택하고 분석을 시작하세요...", interactive=False, show_label=False ) gr.Markdown("---") with gr.Tabs(): # 분석 탭 with gr.TabItem("🤖 AI 분석"): # 설정 영역 with gr.Row(equal_height=True): with gr.Column(scale=2): with gr.Group(): gr.Markdown("### 1️⃣ 프롬프트 설정") system_prompt_input = gr.TextArea( label="시스템 프롬프트", value=load_system_prompt(), lines=5, placeholder="시스템 프롬프트를 입력하세요..." ) prompt_input = gr.TextArea( label="사용자 프롬프트 (전처리)", value=load_user_prompt(), lines=3, placeholder="전처리용 사용자 프롬프트를 입력하세요..." ) postprocess_prompt_input = gr.TextArea( label="후처리 프롬프트", value=load_postprocess_prompt(), lines=3, placeholder="후처리용 프롬프트를 입력하세요..." ) with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 2️⃣ 처리 설정") use_images = gr.Checkbox( label="이미지로 변환하여 처리", value=True, info="PDF를 이미지로 변환하여 비전 모델로 분석" ) image_processing_mode = gr.Radio( choices=["가로 병합 (2페이지씩)", "세로 병합 (2페이지씩)", "낱개 페이지"], value="가로 병합 (2페이지씩)", label="이미지 처리 방식", info="페이지 병합 방식 선택" ) overlap_merge_option = gr.Radio( choices=["일반 병합", "중복 병합 (슬라이딩 윈도우)"], value="일반 병합", label="병합 방식", info="일반: (1,2), (3,4)... | 중복: (1,2), (2,3)...", visible=True ) batch_size_slider = gr.Slider( minimum=1, maximum=3, value=3, step=1, label="이미지 배치 크기", info="한 번에 처리할 이미지 장수 (1-3장)" ) use_docling = gr.Checkbox( label="텍스트 파싱 함께 수행", value=True, info="Docling으로 PDF 텍스트 추출" ) # use_postprocess 체크박스 제거 - 이제 버튼으로 분리 # 실행 버튼 영역 with gr.Row(): with gr.Column(scale=2): output_filename = gr.Textbox( label="결과 파일 이름 (확장자 없이)", value="result", placeholder="저장할 파일 이름을 입력하세요" ) with gr.Column(scale=1): preprocessing_button = gr.Button( "📝 전처리 분석 시작", variant="primary", size="lg" ) with gr.Column(scale=1): postprocessing_button = gr.Button( "🎯 후처리 분석 시작", variant="secondary", size="lg" ) # 결과 영역 gr.Markdown("---") gr.Markdown("## 📊 분석 결과") with gr.Row(): # 배치 처리 결과 - 편집 가능 with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 📝 배치 처리 결과") batch_result_output = gr.Markdown( value="*배치 처리 결과가 여기에 표시됩니다...*", elem_id="batch_result_area", show_label=False, ) # 배치 결과 편집 영역 with gr.Row(): batch_edit_button = gr.Button( "✏️ 편집", variant="secondary", size="sm" ) batch_save_button = gr.Button( "💾 저장", variant="primary", size="sm", visible=False ) batch_cancel_button = gr.Button( "❌ 취소", variant="secondary", size="sm", visible=False ) batch_edit_area = gr.TextArea( value="", lines=15, max_lines=50, interactive=True, show_label=False, visible=False, placeholder="배치 처리 결과를 편집하세요..." ) # 최종 분석 결과 - 자동 크기 조정 with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 🎯 최종 분석 결과") result_output = gr.Markdown( value="*최종 분석 결과가 여기에 표시됩니다...*", elem_id="result_area", show_label=False, ) # 최종 결과 편집 영역 with gr.Row(): result_edit_button = gr.Button( "✏️ 편집", variant="secondary", size="sm" ) result_save_button = gr.Button( "💾 저장", variant="primary", size="sm", visible=False ) result_cancel_button = gr.Button( "❌ 취소", variant="secondary", size="sm", visible=False ) result_edit_area = gr.TextArea( value="", lines=15, max_lines=50, interactive=True, show_label=False, visible=False, placeholder="최종 분석 결과를 편집하세요..." ) # 파일 저장 영역 with gr.Row(): save_button = gr.Button( "💾 결과 저장", variant="secondary", size="sm" ) save_message = gr.Markdown( value="", visible=False ) # 미리보기 탭 with gr.TabItem("🔍 미리보기"): # 이미지 미리보기 영역 (상단) with gr.Row(equal_height=True): with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 🖼️ 이미지 미리보기") preview_button_tab = gr.Button( "이미지 처리 미리보기", variant="secondary", size="sm" ) image_preview_gallery_tab = gr.Gallery( label="처리된 이미지", show_label=False, columns=2, rows=2, height=350, value=[] ) gr.Markdown("---") # 텍스트 추출 비교 영역 (하단) gr.Markdown("### 📄 PDF 텍스트 추출 비교", elem_classes="section-header") with gr.Row(equal_height=True): # 텍스트 기반 추출 with gr.Column(scale=1): with gr.Group(): gr.Markdown("#### 📝 텍스트 기반 추출") gr.Markdown("*PDF의 텍스트 레이어에서 직접 추출*", elem_id="extract_info") text_extract_method = gr.Radio( choices=["Fitz (PyMuPDF)"], value="Fitz (PyMuPDF)", label="추출 방식", info="빠르고 가벼운 텍스트 추출" ) text_extract_btn = gr.Button( "📝 텍스트 추출", variant="primary", size="sm" ) text_extract_result = gr.Markdown( value="*텍스트 추출 결과가 여기에 표시됩니다...*", elem_id="text_result_area" ) # OCR 기반 추출 with gr.Column(scale=1): with gr.Group(): gr.Markdown("#### 🤖 OCR 기반 추출") gr.Markdown("*이미지에서 광학 문자 인식으로 추출*", elem_id="ocr_info") ocr_extract_btn = gr.Button( "🔍 OCR 추출", variant="primary", size="sm" ) ocr_extract_result = gr.Markdown( value="*OCR 추출 결과가 여기에 표시됩니다...*", elem_id="ocr_result_area" ) # 통합 비교 버튼 with gr.Row(): compare_both_btn = gr.Button( "🔄 양쪽 모두 추출하여 비교", variant="secondary", size="lg" ) # API 요청 및 로그 탭 with gr.TabItem("📊 API 요청 & 로그"): with gr.Row(equal_height=True): # API 요청 RAW 정보 with gr.Column(scale=1): with gr.Group(): gr.Markdown("#### 📤 API 요청 (Raw)") api_request_output = gr.Code( value="분석 시작 시 실제 API 요청 내용이 표시됩니다", language="json", label=None, interactive=False ) # 텍스트 파싱 결과 (자동 업데이트) with gr.Column(scale=1): with gr.Group(): gr.Markdown("#### 📄 텍스트 파싱 결과 (실시간)") docling_output = gr.Code( value="PDF 텍스트 파싱 결과가 자동으로 표시됩니다", language="markdown", label=None, interactive=False, lines=20 ) # === 이벤트 핸들러 함수들 === def update_status_only(status_text): """상태 로그만 업데이트하는 함수 (로딩 효과 없음)""" return status_text # === 편집 관련 함수들 === def start_batch_edit(batch_content): """배치 결과 편집 시작""" return ( gr.update(visible=False), # edit button gr.update(visible=True), # save button gr.update(visible=True), # cancel button gr.update(visible=True, value=batch_content), # edit area gr.update(visible=False) # markdown display ) def save_batch_edit(edited_content): """배치 결과 편집 저장""" return ( gr.update(visible=True), # edit button gr.update(visible=False), # save button gr.update(visible=False), # cancel button gr.update(visible=False), # edit area gr.update(visible=True, value=edited_content) # markdown display ) def cancel_batch_edit(): """배치 결과 편집 취소""" return ( gr.update(visible=True), # edit button gr.update(visible=False), # save button gr.update(visible=False), # cancel button gr.update(visible=False), # edit area gr.update(visible=True) # markdown display ) def start_result_edit(result_content): """최종 결과 편집 시작""" return ( gr.update(visible=False), # edit button gr.update(visible=True), # save button gr.update(visible=True), # cancel button gr.update(visible=True, value=result_content), # edit area gr.update(visible=False) # markdown display ) def save_result_edit(edited_content): """최종 결과 편집 저장""" return ( gr.update(visible=True), # edit button gr.update(visible=False), # save button gr.update(visible=False), # cancel button gr.update(visible=False), # edit area gr.update(visible=True, value=edited_content) # markdown display ) def cancel_result_edit(): """최종 결과 편집 취소""" return ( gr.update(visible=True), # edit button gr.update(visible=False), # save button gr.update(visible=False), # cancel button gr.update(visible=False), # edit area gr.update(visible=True) # markdown display ) def process_preprocessing_wrapper(*args): """전처리만 수행하는 래퍼 함수""" try: # 전처리 함수에서 Generator 결과 추출 generator = process_request_preprocessing_only(*args) final_result = None # Generator의 모든 중간 결과를 처리하며 마지막 결과를 얻음 for result in generator: if result and len(result) >= 5: batch_content, result_content, docling_output, status_log, api_request = result # 전처리에서는 배치 결과만 표시, 최종 결과는 안내 메시지 유지 yield batch_content, "*전처리 완료 후 후처리 버튼을 눌러주세요...*", docling_output, status_log, api_request final_result = result if final_result and len(final_result) >= 5: batch_content, result_content, docling_output, status_log, api_request = final_result # 전처리 완료 시 최종 결과는 안내 메시지로 유지 yield batch_content, "*✅ 전처리 완료! 후처리 분석 버튼을 눌러 최종 결과를 확인하세요.*", docling_output, status_log, api_request else: yield "배치 처리 결과가 없습니다.", "전처리를 먼저 수행해주세요.", "", "전처리 완료", "" except Exception as e: error_msg = f"전처리 오류: {str(e)}" print(f"전처리 래퍼 함수 오류: {e}") yield "❌ **전처리 오류 발생**", "전처리 중 오류가 발생했습니다.", "", f"전처리 오류 발생: {str(e)}", "" def process_postprocessing_wrapper(batch_result, system_prompt, postprocess_prompt): """후처리만 수행하는 래퍼 함수""" try: # 배치 결과가 비어있거나 초기 메시지인 경우 확인 if not batch_result or batch_result.strip() == "*배치 처리 결과가 여기에 표시됩니다...*": yield batch_result, "❌ **배치 처리 결과가 없습니다**\n\n전처리를 먼저 수행해주세요.", "", "후처리 실행 불가: 배치 결과 없음", "" return # 후처리 함수에서 Generator 결과 추출 generator = process_request_postprocessing_only(batch_result, system_prompt, postprocess_prompt) final_result = None # Generator의 모든 중간 결과를 처리하며 마지막 결과를 얻음 for result in generator: if result and len(result) >= 5: batch_content, result_content, docling_output, status_log, api_request = result # 실시간으로 결과 업데이트 (배치 결과는 유지) yield batch_result, result_content, docling_output, status_log, api_request final_result = result if final_result and len(final_result) >= 5: batch_content, result_content, docling_output, status_log, api_request = final_result # 후처리 완료 시 배치 결과는 유지하고 최종 결과만 업데이트 yield batch_result, result_content, docling_output, status_log, api_request else: yield batch_result, "후처리 결과가 없습니다.", "", "후처리 완료", "" except Exception as e: error_msg = f"후처리 오류: {str(e)}" print(f"후처리 래퍼 함수 오류: {e}") yield batch_result, f"❌ **후처리 오류 발생**\n\n{error_msg}", "", f"후처리 오류 발생: {str(e)}", "" def on_save_button_click(result_content, filename): """결과 저장 처리""" result = save_result_to_file(result_content, filename) return gr.update(value=result, visible=True) def extract_with_text_method(pdf_path, uploaded_file, method): """텍스트 기반 추출""" final_pdf_path = uploaded_file or pdf_path if not final_pdf_path: return "PDF 파일을 선택해주세요." return extract_text_with_fitz(final_pdf_path) def extract_with_ocr(pdf_path, uploaded_file): """OCR 기반 추출""" final_pdf_path = uploaded_file or pdf_path if not final_pdf_path: return "PDF 파일을 선택해주세요." return extract_text_with_docling(final_pdf_path) def extract_both_methods(pdf_path, uploaded_file, text_method): """양쪽 모두 추출""" final_pdf_path = uploaded_file or pdf_path if not final_pdf_path: return "PDF 파일을 선택해주세요.", "PDF 파일을 선택해주세요." text_result = extract_with_text_method(pdf_path, uploaded_file, text_method) ocr_result = extract_with_ocr(pdf_path, uploaded_file) return text_result, ocr_result def preview_images(pdf_path, uploaded_file, processing_mode, use_images, overlap_option): """이미지 미리보기""" if not use_images: return [] final_pdf_path = uploaded_file or pdf_path if not final_pdf_path: return [] try: return preview_image_processing(final_pdf_path, processing_mode, overlap_option) except Exception as e: print(f"미리보기 오류: {e}") return [] def update_overlap_visibility(processing_mode): """병합 옵션 표시/숨김 제어""" return gr.update(visible="병합" in processing_mode) # === 이벤트 연결 === # 전처리 분석 버튼 preprocessing_button.click( fn=process_preprocessing_wrapper, inputs=[ prompt_input, system_prompt_input, use_images, use_docling, pdf_dropdown, file_upload, output_filename, image_processing_mode, overlap_merge_option, batch_size_slider ], outputs=[batch_result_output, result_output, docling_output, status_log_output, api_request_output], show_progress=True ) # 후처리 분석 버튼 postprocessing_button.click( fn=process_postprocessing_wrapper, inputs=[batch_result_output, system_prompt_input, postprocess_prompt_input], outputs=[batch_result_output, result_output, docling_output, status_log_output, api_request_output], show_progress=True ) # 저장 버튼 save_button.click( fn=on_save_button_click, inputs=[result_output, output_filename], outputs=[save_message] ) # 이미지 미리보기 (미리보기 탭) preview_button_tab.click( fn=preview_images, inputs=[pdf_dropdown, file_upload, image_processing_mode, use_images, overlap_merge_option], outputs=[image_preview_gallery_tab] ) # 이미지 처리 모드 변경 시 중복 옵션 표시/숨김 image_processing_mode.change( fn=update_overlap_visibility, inputs=[image_processing_mode], outputs=[overlap_merge_option] ) # 텍스트 추출 이벤트들 text_extract_btn.click( fn=extract_with_text_method, inputs=[pdf_dropdown, file_upload, text_extract_method], outputs=[text_extract_result] ) ocr_extract_btn.click( fn=extract_with_ocr, inputs=[pdf_dropdown, file_upload], outputs=[ocr_extract_result] ) compare_both_btn.click( fn=extract_both_methods, inputs=[pdf_dropdown, file_upload, text_extract_method], outputs=[text_extract_result, ocr_extract_result] ) # === 편집 관련 이벤트 연결 === # 배치 결과 편집 이벤트 batch_edit_button.click( fn=start_batch_edit, inputs=[batch_result_output], outputs=[batch_edit_button, batch_save_button, batch_cancel_button, batch_edit_area, batch_result_output] ) batch_save_button.click( fn=save_batch_edit, inputs=[batch_edit_area], outputs=[batch_edit_button, batch_save_button, batch_cancel_button, batch_edit_area, batch_result_output] ) batch_cancel_button.click( fn=cancel_batch_edit, outputs=[batch_edit_button, batch_save_button, batch_cancel_button, batch_edit_area, batch_result_output] ) # 최종 결과 편집 이벤트 result_edit_button.click( fn=start_result_edit, inputs=[result_output], outputs=[result_edit_button, result_save_button, result_cancel_button, result_edit_area, result_output] ) result_save_button.click( fn=save_result_edit, inputs=[result_edit_area], outputs=[result_edit_button, result_save_button, result_cancel_button, result_edit_area, result_output] ) result_cancel_button.click( fn=cancel_result_edit, outputs=[result_edit_button, result_save_button, result_cancel_button, result_edit_area, result_output] ) return app