import os
import gradio as gr
import json
import logging
from app.app import app as flask_app # Flask 앱 가져오기
from flask import json as flask_json
from retrying import retry
# 로거 설정
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG # INFO에서 DEBUG로 변경하여 더 상세한 로그 확인
)
logger = logging.getLogger(__name__)
# Flask 테스트 클라이언트 초기화
@retry(tries=5, delay=1, backoff=2)
def init_flask_client():
"""백그라운드 실행 Flask 서버에 연결하는 클라이언트 초기화"""
try:
test_client = flask_app.test_client()
logger.info("Flask 테스트 클라이언트 초기화 성공")
return test_client
except Exception as e:
logger.error(f"Flask 테스트 클라이언트 초기화 실패: {e}")
raise e
# 소스 정보 포맷팅 헤루퍼 함수
def format_source_info(sources, prefix=""):
"""검색 결과 소스 정보를 형식화
Args:
sources: 소스 정보 리스트
prefix: 로그 메시지 접두사 (기본값: "")
Returns:
형식화된 소스 정보 문자열
"""
source_info = ""
if sources and len(sources) > 0:
logger.debug(f"{prefix}소스 정보 포맷팅: {len(sources)}개 소스: {json.dumps(sources, ensure_ascii=False, indent=2)}")
source_list = []
for src in sources:
source_text = src["source"]
# id 필드가 있으면 함께 표시
if "id" in src:
logger.debug(f"{prefix}ID 필드 발견: {src['id']}")
source_text += f" (ID: {src['id']})"
source_list.append(source_text)
if source_list:
source_info = "\n\n참조 소스:\n" + "\n".join(source_list)
logger.debug(f"{prefix}최종 소스 정보 형식: {source_info}")
return source_info
# Flask 테스트 클라이언트 초기화
flask_client = init_flask_client()
# Gradio 버전 확인 메시지
logger.info(f"Gradio 버전: {gr.__version__}")
# Gradio 인터페이스 생성
with gr.Blocks(title="RAG 검색 챗봇 with 음성인식") as demo:
gr.HTML("""
RAG 검색 챗봇 with 음성인식
텍스트 또는 음성으로 질문을 입력하세요.
""")
with gr.Tab("텍스트 챗"):
text_input = gr.Textbox(label="질문 입력", placeholder="여기에 질문을 입력하세요...")
text_output = gr.Textbox(label="응답", interactive=False)
text_button = gr.Button("질문 제출")
with gr.Tab("음성 챗"):
with gr.Row():
# 녹음 UI 개선: 마이크로폰으로 소스 지정 및 음파 표시 활성화
audio_input = gr.Audio(
label="음성 입력",
type="filepath",
sources=["microphone"],
show_label=True,
waveform_options={"show_controls": True, "normalize": True},
interactive=True
)
audio_transcription = gr.Textbox(label="인식된 텍스트", interactive=False)
audio_output = gr.Textbox(label="응답", interactive=False)
gr.Markdown("""
녹음 정지 버튼을 누르면 자동으로 음성이 전송됩니다.
""")
with gr.Tab("문서 업로드"):
doc_input = gr.File(label="문서 업로드", file_types=[".txt", ".md", ".pdf", ".docx", ".csv"])
doc_output = gr.Textbox(label="업로드 결과", interactive=False)
doc_button = gr.Button("문서 업로드")
# 텍스트 챗 기능
def handle_text_chat(query):
if not query:
return "질문을 입력하세요."
try:
logger.info("텍스트 챗 요청: /api/chat")
response = flask_client.post("/api/chat", json={"query": query})
data = flask_json.loads(response.data)
# 디버깅을 위한 API 응답 로그
logger.info(f"API 응답 구조: {json.dumps(data, ensure_ascii=False, indent=2)[:500]}...")
if "error" in data:
logger.error(f"텍스트 챗 오류: {data['error']}")
return data["error"]
# 소스 정보 추출 및 포맷팅
source_info = ""
if "sources" in data and data["sources"]:
source_info = format_source_info(data["sources"])
# 응답과 소스 정보를 함께 반환
return data["answer"] + source_info
except Exception as e:
logger.error(f"텍스트 챗 처리 실패: {str(e)}")
return f"처리 중 오류 발생: {str(e)}"
# 음성 챗 기능
def handle_voice_chat(audio_file):
if not audio_file:
return "", "음성을 업로드하세요."
try:
logger.info("음성 챗 요청: /api/voice")
with open(audio_file, "rb") as f:
# Flask 테스트 클라이언트는 files 직접 지원 안 하므로, 데이터를 읽어 전달
response = flask_client.post(
"/api/voice",
data={"audio": (f, "audio_file")}
)
data = flask_json.loads(response.data)
# 디버깅을 위한 API 응답 로그
logger.info(f"[음성챗] API 응답 구조: {json.dumps(data, ensure_ascii=False, indent=2)[:500]}...")
if "error" in data:
logger.error(f"음성 챗 오류: {data['error']}")
return "", data["error"]
# 소스 정보 추출 및 포맷팅
source_info = ""
if "sources" in data and data["sources"]:
source_info = format_source_info(data["sources"], prefix="[음성챗] ")
# 인식된 텍스트와 소스 정보가 포함된 응답 반환
return data["transcription"], data["answer"] + source_info
except Exception as e:
logger.error(f"음성 챗 처리 실패: {str(e)}")
return "", f"처리 중 오류 발생: {str(e)}"
# 문서 업로드 기능
def handle_doc_upload(doc_file):
if not doc_file:
return "문서를 업로드하세요."
try:
logger.info(f"문서 업로드 요청: /api/upload, 파일명: {doc_file.name}")
file_extension = os.path.splitext(doc_file.name)[1].lower()
logger.info(f"파일 확장자: {file_extension}")
with open(doc_file, "rb") as f:
response = flask_client.post(
"/api/upload",
data={"document": (f, doc_file.name)}
)
data = flask_json.loads(response.data)
if "error" in data:
logger.error(f"문서 업로드 오류: {data['error']}")
return data["error"]
return data["message"]
except Exception as e:
logger.error(f"문서 업로드 처리 실패: {str(e)}")
return f"처리 중 오류 발생: {str(e)}"
# 이벤트 핸들러 연결
text_button.click(
fn=handle_text_chat,
inputs=text_input,
outputs=text_output
)
# 음성 입력 값이 변경될 때 자동으로 전송
audio_input.change(
fn=handle_voice_chat,
inputs=audio_input,
outputs=[audio_transcription, audio_output]
)
doc_button.click(
fn=handle_doc_upload,
inputs=doc_input,
outputs=doc_output
)
if __name__ == "__main__":
demo.launch(server_port=7860)