"""윤정님 추출 모델 wrapper. 가정통신문 텍스트 → list[YunjeongTodo]. v2 모델 (model/extraction/file/predict.py): binary 분류(BINARY_THRESHOLD=0.5) + 정규식 due_date/amount/action_hint. 출력 스키마가 YunjeongTodo와 1:1. v1 (구버전 model/extraction/predict.py)은 윤정님이 v2 머지하면서 삭제. v1 adapter 경로는 호환성을 위해 남겨두지만 실제로는 v2 진입점이 사용됨. """ import re import sys from pathlib import Path from app.models.schemas import YunjeongTodo # v2 진입점은 model/extraction/file/predict.py. # CI/테스트 환경에선 외부 마운트 부재 → 가드로 빈 결과 반환. _EXTRACTION_DIR = Path("/app/external_model/extraction/file") if str(_EXTRACTION_DIR) not in sys.path: sys.path.insert(0, str(_EXTRACTION_DIR)) try: import predict as _yunjeong # noqa: E402 except ImportError as error: print(f"[extractor] predict module unavailable: {error}") _yunjeong = None _AMOUNT_RE = re.compile(r"(\d{1,3}(?:,\d{3})+|\d+)\s*원") def extract_title(notice_text: str) -> str | None: """가정통신문 원문 → 제목 한 줄. 못 찾으면 None. 윤정님 PR #90 (predict.py:extract_title) — split_sentences()의 _HEADER_ONLY 필터가 제목을 차단하기 전에 원문 줄을 직접 스캔. predict()와 별도 호출. """ if not notice_text or not notice_text.strip(): return None if _yunjeong is None or not hasattr(_yunjeong, "extract_title"): return None try: return _yunjeong.extract_title(notice_text) except Exception as error: print(f"[extractor] extract_title failed: {error}") return None def extract_todos(notice_text: str, source: str | None = None) -> list[YunjeongTodo]: """가정통신문 원문 → list[YunjeongTodo]. 할일 없으면 [].""" if not notice_text or not notice_text.strip(): return [] if _yunjeong is None: return [] # 모델 모듈 없음 (CI 등) — 빈 결과로 후속 단계 정상 동작 # v2 진입점: predict(text, source) → list[dict] if hasattr(_yunjeong, "predict"): try: raw_items = _yunjeong.predict(notice_text, source=source) except TypeError: # source kwarg 미지원 버전 호환 raw_items = _yunjeong.predict(notice_text) return [YunjeongTodo(**raw) for raw in raw_items] # v1 fallback (구버전 predict.py가 마운트되어 있을 경우) if hasattr(_yunjeong, "extract_todos_dict"): raw_items = _yunjeong.extract_todos_dict(notice_text) return [_adapt_v1(item) for item in raw_items] return [] def _adapt_v1(v1_item: dict) -> YunjeongTodo: text = v1_item.get("text_ko", "") return YunjeongTodo( text=text, source=None, due_date=v1_item.get("due_date"), amount=_extract_amount_value(text), confidence=float(v1_item.get("importance", 0.5)), action_hint=None, ) def _extract_amount_value(text: str) -> int | None: m = _AMOUNT_RE.search(text) if not m: return None try: return int(m.group(1).replace(",", "")) except ValueError: return None