HUTAMS_AUDIO/app/core/dictionary.py

152 lines
6.1 KiB
Python

import json
import os
import csv
import logging
from pydantic import BaseModel
logger = logging.getLogger("uvicorn.error")
RAILWAY_TERMS_LIST: list[str] = []
RAILWAY_TERMS_DICT: dict[str, list[dict]] = {}
def load_terms_from_csv(file_path: str):
"""
[Chapter 6.1] 대규모 도메인 사전(CSV) 안전 적재
동음이의어(중복 키)를 허용하기 위해 List 형태로 Value를 저장하며,
RapidFuzz 일괄 대조를 위한 중복 제거 리스트를 함께 생성합니다.
"""
global RAILWAY_TERMS_LIST, RAILWAY_TERMS_DICT
RAILWAY_TERMS_LIST.clear()
RAILWAY_TERMS_DICT.clear()
if not os.path.exists(file_path):
raise FileNotFoundError(f"CSV 사전을 찾을 수 없습니다: {file_path}")
# 인코딩 Fallback: utf-8-sig 우선, 실패 시 cp949
try:
with open(file_path, mode="r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
rows = list(reader)
except UnicodeDecodeError:
logger.warning(f"UTF-8-SIG 디코딩 실패. CP949로 재시도합니다: {file_path}")
with open(file_path, mode="r", encoding="cp949") as f:
reader = csv.DictReader(f)
rows = list(reader)
unique_terms = set()
count = 0
for row in rows:
keyword = row.get("용어명", "").strip()
desc = row.get("내용", "").strip()
category = row.get("관련분야", "").strip()
if not keyword:
continue
unique_terms.add(keyword)
if keyword not in RAILWAY_TERMS_DICT:
RAILWAY_TERMS_DICT[keyword] = []
RAILWAY_TERMS_DICT[keyword].append({
"desc": desc,
"category": category
})
count += 1
RAILWAY_TERMS_LIST.extend(list(unique_terms))
logger.info(f"✅ 대규모 사전 로드 완료: 총 {count}행 처리, 매칭 대상 {len(RAILWAY_TERMS_LIST)}단어 (파일: {file_path})")
# ── 화자 분류용 호출 부호 외부 정의 ──────────────────────────────────────────
# speaker_classifier.py가 이 목록을 임포트하여 사용합니다.
# 현장 호출 부호가 추가될 때 여기만 수정하면 전체 파이프라인에 반영됩니다.
CALLSIGNS_CONTROL: list[str] = [
# ── 부산 도시철도 관제 호출 코드 ──
"전철 보안", "전철 범일", "전철 호포", "전철 신평", "전철 관제", "전철 통제",
"전철보안", "전철범일", "전철호포", "전철신평",
# 관제사 특유 어구
"진로 확인 부탁", "통과 허가", "신호진로 확인",
]
CALLSIGNS_TRAIN: list[str] = [
# ── 차량/열차 유형 ──
"모터카", "전기 모터카", "신호 모터카", "검측차", "검축차", "궤도 검측차",
# 열차 발화 특유 어구
"출발 합니다", "출발하겠습니다", "통과 하겠습니다", "통과하겠습니다",
"확인하고 통과", "신호 확인 후 통과",
]
class DomainDictionary(BaseModel):
stations: list[str] = [
"다대포해수욕장", "다대포항", "낫개", "신장림", "장림", "동매", "신평", "하단", "당리", "사하", "괴정",
"대티", "서대신", "동대신", "토성", "자갈치", "남포", "중앙", "노포", "범어사", "남산", "두실", "구서", "장전",
"부산대", "온천장", "명륜", "동래", "교대", "연산", "시청", "양정", "부전", "서면", "범내골", "범일", "좌천", "부산진",
"초량", "부산역"
]
railway_terms: list[str] = [
"모터카", "분기기", "신호기", "궤도", "검축차", "하선", "상선", "입고", "출고", "무전", "수신", "양호",
"신호 모터카", "전기 모터카", "궤도 검측차"
]
HARDCODED_FIXES: dict[str, str] = {
"신호질로": "신호 진로",
"멀티플": "멀티플 타이탬퍼"
}
def get_prompt(self) -> str:
"""STT 모델에 주입할 initial_prompt 문자열을 반환합니다."""
return " ".join(self.stations + self.railway_terms)
def post_process_correction(self, text: str, threshold: float = 85.0) -> str:
"""
[Chapter 6.1 최적화 알고리즘]
RapidFuzz를 사용하여 문장 내 특정 어절들을 사전 단어와 비교 후
오타로 판단되면 철도 전문 용어로 자동 교정합니다. (띄어쓰기 기준으로 토큰화)
"""
from rapidfuzz import process, fuzz
# RAILWAY_TERMS_LIST가 비어있으면 기본 하드코딩을 쓰고, 있으면 합침
all_terms = self.stations + self.railway_terms
if RAILWAY_TERMS_LIST:
all_terms.extend(RAILWAY_TERMS_LIST)
# 1. 하드코딩 교정 (단순 매칭/replace)
for bad_word, good_word in self.HARDCODED_FIXES.items():
text = text.replace(bad_word, good_word)
words = text.split()
corrected_words = []
# 유효한 최적화를 위해 중복 제거
unique_terms = list(set(all_terms))
for word in words:
# 특수 기호 경계 지우기 (단순화)
clean_word = "".join(c for c in word if c.isalnum())
best_match = None
# 짧은 단어 무시 (길이 2 이상만 대조, CPU 최적화)
if len(clean_word) >= 2 and unique_terms:
match_result = process.extractOne(
clean_word,
unique_terms,
scorer=fuzz.WRatio,
score_cutoff=threshold
)
if match_result:
best_match = match_result[0]
if best_match:
corrected_words.append(best_match)
else:
corrected_words.append(word)
# 3. 추가 보정 (선택사항)
for i, cw in enumerate(corrected_words):
if "다대포" in cw and not cw.endswith("") and not "해수욕" in cw:
corrected_words[i] = cw + ""
return " ".join(corrected_words)
# 전역 인스턴스
domain_dict = DomainDictionary()