File size: 5,963 Bytes
d63774a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
import re
from collections import Counter

from underthesea import text_normalize as uts_text_normalize, word_tokenize

_MEDICAL_TERM_MAP = {
    "xray": "x-quang",
    "x ray": "x-quang",
    "x-ray": "x-quang",
    "x quang": "x-quang",
    "mri scan": "mri",
    "mr": "mri",
    "ct scan": "ct",
    "ct-scan": "ct",
    "cat scan": "ct",
    "computed tomography": "ct",
    "transverse  plane": "mặt phẳng ngang",
    "transverse plane": "mặt phẳng ngang",
    "coronal plane": "mặt phẳng vành",
    "sagittal plane": "mặt phẳng dọc",
    "elliptical": "hình elip",
    "spleen": "lách",
    "liver": "gan",
    "lung": "phổi",
    "lungs": "phổi",
    "heart": "tim",
    "brain": "não",
    "kidney": "thận",
    "bladder": "bàng quang",
    "cardiomegaly": "tim to",
}

_NON_CANONICAL_ALIASES = {
    "xray",
    "x ray",
    "x-ray",
    "x quang",
    "mri scan",
    "mr",
    "ct scan",
    "ct-scan",
    "cat scan",
    "computed tomography",
    "transverse plane",
    "coronal plane",
    "sagittal plane",
    "elliptical",
    "spleen",
    "liver",
    "lung",
    "lungs",
    "heart",
    "brain",
    "kidney",
    "bladder",
    "cardiomegaly",
}


def text_normalize(text: str) -> str:
    """Wrapper để chuẩn hóa Unicode và spacing cho tiếng Việt."""
    if not text:
        return ""
    return uts_text_normalize(str(text))


def normalize_answer(text: str) -> str:
    """
    Chuẩn hóa đáp án về dạng canonical để train/eval ổn định.
    """
    if not text:
        return ""

    text = text_normalize(str(text))
    text = text.replace("_", " ")
    text = text.lower().strip()
    text = re.sub(r"[@#]{1,2}", " ", text)
    text = re.sub(r"[“”\"']", "", text)
    text = re.sub(r"[,:;!?()\[\]{}]+", " ", text)
    text = re.sub(r"\s+", " ", text).strip()

    for src, dst in sorted(_MEDICAL_TERM_MAP.items(), key=lambda item: -len(item[0])):
        text = re.sub(rf"\b{re.escape(src)}\b", dst, text)

    text = re.sub(r"\s+", " ", text).strip()
    text = re.sub(r"[.\-]+$", "", text).strip()
    return text


def _tokenize_vietnamese_words(text: str) -> list[str]:
    normalized = normalize_answer(text)
    if not normalized:
        return []
    try:
        tokens = word_tokenize(normalized)
        return [token.strip() for token in tokens if token and token.strip()]
    except Exception:
        return normalized.split()


def count_words(text: str) -> int:
    return len(_tokenize_vietnamese_words(text))


def _trim_to_max_words(text: str, max_words: int) -> str:
    words = _tokenize_vietnamese_words(text)
    if len(words) <= max_words:
        return " ".join(words)
    return " ".join(words[:max_words])


def _choose_best_answer_text(answer_vi: str, answer_full_vi: str, max_words: int) -> str:
    short_answer = normalize_answer(answer_vi)
    full_answer = normalize_answer(answer_full_vi)

    if short_answer and count_words(short_answer) <= max_words:
        return short_answer
    if full_answer:
        return _trim_to_max_words(full_answer, max_words)
    return _trim_to_max_words(short_answer, max_words)


def get_target_answer(item: dict, max_words: int = 10) -> str:
    """
    Chọn target answer ngắn, chuẩn hóa và không vượt quá số từ cho phép.
    """
    answer_vi = item.get("answer_vi", "")
    answer_full_vi = item.get("answer_full_vi", "")
    answer = _choose_best_answer_text(answer_vi, answer_full_vi, max_words=max_words)
    if answer:
        return answer
    fallback = item.get("answer", "")
    return _trim_to_max_words(fallback, max_words)


def postprocess_answer(text: str, max_words: int = 10) -> str:
    """
    Chuẩn hóa output model và cắt ngắn về tối đa `max_words`.
    Không mở rộng câu trả lời để tránh làm xấu exact match.
    """
    if not text:
        return ""
    text = clean_vqa_output(text)
    text = normalize_answer(text)
    return _trim_to_max_words(text, max_words=max_words)


def is_medical_term_compliant(text: str) -> bool:
    """
    Heuristic nhẹ: không còn alias y khoa phổ biến chưa canonicalize.
    """
    normalized = normalize_answer(text)
    if not normalized:
        return False
    for alias in _NON_CANONICAL_ALIASES:
        if re.search(rf"\b{re.escape(alias)}\b", normalized):
            return False
    return True


def majority_answer(answers: list[str]) -> str:
    """
    Trả về câu trả lời xuất hiện nhiều nhất trong danh sách.
    """
    if not answers:
        return ""
    if isinstance(answers, str):
        return normalize_answer(answers)
    counts = Counter([normalize_answer(a) for a in answers])
    return counts.most_common(1)[0][0]


def clean_vqa_output(text: str) -> str:
    """
    Làm sạch output từ tokenizer trước khi postprocess.
    """
    if not text:
        return ""
    text = re.sub(r"@@\s?", "", text)
    text = re.sub(r"##_?", "", text)
    text = re.sub(r"^\s*yes\s*,?\s*", "có ", text, flags=re.IGNORECASE)
    text = re.sub(r"^\s*no\s*,?\s*", "không ", text, flags=re.IGNORECASE)
    text = re.sub(
        r"^\s*(the answer is|the image is|this image is|the scan is|the ct is|the mri is|there is|there are)\s+",
        "",
        text,
        flags=re.IGNORECASE,
    )
    text = re.sub(
        r"^(có|không)\s+(the\s+)?(image|scan|x-ray|xray|mri|ct|picture|photo|radiograph)\s+(is|shows?|depicts?|demonstrates?|reveals?|indicates?|presents?)\s+",
        r"\1 ",
        text,
        flags=re.IGNORECASE,
    )
    text = re.sub(
        r"^(the\s+)?(image|scan|x-ray|xray|mri|ct|picture|photo|radiograph)\s+(is|shows?|depicts?|demonstrates?|reveals?|indicates?|presents?)\s+",
        "",
        text,
        flags=re.IGNORECASE,
    )
    text = re.sub(r"\b(answer|response|assistant|trả lời)\b\s*:?\s*$", "", text, flags=re.IGNORECASE)
    text = re.sub(r"\s+", " ", text).strip()
    return text