132 lines
4.8 KiB
Python
132 lines
4.8 KiB
Python
"""
|
|
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)
|