jeongsoo commited on
Commit
4044010
Β·
1 Parent(s): cb86c03

Add greeting function to app.py

Browse files
Files changed (4) hide show
  1. .gitignore +1 -1
  2. RAG4_Voice_Fast +1 -0
  3. app.py +138 -180
  4. vito_stt.py +254 -0
.gitignore CHANGED
@@ -23,7 +23,7 @@ var/
23
  *.egg
24
 
25
  # 폴더
26
- documents/
27
  faiss_index/
28
  cached_data/
29
  preprocessed_index/
 
23
  *.egg
24
 
25
  # 폴더
26
+ !documents/
27
  faiss_index/
28
  cached_data/
29
  preprocessed_index/
RAG4_Voice_Fast ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 1f59ca46087f51b255eebf8f37d21083256683fe
app.py CHANGED
@@ -14,13 +14,14 @@ from pathlib import Path
14
  from langchain.schema import Document
15
 
16
  from config import (
17
- PDF_DIRECTORY, CACHE_DIRECTORY, CHUNK_SIZE, CHUNK_OVERLAP,
18
  LLM_MODEL, LOG_LEVEL, LOG_FILE, print_config, validate_config
19
  )
20
  from optimized_document_processor import OptimizedDocumentProcessor
21
  from vector_store import VectorStore
22
 
23
  import sys
 
24
  print("===== Script starting =====")
25
  sys.stdout.flush() # μ¦‰μ‹œ 좜λ ₯ κ°•μ œ
26
 
@@ -31,40 +32,42 @@ sys.stdout.flush()
31
  print("Config loaded!")
32
  sys.stdout.flush()
33
 
 
34
  # λ‘œκΉ… μ„€μ • κ°œμ„ 
35
  def setup_logging():
36
  """μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‘œκΉ… μ„€μ •"""
37
  # 둜그 레벨 μ„€μ •
38
  log_level = getattr(logging, LOG_LEVEL.upper(), logging.INFO)
39
-
40
  # 둜그 포맷 μ„€μ •
41
  log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
42
  formatter = logging.Formatter(log_format)
43
-
44
  # 루트 둜거 μ„€μ •
45
  root_logger = logging.getLogger()
46
  root_logger.setLevel(log_level)
47
-
48
  # ν•Έλ“€λŸ¬ μ΄ˆκΈ°ν™”
49
  # μ½˜μ†” ν•Έλ“€λŸ¬
50
  console_handler = logging.StreamHandler()
51
  console_handler.setFormatter(formatter)
52
  root_logger.addHandler(console_handler)
53
-
54
  # 파일 ν•Έλ“€λŸ¬ (νšŒμ „μ‹)
55
  try:
56
  file_handler = RotatingFileHandler(
57
- LOG_FILE,
58
- maxBytes=10*1024*1024, # 10 MB
59
  backupCount=5
60
  )
61
  file_handler.setFormatter(formatter)
62
  root_logger.addHandler(file_handler)
63
  except Exception as e:
64
  console_handler.warning(f"둜그 파일 μ„€μ • μ‹€νŒ¨: {e}, μ½˜μ†” λ‘œκΉ…λ§Œ μ‚¬μš©ν•©λ‹ˆλ‹€.")
65
-
66
  return logging.getLogger("AutoRAG")
67
 
 
68
  # 둜거 μ„€μ •
69
  logger = setup_logging()
70
 
@@ -99,12 +102,10 @@ if config_status["status"] != "valid":
99
  for warning in config_status["warnings"]:
100
  logger.warning(f"μ„€μ • κ²½κ³ : {warning}")
101
 
102
-
103
-
104
-
105
  # μ•ˆμ „ν•œ μž„ν¬νŠΈ
106
  try:
107
  from rag_chain import RAGChain
 
108
  RAG_CHAIN_AVAILABLE = True
109
  print("RAG 체인 λͺ¨λ“ˆ λ‘œλ“œ 성곡!")
110
  except ImportError as e:
@@ -117,6 +118,7 @@ except Exception as e:
117
  # 폴백 RAG κ΄€λ ¨ λͺ¨λ“ˆλ„ 미리 확인
118
  try:
119
  from fallback_rag_chain import FallbackRAGChain
 
120
  FALLBACK_AVAILABLE = True
121
  print("폴백 RAG 체인 λͺ¨λ“ˆ λ‘œλ“œ 성곡!")
122
  except ImportError as e:
@@ -125,6 +127,7 @@ except ImportError as e:
125
 
126
  try:
127
  from offline_fallback_rag import OfflineFallbackRAG
 
128
  OFFLINE_FALLBACK_AVAILABLE = True
129
  print("μ˜€ν”„λΌμΈ 폴백 RAG λͺ¨λ“ˆ λ‘œλ“œ 성곡!")
130
  except ImportError as e:
@@ -163,7 +166,7 @@ class AutoRAGChatApp:
163
  """
164
  try:
165
  logger.info("AutoRAGChatApp μ΄ˆκΈ°ν™” μ‹œμž‘")
166
-
167
  # 데이터 디렉토리 μ •μ˜ (μ„€μ •μ—μ„œ κ°€μ Έμ˜΄)
168
  # μ ˆλŒ€ 경둜둜 λ³€ν™˜ν•˜μ—¬ μ‚¬μš©
169
  self.pdf_directory = os.path.abspath(PDF_DIRECTORY)
@@ -173,10 +176,10 @@ class AutoRAGChatApp:
173
  self.vector_index_dir = os.path.join(self.cache_directory, "vector_index")
174
 
175
  logger.info(f"μ„€μ •λœ PDF 디렉토리 (μ ˆλŒ€ 경둜): {self.pdf_directory}")
176
-
177
  # 디렉토리 검증
178
  self._verify_pdf_directory()
179
-
180
  # 디렉토리 생성
181
  self._ensure_directories_exist()
182
 
@@ -211,9 +214,9 @@ class AutoRAGChatApp:
211
  # μ‹œμž‘ μ‹œ μžλ™μœΌλ‘œ λ¬Έμ„œ λ‘œλ“œ 및 처리
212
  logger.info("λ¬Έμ„œ μžλ™ λ‘œλ“œ 및 처리 μ‹œμž‘...")
213
  self.auto_process_documents()
214
-
215
  logger.info("AutoRAGChatApp μ΄ˆκΈ°ν™” μ™„λ£Œ")
216
-
217
  except Exception as e:
218
  logger.critical(f"μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ΄ˆκΈ°ν™” 쀑 μ‹¬κ°ν•œ 였λ₯˜: {e}", exc_info=True)
219
  # κΈ°λ³Έ μƒνƒœ μ„€μ •μœΌλ‘œ μ΅œμ†Œν•œμ˜ κΈ°λŠ₯ μœ μ§€
@@ -233,7 +236,7 @@ class AutoRAGChatApp:
233
  self.chunks_dir,
234
  self.vector_index_dir
235
  ]
236
-
237
  for directory in directories:
238
  try:
239
  os.makedirs(directory, exist_ok=True)
@@ -254,7 +257,7 @@ class AutoRAGChatApp:
254
  if not os.path.exists(file_path):
255
  logger.error(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
256
  raise FileNotFoundError(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
257
-
258
  try:
259
  logger.info(f"docling으둜 처리 μ‹œλ„: {file_path}")
260
 
@@ -287,14 +290,14 @@ class AutoRAGChatApp:
287
  except TimeoutError as te:
288
  logger.warning(f"docling 처리 μ‹œκ°„ 초과: {te}")
289
  logger.info("PyPDFLoader둜 λŒ€μ²΄ν•©λ‹ˆλ‹€.")
290
-
291
  # PyPDFLoader둜 λŒ€μ²΄
292
  try:
293
  return self.document_processor.process_pdf(file_path, use_docling=False)
294
  except Exception as inner_e:
295
  logger.error(f"PyPDFLoader 처리 였λ₯˜: {inner_e}", exc_info=True)
296
  raise DocumentProcessingError(f"PDF λ‘œλ”© μ‹€νŒ¨ (PyPDFLoader): {str(inner_e)}")
297
-
298
  except Exception as e:
299
  # docling 였λ₯˜ 확인
300
  error_str = str(e)
@@ -366,7 +369,7 @@ class AutoRAGChatApp:
366
  if not os.path.exists(file_path):
367
  logger.error(f"ν•΄μ‹œ 계산 μ‹€νŒ¨ - 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
368
  raise FileNotFoundError(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
369
-
370
  try:
371
  hasher = hashlib.md5()
372
  with open(file_path, 'rb') as f:
@@ -393,7 +396,7 @@ class AutoRAGChatApp:
393
  if not os.path.exists(file_path):
394
  logger.warning(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
395
  return False
396
-
397
  # μΈλ±μŠ€μ— 파일 쑴재 μ—¬λΆ€ 확인
398
  if file_path not in self.file_index:
399
  return False
@@ -480,13 +483,13 @@ class AutoRAGChatApp:
480
  if file_path not in self.file_index:
481
  logger.error(f"μΈλ±μŠ€μ— 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
482
  raise KeyError(f"μΈλ±μŠ€μ— 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
483
-
484
  chunks_path = self.file_index[file_path]['chunks_path']
485
-
486
  if not os.path.exists(chunks_path):
487
  logger.error(f"청크 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {chunks_path}")
488
  raise FileNotFoundError(f"청크 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {chunks_path}")
489
-
490
  try:
491
  with open(chunks_path, 'rb') as f:
492
  chunks = pickle.load(f)
@@ -511,15 +514,15 @@ class AutoRAGChatApp:
511
  except Exception as e:
512
  logger.error(f"PDF 디렉토리 생성 μ‹€νŒ¨: {e}")
513
  raise
514
-
515
  # 디렉토리인지 확인
516
  if not os.path.isdir(self.pdf_directory):
517
  logger.error(f"PDF κ²½λ‘œκ°€ 디렉토리가 μ•„λ‹™λ‹ˆλ‹€: {self.pdf_directory}")
518
  raise ConfigurationError(f"PDF κ²½λ‘œκ°€ 디렉토리가 μ•„λ‹™λ‹ˆλ‹€: {self.pdf_directory}")
519
-
520
  # PDF 파일 쑴재 확인
521
  pdf_files = [f for f in os.listdir(self.pdf_directory) if f.lower().endswith('.pdf')]
522
-
523
  if pdf_files:
524
  logger.info(f"PDF λ””λ ‰ν† λ¦¬μ—μ„œ {len(pdf_files)}개의 PDF νŒŒμΌμ„ μ°Ύμ•˜μŠ΅λ‹ˆλ‹€: {pdf_files}")
525
  else:
@@ -530,7 +533,7 @@ class AutoRAGChatApp:
530
  "documents",
531
  os.path.join(os.getcwd(), "documents")
532
  ]
533
-
534
  found_pdfs = False
535
  for alt_path in alternative_paths:
536
  if os.path.exists(alt_path) and os.path.isdir(alt_path):
@@ -540,11 +543,11 @@ class AutoRAGChatApp:
540
  self.pdf_directory = os.path.abspath(alt_path)
541
  found_pdfs = True
542
  break
543
-
544
  if not found_pdfs:
545
  logger.warning(f"PDF 디렉토리에 PDF 파일이 μ—†μŠ΅λ‹ˆλ‹€: {self.pdf_directory}")
546
  logger.info("PDF νŒŒμΌμ„ 디렉토리에 μΆ”κ°€ν•΄μ£Όμ„Έμš”.")
547
-
548
  except Exception as e:
549
  logger.error(f"PDF 디렉토리 검증 쀑 였λ₯˜: {e}", exc_info=True)
550
  raise
@@ -757,7 +760,7 @@ class AutoRAGChatApp:
757
  def _process_vector_index(self, new_files: List[str], updated_files: List[str]) -> None:
758
  """
759
  벑터 인덱슀 처리
760
-
761
  Args:
762
  new_files: μƒˆλ‘œ μΆ”κ°€λœ 파일 λͺ©λ‘
763
  updated_files: μ—…λ°μ΄νŠΈλœ 파일 λͺ©λ‘
@@ -1118,36 +1121,32 @@ class AutoRAGChatApp:
1118
  logger.error("Gradio 라이브러리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. pip install gradio둜 μ„€μΉ˜ν•˜μ„Έμš”.")
1119
  print("Gradio 라이브러리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. pip install gradio둜 μ„€μΉ˜ν•˜μ„Έμš”.")
1120
  return
1121
- # λ‚΄λΆ€ ν•¨μˆ˜λ“€μ΄ ν˜„μž¬ μΈμŠ€ν„΄μŠ€(self)에 μ ‘κ·Όν•  수 μžˆλ„λ‘ ν΄λ‘œμ € λ³€μˆ˜λ‘œ μ •μ˜
1122
  app_instance = self
1123
  try:
1124
  with gr.Blocks(title="PDF λ¬Έμ„œ 기반 RAG 챗봇") as app:
1125
  gr.Markdown("# PDF λ¬Έμ„œ 기반 RAG 챗봇")
1126
  gr.Markdown(f"* μ‚¬μš© 쀑인 LLM λͺ¨λΈ: **{LLM_MODEL}**")
1127
 
1128
- # μ—¬κΈ°λ₯Ό μˆ˜μ •: μ‹€μ œ 경둜 ν‘œμ‹œ
1129
- actual_pdf_dir = self.pdf_directory.replace('\\', '\\\\') if os.name == 'nt' else self.pdf_directory
 
 
1130
  gr.Markdown(f"* PDF λ¬Έμ„œ 폴더: **{actual_pdf_dir}**")
 
1131
  with gr.Row():
1132
  with gr.Column(scale=1):
1133
- # λ¬Έμ„œ μƒνƒœ μ„Ήμ…˜
1134
  status_box = gr.Textbox(
1135
  label="λ¬Έμ„œ 처리 μƒνƒœ",
1136
  value=self._get_status_message(),
1137
  lines=5,
1138
  interactive=False
1139
  )
1140
-
1141
- # μΊμ‹œ 관리 λ²„νŠΌ
1142
  refresh_button = gr.Button("λ¬Έμ„œ μƒˆλ‘œ 읽기", variant="primary")
1143
  reset_button = gr.Button("μΊμ‹œ μ΄ˆκΈ°ν™”", variant="stop")
1144
-
1145
- # μƒνƒœ 및 였λ₯˜ ν‘œμ‹œ
1146
  status_info = gr.Markdown(
1147
  value=f"μ‹œμŠ€ν…œ μƒνƒœ: {'μ΄ˆκΈ°ν™”λ¨' if self.is_initialized else 'μ΄ˆκΈ°ν™”λ˜μ§€ μ•ŠμŒ'}"
1148
  )
1149
-
1150
- # 처리된 파일 정보
1151
  with gr.Accordion("μΊμ‹œ μ„ΈλΆ€ 정보", open=False):
1152
  cache_info = gr.Textbox(
1153
  label="μΊμ‹œλœ 파일 정보",
@@ -1157,25 +1156,20 @@ class AutoRAGChatApp:
1157
  )
1158
 
1159
  with gr.Column(scale=2):
1160
- # μ±„νŒ… μΈν„°νŽ˜μ΄μŠ€
1161
  chatbot = gr.Chatbot(
1162
  label="λŒ€ν™” λ‚΄μš©",
1163
  bubble_full_width=False,
1164
  height=500,
1165
  show_copy_button=True
1166
  )
1167
-
1168
- # μŒμ„± λ…ΉμŒ UI μΆ”κ°€
1169
  with gr.Row():
1170
  with gr.Column(scale=4):
1171
- # 질문 μž…λ ₯κ³Ό 전솑 λ²„νŠΌ
1172
  query_box = gr.Textbox(
1173
  label="질문",
1174
  placeholder="처리된 λ¬Έμ„œ λ‚΄μš©μ— λŒ€ν•΄ μ§ˆλ¬Έν•˜μ„Έμš”...",
1175
  lines=2
1176
  )
1177
  with gr.Column(scale=1):
1178
- # μŒμ„± λ…ΉμŒ μ»΄ν¬λ„ŒνŠΈ
1179
  audio_input = gr.Audio(
1180
  sources=["microphone"],
1181
  type="numpy",
@@ -1186,153 +1180,118 @@ class AutoRAGChatApp:
1186
  submit_btn = gr.Button("전솑", variant="primary")
1187
  clear_chat_button = gr.Button("λŒ€ν™” μ΄ˆκΈ°ν™”")
1188
 
1189
- # μŒμ„± 인식 처리 ν•¨μˆ˜
1190
- # app.py λ‚΄ process_audio ν•¨μˆ˜ 보강
1191
- # Gradio μ•± 내에 μžˆλŠ” μŒμ„± 인식 처리 ν•¨μˆ˜ (원본)
1192
- def process_audio(audio):
1193
- logger.info("μŒμ„± 인식 처리 μ‹œμž‘...")
1194
- try:
1195
- from clova_stt import ClovaSTT
1196
- import numpy as np
1197
- import soundfile as sf
1198
- import tempfile
1199
- import os
1200
-
1201
- if audio is None:
1202
- return "μŒμ„±μ΄ λ…ΉμŒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
1203
-
1204
- # μ˜€λ””μ˜€ 데이터λ₯Ό μž„μ‹œ 파일둜 μ €μž₯
1205
- sr, y = audio
1206
- logger.info(f"μ˜€λ””μ˜€ λ…ΉμŒ 데이터 μˆ˜μ‹ : μƒ˜ν”Œλ ˆμ΄νŠΈ={sr}Hz, 길이={len(y)}μƒ˜ν”Œ")
1207
- if len(y) / sr < 1.0:
1208
- return "λ…ΉμŒλœ μŒμ„±μ΄ λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
1209
-
1210
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
1211
- temp_path = temp_file.name
1212
- sf.write(temp_path, y, sr, format="WAV")
1213
- logger.info(f"μž„μ‹œ WAV 파일 μ €μž₯됨: {temp_path}")
1214
-
1215
- # μŒμ„± 인식 μ‹€ν–‰
1216
- stt_client = ClovaSTT()
1217
- with open(temp_path, "rb") as f:
1218
- audio_bytes = f.read()
1219
- result = stt_client.recognize(audio_bytes)
1220
-
1221
- # μž„μ‹œ 파일 μ‚­μ œ
1222
- try:
1223
- os.unlink(temp_path)
1224
- logger.info("μž„μ‹œ μ˜€λ””μ˜€ 파일 μ‚­μ œλ¨")
1225
- except Exception as e:
1226
- logger.warning(f"μž„μ‹œ 파일 μ‚­μ œ μ‹€νŒ¨: {e}")
1227
-
1228
- if result["success"]:
1229
- recognized_text = result["text"]
1230
- logger.info(f"μŒμ„±μΈμ‹ 성곡: {recognized_text}")
1231
- return recognized_text
1232
- else:
1233
- error_msg = f"μŒμ„± 인식 μ‹€νŒ¨: {result.get('error', 'μ•Œ 수 μ—†λŠ” 였λ₯˜')}"
1234
- logger.error(error_msg)
1235
- return error_msg
1236
-
1237
- except ImportError as e:
1238
- logger.error(f"ν•„μš”ν•œ 라이브러리 λˆ„λ½: {e}")
1239
- return "μŒμ„±μΈμ‹μ— ν•„οΏ½οΏ½οΏ½ν•œ λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μ„€μΉ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. pip install soundfile numpy requestsλ₯Ό μ‹€ν–‰ν•΄μ£Όμ„Έμš”."
1240
- except Exception as e:
1241
- logger.error(f"μŒμ„± 처리 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
1242
- return f"μŒμ„± 처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
1243
-
1244
- # μƒˆλ‘œ μΆ”κ°€ν•  process_audio_and_submit ν•¨μˆ˜
1245
- def process_audio_and_submit(audio, chat_history):
1246
- """
1247
- λ…ΉμŒ μ •μ§€ μ‹œ μŒμ„± 인식 ν›„ μžλ™μœΌλ‘œ μ§ˆλ¬Έμ„ μ²˜λ¦¬ν•˜λŠ” ν•¨μˆ˜.
1248
- μž…λ ₯:
1249
- - audio: λ…ΉμŒ 데이터 (gr.Audio의 κ°’)
1250
- - chat_history: ν˜„μž¬ λŒ€ν™” 기둝 (gr.Chatbot의 κ°’)
1251
- 좜λ ₯:
1252
- - query_box: 빈 λ¬Έμžμ—΄ (질문 μž…λ ₯λž€ μ΄ˆκΈ°ν™”)
1253
- - chatbot: μ—…λ°μ΄νŠΈλœ λŒ€ν™” 기둝
1254
- """
1255
- recognized_text = process_audio(audio)
1256
-
1257
- # μŒμ„± 인식 κ²°κ³Όκ°€ 였λ₯˜ λ©”μ‹œμ§€μΈ 경우 κ·ΈλŒ€λ‘œ λ°˜ν™˜
1258
- if not recognized_text or recognized_text.startswith("μŒμ„± 인식 μ‹€νŒ¨") or recognized_text.startswith(
1259
- "μŒμ„± 처리 쀑 였λ₯˜"):
1260
- return recognized_text, chat_history
1261
-
1262
- # μΈμ‹λœ ν…μŠ€νŠΈλ₯Ό μ‚¬μš©ν•˜μ—¬ 질문 처리
1263
- return app_instance.process_query(recognized_text, chat_history)
1264
-
1265
- # κΈ°μ‘΄ update_ui_after_refresh ν•¨μˆ˜ μˆ˜μ • (self λŒ€μ‹  app_instance μ‚¬μš©)
1266
- def update_ui_after_refresh(result):
1267
- return (
1268
- result, # μƒνƒœ λ©”μ‹œμ§€
1269
- app_instance._get_status_message(), # μƒνƒœ λ°•μŠ€ μ—…λ°μ΄νŠΈ
1270
- f"μ‹œμŠ€ν…œ μƒνƒœ: {'μ΄ˆκΈ°ν™”λ¨' if app_instance.is_initialized else 'μ΄ˆκΈ°ν™”λ˜μ§€ μ•ŠμŒ'}", # μƒνƒœ 정보 μ—…λ°μ΄νŠΈ
1271
- app_instance._get_cache_info() # μΊμ‹œ 정보 μ—…λ°μ΄νŠΈ
1272
- )
1273
 
1274
- # --- Gradio 이벀트 ν•Έλ“€λŸ¬ μ„€μ • ---
1275
- # 예: audio_input μ»΄ν¬λ„ŒνŠΈμ˜ stop_recording 이벀트λ₯Ό μ•„λž˜μ™€ 같이 μˆ˜μ •
1276
- audio_input.stop_recording(
1277
- fn=process_audio_and_submit,
1278
- inputs=[audio_input, chatbot],
1279
- outputs=[query_box, chatbot]
1280
- )
1281
 
1282
- # μŒμ„± 인식 κ²°κ³Όλ₯Ό 질문 μƒμžμ— μ—…λ°μ΄νŠΈ
1283
- audio_input.stop_recording(
1284
- fn=process_audio,
1285
- inputs=[audio_input],
1286
- outputs=[query_box]
1287
- )
1288
 
1289
- # λ¬Έμ„œ μƒˆλ‘œ 읽기 λ²„νŠΌ
1290
- refresh_button.click(
1291
- fn=lambda: update_ui_after_refresh(self.auto_process_documents()),
1292
- inputs=[],
1293
- outputs=[status_box, status_box, status_info, cache_info]
1294
- )
1295
 
1296
- # μΊμ‹œ μ΄ˆκΈ°ν™” λ²„νŠΌ
1297
- def reset_and_process():
1298
- reset_result = self.reset_cache()
1299
- process_result = self.auto_process_documents()
1300
- return update_ui_after_refresh(f"{reset_result}\n\n{process_result}")
1301
 
1302
- reset_button.click(
1303
- fn=reset_and_process,
1304
- inputs=[],
1305
- outputs=[status_box, status_box, status_info, cache_info]
1306
- )
1307
 
1308
- # 전솑 λ²„νŠΌ 클릭 이벀트
1309
- submit_btn.click(
1310
- fn=self.process_query,
1311
- inputs=[query_box, chatbot],
1312
- outputs=[query_box, chatbot]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1313
  )
1314
 
1315
- # μ—”ν„°ν‚€ μž…λ ₯ 이벀트
1316
- query_box.submit(
1317
- fn=self.process_query,
1318
- inputs=[query_box, chatbot],
1319
- outputs=[query_box, chatbot]
1320
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1321
 
1322
- # λŒ€ν™” μ΄ˆκΈ°ν™” λ²„νŠΌ
1323
- clear_chat_button.click(
1324
- fn=lambda: [],
1325
- outputs=[chatbot]
1326
- )
1327
 
1328
- # μ•± μ‹€ν–‰
1329
- app.launch(share=False)
1330
  except Exception as e:
1331
  logger.error(f"Gradio μ•± μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
1332
  print(f"Gradio μ•± μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {e}")
1333
 
1334
 
1335
-
1336
  def _get_status_message(self) -> str:
1337
  """
1338
  ν˜„μž¬ 처리 μƒνƒœ λ©”μ‹œμ§€ 생성
@@ -1448,7 +1407,6 @@ class AutoRAGChatApp:
1448
  return file_info
1449
 
1450
 
1451
-
1452
  if __name__ == "__main__":
1453
  app = AutoRAGChatApp()
1454
  app.launch_app()
 
14
  from langchain.schema import Document
15
 
16
  from config import (
17
+ PDF_DIRECTORY, CACHE_DIRECTORY, CHUNK_SIZE, CHUNK_OVERLAP,
18
  LLM_MODEL, LOG_LEVEL, LOG_FILE, print_config, validate_config
19
  )
20
  from optimized_document_processor import OptimizedDocumentProcessor
21
  from vector_store import VectorStore
22
 
23
  import sys
24
+
25
  print("===== Script starting =====")
26
  sys.stdout.flush() # μ¦‰μ‹œ 좜λ ₯ κ°•μ œ
27
 
 
32
  print("Config loaded!")
33
  sys.stdout.flush()
34
 
35
+
36
  # λ‘œκΉ… μ„€μ • κ°œμ„ 
37
  def setup_logging():
38
  """μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‘œκΉ… μ„€μ •"""
39
  # 둜그 레벨 μ„€μ •
40
  log_level = getattr(logging, LOG_LEVEL.upper(), logging.INFO)
41
+
42
  # 둜그 포맷 μ„€μ •
43
  log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
44
  formatter = logging.Formatter(log_format)
45
+
46
  # 루트 둜거 μ„€μ •
47
  root_logger = logging.getLogger()
48
  root_logger.setLevel(log_level)
49
+
50
  # ν•Έλ“€λŸ¬ μ΄ˆκΈ°ν™”
51
  # μ½˜μ†” ν•Έλ“€λŸ¬
52
  console_handler = logging.StreamHandler()
53
  console_handler.setFormatter(formatter)
54
  root_logger.addHandler(console_handler)
55
+
56
  # 파일 ν•Έλ“€λŸ¬ (νšŒμ „μ‹)
57
  try:
58
  file_handler = RotatingFileHandler(
59
+ LOG_FILE,
60
+ maxBytes=10 * 1024 * 1024, # 10 MB
61
  backupCount=5
62
  )
63
  file_handler.setFormatter(formatter)
64
  root_logger.addHandler(file_handler)
65
  except Exception as e:
66
  console_handler.warning(f"둜그 파일 μ„€μ • μ‹€νŒ¨: {e}, μ½˜μ†” λ‘œκΉ…λ§Œ μ‚¬μš©ν•©λ‹ˆλ‹€.")
67
+
68
  return logging.getLogger("AutoRAG")
69
 
70
+
71
  # 둜거 μ„€μ •
72
  logger = setup_logging()
73
 
 
102
  for warning in config_status["warnings"]:
103
  logger.warning(f"μ„€μ • κ²½κ³ : {warning}")
104
 
 
 
 
105
  # μ•ˆμ „ν•œ μž„ν¬νŠΈ
106
  try:
107
  from rag_chain import RAGChain
108
+
109
  RAG_CHAIN_AVAILABLE = True
110
  print("RAG 체인 λͺ¨λ“ˆ λ‘œλ“œ 성곡!")
111
  except ImportError as e:
 
118
  # 폴백 RAG κ΄€λ ¨ λͺ¨λ“ˆλ„ 미리 확인
119
  try:
120
  from fallback_rag_chain import FallbackRAGChain
121
+
122
  FALLBACK_AVAILABLE = True
123
  print("폴백 RAG 체인 λͺ¨λ“ˆ λ‘œλ“œ 성곡!")
124
  except ImportError as e:
 
127
 
128
  try:
129
  from offline_fallback_rag import OfflineFallbackRAG
130
+
131
  OFFLINE_FALLBACK_AVAILABLE = True
132
  print("μ˜€ν”„λΌμΈ 폴백 RAG λͺ¨λ“ˆ λ‘œλ“œ 성곡!")
133
  except ImportError as e:
 
166
  """
167
  try:
168
  logger.info("AutoRAGChatApp μ΄ˆκΈ°ν™” μ‹œμž‘")
169
+
170
  # 데이터 디렉토리 μ •μ˜ (μ„€μ •μ—μ„œ κ°€μ Έμ˜΄)
171
  # μ ˆλŒ€ 경둜둜 λ³€ν™˜ν•˜μ—¬ μ‚¬μš©
172
  self.pdf_directory = os.path.abspath(PDF_DIRECTORY)
 
176
  self.vector_index_dir = os.path.join(self.cache_directory, "vector_index")
177
 
178
  logger.info(f"μ„€μ •λœ PDF 디렉토리 (μ ˆλŒ€ 경둜): {self.pdf_directory}")
179
+
180
  # 디렉토리 검증
181
  self._verify_pdf_directory()
182
+
183
  # 디렉토리 생성
184
  self._ensure_directories_exist()
185
 
 
214
  # μ‹œμž‘ μ‹œ μžλ™μœΌλ‘œ λ¬Έμ„œ λ‘œλ“œ 및 처리
215
  logger.info("λ¬Έμ„œ μžλ™ λ‘œλ“œ 및 처리 μ‹œμž‘...")
216
  self.auto_process_documents()
217
+
218
  logger.info("AutoRAGChatApp μ΄ˆκΈ°ν™” μ™„λ£Œ")
219
+
220
  except Exception as e:
221
  logger.critical(f"μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ΄ˆκΈ°ν™” 쀑 μ‹¬κ°ν•œ 였λ₯˜: {e}", exc_info=True)
222
  # κΈ°λ³Έ μƒνƒœ μ„€μ •μœΌλ‘œ μ΅œμ†Œν•œμ˜ κΈ°λŠ₯ μœ μ§€
 
236
  self.chunks_dir,
237
  self.vector_index_dir
238
  ]
239
+
240
  for directory in directories:
241
  try:
242
  os.makedirs(directory, exist_ok=True)
 
257
  if not os.path.exists(file_path):
258
  logger.error(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
259
  raise FileNotFoundError(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
260
+
261
  try:
262
  logger.info(f"docling으둜 처리 μ‹œλ„: {file_path}")
263
 
 
290
  except TimeoutError as te:
291
  logger.warning(f"docling 처리 μ‹œκ°„ 초과: {te}")
292
  logger.info("PyPDFLoader둜 λŒ€μ²΄ν•©λ‹ˆλ‹€.")
293
+
294
  # PyPDFLoader둜 λŒ€μ²΄
295
  try:
296
  return self.document_processor.process_pdf(file_path, use_docling=False)
297
  except Exception as inner_e:
298
  logger.error(f"PyPDFLoader 처리 였λ₯˜: {inner_e}", exc_info=True)
299
  raise DocumentProcessingError(f"PDF λ‘œλ”© μ‹€νŒ¨ (PyPDFLoader): {str(inner_e)}")
300
+
301
  except Exception as e:
302
  # docling 였λ₯˜ 확인
303
  error_str = str(e)
 
369
  if not os.path.exists(file_path):
370
  logger.error(f"ν•΄μ‹œ 계산 μ‹€νŒ¨ - 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
371
  raise FileNotFoundError(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
372
+
373
  try:
374
  hasher = hashlib.md5()
375
  with open(file_path, 'rb') as f:
 
396
  if not os.path.exists(file_path):
397
  logger.warning(f"파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
398
  return False
399
+
400
  # μΈλ±μŠ€μ— 파일 쑴재 μ—¬λΆ€ 확인
401
  if file_path not in self.file_index:
402
  return False
 
483
  if file_path not in self.file_index:
484
  logger.error(f"μΈλ±μŠ€μ— 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
485
  raise KeyError(f"μΈλ±μŠ€μ— 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {file_path}")
486
+
487
  chunks_path = self.file_index[file_path]['chunks_path']
488
+
489
  if not os.path.exists(chunks_path):
490
  logger.error(f"청크 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {chunks_path}")
491
  raise FileNotFoundError(f"청크 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŒ: {chunks_path}")
492
+
493
  try:
494
  with open(chunks_path, 'rb') as f:
495
  chunks = pickle.load(f)
 
514
  except Exception as e:
515
  logger.error(f"PDF 디렉토리 생성 μ‹€νŒ¨: {e}")
516
  raise
517
+
518
  # 디렉토리인지 확인
519
  if not os.path.isdir(self.pdf_directory):
520
  logger.error(f"PDF κ²½λ‘œκ°€ 디렉토리가 μ•„λ‹™λ‹ˆλ‹€: {self.pdf_directory}")
521
  raise ConfigurationError(f"PDF κ²½λ‘œκ°€ 디렉토리가 μ•„λ‹™λ‹ˆλ‹€: {self.pdf_directory}")
522
+
523
  # PDF 파일 쑴재 확인
524
  pdf_files = [f for f in os.listdir(self.pdf_directory) if f.lower().endswith('.pdf')]
525
+
526
  if pdf_files:
527
  logger.info(f"PDF λ””λ ‰ν† λ¦¬μ—μ„œ {len(pdf_files)}개의 PDF νŒŒμΌμ„ μ°Ύμ•˜μŠ΅λ‹ˆλ‹€: {pdf_files}")
528
  else:
 
533
  "documents",
534
  os.path.join(os.getcwd(), "documents")
535
  ]
536
+
537
  found_pdfs = False
538
  for alt_path in alternative_paths:
539
  if os.path.exists(alt_path) and os.path.isdir(alt_path):
 
543
  self.pdf_directory = os.path.abspath(alt_path)
544
  found_pdfs = True
545
  break
546
+
547
  if not found_pdfs:
548
  logger.warning(f"PDF 디렉토리에 PDF 파일이 μ—†μŠ΅λ‹ˆλ‹€: {self.pdf_directory}")
549
  logger.info("PDF νŒŒμΌμ„ 디렉토리에 μΆ”κ°€ν•΄μ£Όμ„Έμš”.")
550
+
551
  except Exception as e:
552
  logger.error(f"PDF 디렉토리 검증 쀑 였λ₯˜: {e}", exc_info=True)
553
  raise
 
760
  def _process_vector_index(self, new_files: List[str], updated_files: List[str]) -> None:
761
  """
762
  벑터 인덱슀 처리
763
+
764
  Args:
765
  new_files: μƒˆλ‘œ μΆ”κ°€λœ 파일 λͺ©λ‘
766
  updated_files: μ—…λ°μ΄νŠΈλœ 파일 λͺ©λ‘
 
1121
  logger.error("Gradio 라이브러리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. pip install gradio둜 μ„€μΉ˜ν•˜μ„Έμš”.")
1122
  print("Gradio 라이브러리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. pip install gradio둜 μ„€μΉ˜ν•˜μ„Έμš”.")
1123
  return
1124
+
1125
  app_instance = self
1126
  try:
1127
  with gr.Blocks(title="PDF λ¬Έμ„œ 기반 RAG 챗봇") as app:
1128
  gr.Markdown("# PDF λ¬Έμ„œ 기반 RAG 챗봇")
1129
  gr.Markdown(f"* μ‚¬μš© 쀑인 LLM λͺ¨λΈ: **{LLM_MODEL}**")
1130
 
1131
+ actual_pdf_dir = (
1132
+ self.pdf_directory.replace("\\", "\\\\")
1133
+ if os.name == 'nt' else self.pdf_directory
1134
+ )
1135
  gr.Markdown(f"* PDF λ¬Έμ„œ 폴더: **{actual_pdf_dir}**")
1136
+
1137
  with gr.Row():
1138
  with gr.Column(scale=1):
 
1139
  status_box = gr.Textbox(
1140
  label="λ¬Έμ„œ 처리 μƒνƒœ",
1141
  value=self._get_status_message(),
1142
  lines=5,
1143
  interactive=False
1144
  )
 
 
1145
  refresh_button = gr.Button("λ¬Έμ„œ μƒˆλ‘œ 읽기", variant="primary")
1146
  reset_button = gr.Button("μΊμ‹œ μ΄ˆκΈ°ν™”", variant="stop")
 
 
1147
  status_info = gr.Markdown(
1148
  value=f"μ‹œμŠ€ν…œ μƒνƒœ: {'μ΄ˆκΈ°ν™”λ¨' if self.is_initialized else 'μ΄ˆκΈ°ν™”λ˜μ§€ μ•ŠμŒ'}"
1149
  )
 
 
1150
  with gr.Accordion("μΊμ‹œ μ„ΈλΆ€ 정보", open=False):
1151
  cache_info = gr.Textbox(
1152
  label="μΊμ‹œλœ 파일 정보",
 
1156
  )
1157
 
1158
  with gr.Column(scale=2):
 
1159
  chatbot = gr.Chatbot(
1160
  label="λŒ€ν™” λ‚΄μš©",
1161
  bubble_full_width=False,
1162
  height=500,
1163
  show_copy_button=True
1164
  )
 
 
1165
  with gr.Row():
1166
  with gr.Column(scale=4):
 
1167
  query_box = gr.Textbox(
1168
  label="질문",
1169
  placeholder="처리된 λ¬Έμ„œ λ‚΄μš©μ— λŒ€ν•΄ μ§ˆλ¬Έν•˜μ„Έμš”...",
1170
  lines=2
1171
  )
1172
  with gr.Column(scale=1):
 
1173
  audio_input = gr.Audio(
1174
  sources=["microphone"],
1175
  type="numpy",
 
1180
  submit_btn = gr.Button("전솑", variant="primary")
1181
  clear_chat_button = gr.Button("λŒ€ν™” μ΄ˆκΈ°ν™”")
1182
 
1183
+ # VITO STT용 μŒμ„± 처리 ν•¨μˆ˜
1184
+ def process_audio(audio):
1185
+ logger.info("μŒμ„± 인식 처리 μ‹œμž‘...")
1186
+ try:
1187
+ from vito_stt import VitoSTT
1188
+ import soundfile as sf
1189
+ import tempfile
1190
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
 
1192
+ if audio is None:
1193
+ return "μŒμ„±μ΄ λ…ΉμŒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
 
 
 
 
 
1194
 
1195
+ sr, y = audio
1196
+ logger.info(f"μ˜€λ””μ˜€ λ…ΉμŒ 데이터 μˆ˜μ‹ : μƒ˜ν”Œλ ˆμ΄νŠΈ={sr}Hz, 길이={len(y)}μƒ˜ν”Œ")
1197
+ if len(y) / sr < 1.0:
1198
+ return "λ…ΉμŒλœ μŒμ„±μ΄ λ„ˆλ¬΄ μ§§μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."
 
 
1199
 
1200
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
1201
+ tmp_path = tmp.name
1202
+ sf.write(tmp_path, y, sr, format="WAV")
1203
+ logger.info(f"μž„μ‹œ WAV 파일 μ €μž₯됨: {tmp_path}")
 
 
1204
 
1205
+ vito = VitoSTT()
1206
+ with open(tmp_path, "rb") as f:
1207
+ audio_bytes = f.read()
1208
+ result = vito.transcribe_audio(audio_bytes, language="ko")
 
1209
 
1210
+ try:
1211
+ os.unlink(tmp_path)
1212
+ logger.info("μž„μ‹œ μ˜€λ””μ˜€ 파일 μ‚­μ œλ¨")
1213
+ except Exception as e:
1214
+ logger.warning(f"μž„μ‹œ 파일 μ‚­μ œ μ‹€νŒ¨: {e}")
1215
 
1216
+ if result.get("success"):
1217
+ recognized_text = result.get("text", "")
1218
+ logger.info(f"μŒμ„±μΈμ‹ 성곡: {recognized_text}")
1219
+ return recognized_text
1220
+ else:
1221
+ error_msg = f"μŒμ„± 인식 μ‹€νŒ¨: {result.get('error', 'μ•Œ 수 μ—†λŠ” 였λ₯˜')}"
1222
+ logger.error(error_msg)
1223
+ return error_msg
1224
+
1225
+ except ImportError as e:
1226
+ logger.error(f"ν•„μš”ν•œ 라이브러리 λˆ„λ½: {e}")
1227
+ return ("μŒμ„±μΈμ‹μ— ν•„μš”ν•œ λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μ„€μΉ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. "
1228
+ "pip install soundfile numpy requests λ₯Ό μ‹€ν–‰ν•΄μ£Όμ„Έμš”.")
1229
+ except Exception as e:
1230
+ logger.error(f"μŒμ„± 처리 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
1231
+ return f"μŒμ„± 처리 쀑 였λ₯˜ λ°œμƒ: {str(e)}"
1232
+
1233
+ # μŒμ„± 인식 ν›„ μžλ™ 질문 처리
1234
+ def process_audio_and_submit(audio, chat_history):
1235
+ recognized_text = process_audio(audio)
1236
+ if (not recognized_text
1237
+ or recognized_text.startswith("μŒμ„± 인식 μ‹€νŒ¨")
1238
+ or recognized_text.startswith("μŒμ„± 처리 쀑 였λ₯˜")):
1239
+ return recognized_text, chat_history
1240
+ return app_instance.process_query(recognized_text, chat_history)
1241
+
1242
+ def update_ui_after_refresh(result):
1243
+ return (
1244
+ result,
1245
+ app_instance._get_status_message(),
1246
+ f"μ‹œμŠ€ν…œ μƒνƒœ: {'μ΄ˆκΈ°ν™”λ¨' if app_instance.is_initialized else 'μ΄ˆκΈ°ν™”λ˜μ§€ μ•ŠμŒ'}",
1247
+ app_instance._get_cache_info()
1248
  )
1249
 
1250
+ # 이벀트 ν•Έλ“€λŸ¬ 바인딩
1251
+ audio_input.stop_recording(
1252
+ fn=process_audio_and_submit,
1253
+ inputs=[audio_input, chatbot],
1254
+ outputs=[query_box, chatbot]
1255
+ )
1256
+ audio_input.stop_recording(
1257
+ fn=process_audio,
1258
+ inputs=[audio_input],
1259
+ outputs=[query_box]
1260
+ )
1261
+ refresh_button.click(
1262
+ fn=lambda: update_ui_after_refresh(self.auto_process_documents()),
1263
+ inputs=[],
1264
+ outputs=[status_box, status_box, status_info, cache_info]
1265
+ )
1266
+ reset_button.click(
1267
+ fn=lambda: update_ui_after_refresh(
1268
+ f"{self.reset_cache()}\n\n{self.auto_process_documents()}"
1269
+ ),
1270
+ inputs=[],
1271
+ outputs=[status_box, status_box, status_info, cache_info]
1272
+ )
1273
+ submit_btn.click(
1274
+ fn=self.process_query,
1275
+ inputs=[query_box, chatbot],
1276
+ outputs=[query_box, chatbot]
1277
+ )
1278
+ query_box.submit(
1279
+ fn=self.process_query,
1280
+ inputs=[query_box, chatbot],
1281
+ outputs=[query_box, chatbot]
1282
+ )
1283
+ clear_chat_button.click(
1284
+ fn=lambda: [],
1285
+ outputs=[chatbot]
1286
+ )
1287
 
1288
+ app.launch(share=False)
 
 
 
 
1289
 
 
 
1290
  except Exception as e:
1291
  logger.error(f"Gradio μ•± μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
1292
  print(f"Gradio μ•± μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {e}")
1293
 
1294
 
 
1295
  def _get_status_message(self) -> str:
1296
  """
1297
  ν˜„μž¬ 처리 μƒνƒœ λ©”μ‹œμ§€ 생성
 
1407
  return file_info
1408
 
1409
 
 
1410
  if __name__ == "__main__":
1411
  app = AutoRAGChatApp()
1412
  app.launch_app()
vito_stt.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ VITO APIλ₯Ό μ‚¬μš©ν•œ μŒμ„± 인식(STT) λͺ¨λ“ˆ
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ import requests
9
+ import json
10
+ import time # time import μΆ”κ°€
11
+ from dotenv import load_dotenv
12
+
13
+ # ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ
14
+ load_dotenv()
15
+
16
+ # 둜거 μ„€μ • (app.py와 κ³΅μœ ν•˜κ±°λ‚˜ λ…λ¦½μ μœΌλ‘œ μ„€μ • κ°€λŠ₯)
17
+ # μ—¬κΈ°μ„œλŠ” 독립적인 둜거λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. ν•„μš”μ‹œ app.py의 둜거λ₯Ό μ‚¬μš©ν•˜λ„λ‘ μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
18
+ logger = logging.getLogger("VitoSTT")
19
+ # κΈ°λ³Έ λ‘œκΉ… 레벨 μ„€μ • (ν•Έλ“€λŸ¬κ°€ μ—†μœΌλ©΄ 좜λ ₯이 μ•ˆλ  수 μžˆμœΌλ―€λ‘œ κΈ°λ³Έ ν•Έλ“€λŸ¬ μΆ”κ°€ κ³ λ €)
20
+ if not logger.hasHandlers():
21
+ handler = logging.StreamHandler()
22
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
23
+ handler.setFormatter(formatter)
24
+ logger.addHandler(handler)
25
+ logger.setLevel(logging.INFO) # κΈ°λ³Έ 레벨 INFO둜 μ„€μ •
26
+
27
+ class VitoSTT:
28
+ """VITO STT API 래퍼 클래슀"""
29
+
30
+ def __init__(self):
31
+ """VITO STT 클래슀 μ΄ˆκΈ°ν™”"""
32
+ self.client_id = os.getenv("VITO_CLIENT_ID")
33
+ self.client_secret = os.getenv("VITO_CLIENT_SECRET")
34
+
35
+ if not self.client_id or not self.client_secret:
36
+ logger.warning("VITO API 인증 정보가 .env νŒŒμΌμ— μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
37
+ logger.warning("VITO_CLIENT_ID와 VITO_CLIENT_SECRETλ₯Ό ν™•μΈν•˜μ„Έμš”.")
38
+ # μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚€κ±°λ‚˜, κΈ°λŠ₯ μ‚¬μš© μ‹œμ μ— μ²΄ν¬ν•˜λ„λ‘ λ‘˜ 수 μžˆμŠ΅λ‹ˆλ‹€.
39
+ # μ—¬κΈ°μ„œλŠ” 경고만 ν•˜κ³  λ„˜μ–΄κ°‘λ‹ˆλ‹€.
40
+ else:
41
+ logger.info("VITO STT API ν΄λΌμ΄μ–ΈνŠΈ ID/Secret λ‘œλ“œ μ™„λ£Œ.")
42
+
43
+ # API μ—”λ“œν¬μΈνŠΈ
44
+ self.token_url = "https://openapi.vito.ai/v1/authenticate"
45
+ self.stt_url = "https://openapi.vito.ai/v1/transcribe"
46
+
47
+ # μ•‘μ„ΈμŠ€ 토큰
48
+ self.access_token = None
49
+ self._token_expires_at = 0 # 토큰 만료 μ‹œκ°„ 좔적 (선택적 κ°œμ„ )
50
+
51
+ def get_access_token(self):
52
+ """VITO API μ•‘μ„ΈμŠ€ 토큰 νšλ“"""
53
+ # ν˜„μž¬ μ‹œκ°„μ„ 가져와 토큰 만료 μ—¬λΆ€ 확인 (선택적 κ°œμ„ )
54
+ # now = time.time()
55
+ # if self.access_token and now < self._token_expires_at:
56
+ # logger.debug("κΈ°μ‘΄ VITO API 토큰 μ‚¬μš©")
57
+ # return self.access_token
58
+
59
+ if not self.client_id or not self.client_secret:
60
+ logger.error("API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•„ 토큰을 νšλ“ν•  수 μ—†μŠ΅λ‹ˆλ‹€.")
61
+ raise ValueError("VITO API 인증 정보가 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
62
+
63
+ logger.info("VITO API μ•‘μ„ΈμŠ€ 토큰 μš”μ²­ 쀑...")
64
+ try:
65
+ response = requests.post(
66
+ self.token_url,
67
+ data={"client_id": self.client_id, "client_secret": self.client_secret},
68
+ timeout=10 # νƒ€μž„μ•„μ›ƒ μ„€μ •
69
+ )
70
+ response.raise_for_status() # HTTP 였λ₯˜ λ°œμƒ μ‹œ μ˜ˆμ™Έ λ°œμƒ
71
+
72
+ result = response.json()
73
+ self.access_token = result.get("access_token")
74
+ expires_in = result.get("expires_in", 3600) # 만료 μ‹œκ°„ (초), κΈ°λ³Έκ°’ 1μ‹œκ°„
75
+ self._token_expires_at = time.time() + expires_in - 60 # 60초 μ—¬μœ 
76
+
77
+ if not self.access_token:
78
+ logger.error("VITO API μ‘λ‹΅μ—μ„œ 토큰을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
79
+ raise ValueError("VITO API 토큰을 λ°›μ•„μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
80
+
81
+ logger.info("VITO API μ•‘μ„ΈμŠ€ 토큰 νšλ“ 성곡")
82
+ return self.access_token
83
+ except requests.exceptions.Timeout:
84
+ logger.error(f"VITO API 토큰 νšλ“ μ‹œκ°„ 초과: {self.token_url}")
85
+ raise TimeoutError("VITO API 토큰 νšλ“ μ‹œκ°„ 초과")
86
+ except requests.exceptions.RequestException as e:
87
+ logger.error(f"VITO API 토큰 νšλ“ μ‹€νŒ¨: {e}")
88
+ if hasattr(e, 'response') and e.response is not None:
89
+ logger.error(f"응닡 μ½”λ“œ: {e.response.status_code}, λ‚΄μš©: {e.response.text}")
90
+ raise ConnectionError(f"VITO API 토큰 νšλ“ μ‹€νŒ¨: {e}")
91
+
92
+
93
+ def transcribe_audio(self, audio_bytes, language="ko"):
94
+ """
95
+ μ˜€λ””μ˜€ λ°”μ΄νŠΈ 데이터λ₯Ό ν…μŠ€νŠΈλ‘œ λ³€ν™˜
96
+
97
+ Args:
98
+ audio_bytes: μ˜€λ””μ˜€ 파일 λ°”μ΄νŠΈ 데이터
99
+ language: μ–Έμ–΄ μ½”λ“œ (κΈ°λ³Έκ°’: 'ko')
100
+
101
+ Returns:
102
+ μΈμ‹λœ ν…μŠ€νŠΈ λ˜λŠ” 였λ₯˜ λ©”μ‹œμ§€λ₯Ό ν¬ν•¨ν•œ λ”•μ…”λ„ˆλ¦¬
103
+ {'success': True, 'text': 'μΈμ‹λœ ν…μŠ€νŠΈ'}
104
+ {'success': False, 'error': '였λ₯˜ λ©”μ‹œμ§€', 'details': '상세 λ‚΄μš©'}
105
+ """
106
+ if not self.client_id or not self.client_secret:
107
+ logger.error("API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
108
+ return {"success": False, "error": "API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}
109
+
110
+ try:
111
+ # 토큰 νšλ“ λ˜λŠ” κ°±μ‹ 
112
+ # (선택적 κ°œμ„ : 만료 μ‹œκ°„ 체크 둜직 μΆ”κ°€ μ‹œ self._token_expires_at μ‚¬μš©)
113
+ if not self.access_token: # or time.time() >= self._token_expires_at:
114
+ logger.info("VITO API 토큰 νšλ“/κ°±μ‹  μ‹œλ„...")
115
+ self.get_access_token()
116
+
117
+ headers = {
118
+ "Authorization": f"Bearer {self.access_token}"
119
+ }
120
+
121
+ files = {
122
+ "file": ("audio_file", audio_bytes) # 파일λͺ… νŠœν”Œλ‘œ 전달
123
+ }
124
+
125
+ # API μ„€μ •κ°’ (ν•„μš”μ— 따라 μˆ˜μ •)
126
+ config = {
127
+ "diarization": {"use_verification": False},
128
+ "use_multi_channel": False,
129
+ "use_itn": True, # Inverse Text Normalization (숫자, λ‚ μ§œ λ“± λ³€ν™˜)
130
+ "use_disfluency_filter": True, # ν•„λŸ¬ (음, μ•„...) 제거
131
+ "use_profanity_filter": False, # 비속어 필터링
132
+ "language": language,
133
+ # "type": "audio" # type νŒŒλΌλ―Έν„°λŠ” VITO λ¬Έμ„œμƒ ν•„μˆ˜ μ•„λ‹˜ (μžλ™ 감지)
134
+ }
135
+ data = {"config": json.dumps(config)}
136
+
137
+ logger.info(f"VITO STT API ({self.stt_url}) μš”μ²­ 전솑 쀑...")
138
+ response = requests.post(
139
+ self.stt_url,
140
+ headers=headers,
141
+ files=files,
142
+ data=data,
143
+ timeout=20 # μ—…λ‘œλ“œ νƒ€μž„μ•„μ›ƒ
144
+ )
145
+ response.raise_for_status()
146
+
147
+ result = response.json()
148
+ job_id = result.get("id")
149
+
150
+ if not job_id:
151
+ logger.error("VITO API μž‘μ—… IDλ₯Ό λ°›μ•„μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
152
+ return {"success": False, "error": "VITO API μž‘μ—… IDλ₯Ό λ°›μ•„μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."}
153
+
154
+ logger.info(f"VITO STT μž‘μ—… ID: {job_id}, κ²°κ³Ό 확인 μ‹œμž‘...")
155
+
156
+ # κ²°κ³Ό 확인 URL
157
+ transcript_url = f"{self.stt_url}/{job_id}"
158
+ max_tries = 15 # μ΅œλŒ€ μ‹œλ„ 횟수 증가
159
+ wait_time = 2 # λŒ€κΈ° μ‹œκ°„ 증가 (초)
160
+
161
+ for try_count in range(max_tries):
162
+ time.sleep(wait_time) # API λΆ€ν•˜ κ°μ†Œ μœ„ν•΄ λŒ€κΈ°
163
+ logger.debug(f"κ²°κ³Ό 확인 μ‹œλ„ ({try_count + 1}/{max_tries}) - URL: {transcript_url}")
164
+ get_response = requests.get(
165
+ transcript_url,
166
+ headers=headers,
167
+ timeout=10 # κ²°κ³Ό 확인 νƒ€μž„μ•„μ›ƒ
168
+ )
169
+ get_response.raise_for_status()
170
+
171
+ result = get_response.json()
172
+ status = result.get("status")
173
+ logger.debug(f"ν˜„μž¬ μƒνƒœ: {status}")
174
+
175
+ if status == "completed":
176
+ # κ²°κ³Ό μΆ”μΆœ (utterances ꡬ쑰 확인 ν•„μš”)
177
+ utterances = result.get("results", {}).get("utterances", [])
178
+ if utterances:
179
+ # 전체 ν…μŠ€νŠΈλ₯Ό ν•˜λ‚˜λ‘œ ν•©μΉ¨
180
+ transcript = " ".join([seg.get("msg", "") for seg in utterances if seg.get("msg")]).strip()
181
+ logger.info(f"VITO STT 인식 성곡 (일뢀): {transcript[:50]}...")
182
+ return {
183
+ "success": True,
184
+ "text": transcript
185
+ # "raw_result": result # ν•„μš”μ‹œ 전체 κ²°κ³Ό λ°˜ν™˜
186
+ }
187
+ else:
188
+ logger.warning("VITO STT μ™„λ£Œλ˜μ—ˆμœΌλ‚˜ κ²°κ³Ό utterancesκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
189
+ return {"success": True, "text": ""} # μ„±κ³΅μ΄μ§€λ§Œ ν…μŠ€νŠΈ μ—†μŒ
190
+
191
+ elif status == "failed":
192
+ error_msg = f"VITO API λ³€ν™˜ μ‹€νŒ¨: {result.get('message', 'μ•Œ 수 μ—†λŠ” 였λ₯˜')}"
193
+ logger.error(error_msg)
194
+ return {"success": False, "error": error_msg, "details": result}
195
+
196
+ elif status == "transcribing":
197
+ logger.info(f"VITO API 처리 쀑... ({try_count + 1}/{max_tries})")
198
+ else: # registered, waiting λ“± λ‹€λ₯Έ μƒνƒœ
199
+ logger.info(f"VITO API μƒνƒœ '{status}', λŒ€κΈ° 쀑... ({try_count + 1}/{max_tries})")
200
+
201
+
202
+ logger.error(f"VITO API 응닡 νƒ€μž„μ•„μ›ƒ ({max_tries * wait_time}초 초과)")
203
+ return {"success": False, "error": "VITO API 응닡 νƒ€μž„μ•„μ›ƒ"}
204
+
205
+ except requests.exceptions.HTTPError as e:
206
+ # 토큰 만료 였λ₯˜ 처리 (401 Unauthorized)
207
+ if e.response.status_code == 401:
208
+ logger.warning("VITO API 토큰이 λ§Œλ£Œλ˜μ—ˆκ±°λ‚˜ μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 토큰 μž¬λ°œκΈ‰ μ‹œλ„...")
209
+ self.access_token = None # κΈ°μ‘΄ 토큰 λ¬΄νš¨ν™”
210
+ try:
211
+ # μž¬κ·€ 호좜 λŒ€μ‹ , 토큰 μž¬λ°œκΈ‰ ν›„ λ‹€μ‹œ μ‹œλ„ν•˜λŠ” 둜직 ꡬ성
212
+ self.get_access_token()
213
+ logger.info("μƒˆ ν† ν°μœΌλ‘œ μž¬μ‹œλ„ν•©λ‹ˆλ‹€.")
214
+ # μž¬μ‹œλ„λŠ” 이 ν•¨μˆ˜λ₯Ό λ‹€μ‹œ ν˜ΈμΆœν•˜λŠ” λŒ€μ‹ , ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œ μ²˜λ¦¬ν•˜λŠ” 것이 더 μ•ˆμ „ν•  οΏ½οΏ½οΏ½ 있음
215
+ # μ—¬κΈ°μ„œλŠ” ν•œ 번 더 μ‹œλ„ν•˜λŠ” 둜직 μΆ”κ°€ (λ¬΄ν•œ 루프 λ°©μ§€ ν•„μš”)
216
+ # return self.transcribe_audio(audio_bytes, language) # μž¬κ·€ 호좜 방식
217
+ # --- λΉ„μž¬κ·€ 방식 ---
218
+ headers["Authorization"] = f"Bearer {self.access_token}" # 헀더 μ—…λ°μ΄νŠΈ
219
+ # POST μš”μ²­λΆ€ν„° λ‹€μ‹œ μ‹œμž‘ (μ½”λ“œ 쀑볡 λ°œμƒ κ°€λŠ₯μ„± 있음)
220
+ # ... (POST μš”μ²­ 및 κ²°κ³Ό 폴링 둜직 반볡) ...
221
+ # κ°„λ‹¨ν•˜κ²ŒλŠ” κ·Έλƒ₯ μ‹€νŒ¨ μ²˜λ¦¬ν•˜κ³  μƒμœ„μ—μ„œ μž¬μ‹œλ„ μœ λ„
222
+ return {"success": False, "error": "토큰 만료 ν›„ μž¬μ‹œλ„ ν•„μš”", "details": "토큰 μž¬λ°œκΈ‰ 성곡"}
223
+
224
+ except Exception as token_e:
225
+ logger.error(f"토큰 μž¬νšλ“ μ‹€νŒ¨: {token_e}")
226
+ return {"success": False, "error": f"토큰 μž¬νšλ“ μ‹€νŒ¨: {str(token_e)}"}
227
+
228
+ else:
229
+ # 401 μ™Έ λ‹€λ₯Έ HTTP 였λ₯˜
230
+ error_body = ""
231
+ try:
232
+ error_body = e.response.text
233
+ except Exception:
234
+ pass
235
+ logger.error(f"VITO API HTTP 였λ₯˜: {e.response.status_code}, 응닡: {error_body}")
236
+ return {
237
+ "success": False,
238
+ "error": f"API HTTP 였λ₯˜: {e.response.status_code}",
239
+ "details": error_body
240
+ }
241
+
242
+ except requests.exceptions.Timeout:
243
+ logger.error("VITO API μš”μ²­ μ‹œκ°„ 초과")
244
+ return {"success": False, "error": "API μš”μ²­ μ‹œκ°„ 초과"}
245
+ except requests.exceptions.RequestException as e:
246
+ logger.error(f"VITO API μš”μ²­ 쀑 λ„€νŠΈμ›Œν¬ 였λ₯˜ λ°œμƒ: {str(e)}")
247
+ return {"success": False, "error": "API μš”μ²­ λ„€νŠΈμ›Œν¬ 였λ₯˜", "details": str(e)}
248
+ except Exception as e:
249
+ logger.error(f"μŒμ„±μΈμ‹ 처리 쀑 μ˜ˆμƒμΉ˜ λͺ»ν•œ 였λ₯˜ λ°œμƒ: {str(e)}", exc_info=True)
250
+ return {
251
+ "success": False,
252
+ "error": "μŒμ„±μΈμ‹ λ‚΄λΆ€ 처리 μ‹€νŒ¨",
253
+ "details": str(e)
254
+ }