""" core/review_parser.py — 합의판정/비디오판독 파싱 판독 텍스트에서 항목, 원래 판정, 최종 판정 등을 추출합니다. """ from __future__ import annotations import re from typing import Any from core.config_loader import review_result_groups def infer_review_item(detail_text: str) -> str: """판독 텍스트에서 사이트 표준 항목 추론 '홈런 파울 판정' → '홈런타구 페어 파울' """ dt = detail_text.replace(" ", "") if "홈런" in dt: return "홈런타구 페어 파울" if "아웃" in dt or "세이프" in dt or "포스" in dt or "태그" in dt or "견제" in dt or "도루" in dt: return "포수/태그플레이 아웃/세이프" if "페어" in dt or "파울" in dt: return "외야타구 페어 파울" if "포구" in dt or "노바운드" in dt or "바운드" in dt: return "야수의 포구" if "몸에맞" in dt or "데드볼" in dt: return "몸에 맞는 볼" if "헛스윙" in dt or "스윙" in dt: return "헛스윙" return "기타" def normalize_review_result_token(token: str, review_item: str) -> str | None: """판독 결과 토큰을 정규화 '세이프' → '세이프', '노스윙' → '불인정' """ token = (token or "").strip() if not token: return None if review_item in {"홈런타구 페어 파울", "외야타구 페어 파울"}: if "페어" in token: return "페어" if "파울" in token: return "파울" elif review_item in {"포수/태그플레이 아웃/세이프", "야수의 포구"}: if "아웃" in token: return "아웃" if "세이프" in token: return "세이프" elif review_item == "헛스윙": # '노스윙'에도 '스윙'이 포함되므로 먼저 체크 if "불인정" in token or "노스윙" in token or "공포" in token or "노 스윙" in token: return "불인정" if "스윙" in token or "인정" in token: return "인정" else: if "불인정" in token or "실패" in token: return "불인정" if "인정" in token: return "인정" return token # 모르는 키워드 → 원문 그대로 return None def parse_review_event_text(text: str) -> dict[str, Any]: """판독 텍스트를 파싱하여 구조화된 dict로 변환 입력 예: '6회초 8번타순 1구 후 18:45 ~ 18:46 (1분간) LG요청 비디오 판독: 안중열 포스아웃 관련 세이프→세이프' """ inning_match = re.search(r"(\d+)회(초|말)", text) request_team_match = re.search(r"([가-힣A-Za-z]+)요청\s*(?:비디오 판독|합의 판정)", text) # '→노 스윙' 같은 공백 정규화 normalized = re.sub(r"→([가-힣]+)\s+([가-힣]+)", r"→\1\2", text) detail_match = re.search( r"(?:비디오 판독|합의 판정):\s*(.+?)\s*([가-힣]+)→([가-힣]+)\s*$", normalized, ) detail_text = detail_match.group(1).strip() if detail_match else text review_item = infer_review_item(detail_text) before_result = normalize_review_result_token(detail_match.group(2), review_item) if detail_match else None after_result = normalize_review_result_token(detail_match.group(3), review_item) if detail_match else None return { "type": "video_review", "text": text, "requestInningLabel": ( f"{inning_match.group(1)}{'초' if inning_match.group(2) == '초' else '말'}" if inning_match else None ), "requestTeam": request_team_match.group(1) if request_team_match else None, "reviewItem": review_item, "beforeResult": before_result, "finalResult": after_result, "isSuccess": ( "성공" if before_result and after_result and before_result != after_result else "실패" ), "timing": "before_pitch" if "초구 전" in text else "after_pitch", } def normalize_review_event(review_event: dict[str, Any]) -> dict[str, Any]: """판독 이벤트를 정규화 beforeResult/finalResult가 누락된 경우 텍스트에서 재파싱 """ has_results = ( review_event.get("beforeResult") is not None and review_event.get("finalResult") is not None ) if review_event.get("requestInningLabel") and review_event.get("reviewItem") and has_results: return review_event text = review_event.get("text") or "" parsed = parse_review_event_text(text) parsed.update({k: v for k, v in review_event.items() if k not in parsed}) return parsed def get_review_result_group(review_item: str) -> dict[str, Any] | None: """사이트에서 판독항목에 대응하는 결과 그룹 정보 반환""" groups = review_result_groups() return groups.get(review_item)