Spaces:
Sleeping
Sleeping
File size: 11,515 Bytes
745f62a | 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 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | // Hindi text normalization for medical ASR transcripts.
// Port of src/hindi_normalize.py (Python stdlib re + dicts) to JS.
// Used in both the LAN-sync path (server does the heavy lifting) and the
// on-device path (Cactus-powered Field Mode) where Python isn't available.
//
// The Python parse_hindi_number has a latent bug at lines 184-200 (total is
// never incremented inside the loop). Since WORD_TO_NUM caps at 100, the bug
// never manifests in practice — but the JS port mirrors it so test vectors
// from the Python side match byte-for-byte.
export const WORD_TO_NUM = {
// 0-10
'शून्य': 0, 'एक': 1, 'दो': 2, 'तीन': 3, 'चार': 4,
'पांच': 5, 'पाँच': 5, 'पाच': 5, 'छह': 6, 'छः': 6,
'सात': 7, 'आठ': 8, 'नौ': 9, 'दस': 10,
// 11-19
'ग्यारह': 11, 'गयारह': 11, 'ग्यारा': 11,
'बारह': 12, 'बारा': 12,
'तेरह': 13, 'तेरा': 13,
'चौदह': 14, 'चौदा': 14,
'पंद्रह': 15, 'पन्द्रह': 15, 'पंद्रा': 15,
'सोलह': 16, 'सोला': 16,
'सत्रह': 17, 'सत्तरह': 17,
'अठारह': 18, 'अठारा': 18,
'उन्नीस': 19, 'उन्निस': 19,
// 20-29
'बीस': 20, 'इक्कीस': 21, 'इक्किस': 21,
'बाईस': 22, 'बाइस': 22,
'तेईस': 23, 'तेइस': 23,
'चौबीस': 24, 'चौबिस': 24,
'पच्चीस': 25, 'पचीस': 25, 'पच्चिस': 25,
'छब्बीस': 26, 'छब्बिस': 26,
'सत्ताईस': 27, 'सत्ताइस': 27,
'अट्ठाईस': 28, 'अट्ठाइस': 28, 'अठ्ठाईस': 28,
'उनतीस': 29, 'उन्तीस': 29,
// 30-39
'तीस': 30, 'इकतीस': 31, 'इकत्तीस': 31,
'बत्तीस': 32, 'बतीस': 32,
'तैंतीस': 33, 'तेंतीस': 33,
'चौंतीस': 34, 'चौतीस': 34,
'पैंतीस': 35, 'पेंतीस': 35,
'छत्तीस': 36, 'छतीस': 36,
'सैंतीस': 37, 'सेंतीस': 37,
'अड़तीस': 38, 'अडतीस': 38,
'उनतालीस': 39, 'उन्तालीस': 39,
// 40-49
'चालीस': 40, 'चालिस': 40,
'इकतालीस': 41, 'एकतालीस': 41,
'बयालीस': 42, 'बयालिस': 42,
'तैंतालीस': 43, 'तेंतालीस': 43,
'चौवालीस': 44, 'चवालीस': 44,
'पैंतालीस': 45, 'पेंतालीस': 45,
'छियालीस': 46, 'छयालीस': 46,
'सैंतालीस': 47, 'सेंतालीस': 47,
'अड़तालीस': 48, 'अडतालीस': 48,
'उनचास': 49,
// 50-59
'पचास': 50,
'इक्यावन': 51,
'बावन': 52,
'तिरपन': 53, 'तिरेपन': 53,
'चौवन': 54, 'चौबन': 54,
'पचपन': 55,
'छप्पन': 56, 'छपन': 56,
'सत्तावन': 57, 'सतावन': 57,
'अट्ठावन': 58, 'अठावन': 58, 'अठ्ठावन': 58,
'उनसठ': 59,
// 60-69
'साठ': 60, 'साट': 60,
'इकसठ': 61, 'एकसठ': 61,
'बासठ': 62, 'बासट': 62,
'तिरसठ': 63, 'तिरेसठ': 63,
'चौंसठ': 64, 'चौसठ': 64,
'पैंसठ': 65, 'पेंसठ': 65,
'छियासठ': 66, 'छयासठ': 66,
'सड़सठ': 67, 'सडसठ': 67,
'अड़सठ': 68, 'अडसठ': 68,
'उनहत्तर': 69, 'उनहतर': 69,
// 70-79
'सत्तर': 70, 'सतर': 70,
'इकहत्तर': 71, 'इकहतर': 71,
'बहत्तर': 72, 'बहतर': 72,
'तिहत्तर': 73, 'तिहतर': 73,
'चौहत्तर': 74, 'चौहतर': 74,
'पचहत्तर': 75, 'पचहतर': 75,
'छिहत्तर': 76, 'छिहतर': 76,
'सतहत्तर': 77, 'सतहतर': 77,
'अठहत्तर': 78, 'अठहतर': 78,
'उन्यासी': 79, 'उनासी': 79, 'उन्नासी': 79,
// 80-89
'अस्सी': 80, 'अस्सि': 80,
'इक्यासी': 81, 'एक्यासी': 81,
'बयासी': 82, 'ब्यासी': 82,
'तिरासी': 83,
'चौरासी': 84,
'पचासी': 85,
'छियासी': 86, 'छयासी': 86,
'सत्तासी': 87, 'सतासी': 87,
'अट्ठासी': 88, 'अठासी': 88,
'नवासी': 89, 'नव्वासी': 89,
// 90-99
'नब्बे': 90, 'नब्बें': 90,
'इक्यानवे': 91,
'बानवे': 92,
'तिरानवे': 93,
'चौरानवे': 94,
'पंचानवे': 95, 'पचानवे': 95,
'छियानवे': 96,
'सत्तानवे': 97, 'सतानवे': 97,
'अट्ठानवे': 98, 'अठानवे': 98,
'निन्यानवे': 99, 'निन्नानवे': 99,
// Hundred marker
'सौ': 100, 'सो': 100,
}
export const MEDICAL_TERMS = {
'बीपी': 'BP', 'भीपी': 'BP', 'बीबी': 'BP', 'बी पी': 'BP', 'बी.पी.': 'BP',
'एचबी': 'Hb', 'हबी': 'Hb', 'हीमोग्लोबिन': 'Hb', 'एच बी': 'Hb',
'आईएफए': 'IFA', 'आई एफ ए': 'IFA',
'टीटी': 'TT', 'टी टी': 'TT',
'पीएचसी': 'PHC', 'पी एच सी': 'PHC', 'पीएचसे': 'PHC',
'सीएचसी': 'CHC', 'सी एच सी': 'CHC',
'बीसीजी': 'BCG', 'ओपीवी': 'OPV', 'हेप बी': 'Hep-B',
'आईएमएनसीआई': 'IMNCI',
'किलो': 'kg', 'किलोग्राम': 'kg',
'बटा': '/', 'बता': '/',
'दशमलव': '.', 'दशम्लव': '.', 'दशम्लफ': '.',
'डिग्री': '\u00b0',
}
// Escape a string for safe insertion into a RegExp
function reEscape(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
}
// Sorted longest-first for greedy matching
const _NUM_SORTED = Object.entries(WORD_TO_NUM).sort((a, b) => b[0].length - a[0].length)
// Devanagari Unicode range
const _DEVA = '\\u0900-\\u097F'
// Alternation of all number words (regex-escaped)
const _NUM_WORD_INNER = '(?:' + _NUM_SORTED.map(([w]) => reEscape(w)).join('|') + ')'
// Sequence of Hindi number words separated by spaces, Devanagari-aware boundaries
const _NUM_SEQ_RE = new RegExp(
'(?<![' + _DEVA + '])' +
_NUM_WORD_INNER + '(?:\\s+' + _NUM_WORD_INNER + ')*' +
'(?![' + _DEVA + '])',
'gu'
)
/**
* Parse one Hindi number expression starting at words[start].
* Returns [consumedCount, value] or [0, null] if no number begins here.
*
* Recognized patterns:
* [1-9] सौ [1-99] → एक सौ साठ = 160
* [1-9] सौ → दो सौ = 200
* सौ [1-99] → सौ दस = 110
* सौ → सौ = 100
* [0-99] → अट्ठावन = 58
*
* Adjacent simple digits are NOT merged. "दो तीन" returns [1, 2] — the
* caller advances and parses "तीन" as a separate number. Keeps phrases
* like "2-3 दिन" from collapsing to "5 दिन".
*/
function _parseOneNumber(words, start) {
const n = words.length
if (start >= n) return [0, null]
const v0 = WORD_TO_NUM[words[start]]
if (v0 === undefined) return [0, null]
// [1-9] सौ [optional 1-99]
if (v0 >= 1 && v0 < 10 && start + 1 < n && WORD_TO_NUM[words[start + 1]] === 100) {
const total = v0 * 100
if (start + 2 < n) {
const v2 = WORD_TO_NUM[words[start + 2]]
if (v2 !== undefined && v2 > 0 && v2 < 100) {
return [3, total + v2]
}
}
return [2, total]
}
// सौ [optional 1-99]
if (v0 === 100) {
if (start + 1 < n) {
const v1 = WORD_TO_NUM[words[start + 1]]
if (v1 !== undefined && v1 > 0 && v1 < 100) {
return [2, 100 + v1]
}
}
return [1, 100]
}
// any single number word (0-99)
return [1, v0]
}
/**
* Parse a single Hindi number expression into an integer.
* For unrelated adjacent number words ("दो तीन"), returns only the first
* parseable number (2). Use convertNumbers() to handle mixed sequences.
*/
export function parseHindiNumber(text) {
const words = text.trim().split(/\s+/)
if (!words.length || words[0] === '') return null
const [consumed, val] = _parseOneNumber(words, 0)
if (consumed === 0) return null
return val
}
// Whisper sometimes merges number words. Split compounds before main parsing.
const _COMPOUND_SPLITS = /(एकसो|दोसो|तीनसो|चारसो|पांचसो|पाँचसो|छहसो|सातसो|आठसो|नौसो)/g
const _COMPOUND_SPLIT_MAP = {
'एकसो': 'एक सो', 'दोसो': 'दो सो', 'तीनसो': 'तीन सो',
'चारसो': 'चार सो', 'पांचसो': 'पांच सो', 'पाँचसो': 'पाँच सो',
'छहसो': 'छह सो', 'सातसो': 'सात सो', 'आठसो': 'आठ सो', 'नौसो': 'नौ सो',
}
/**
* Replace all Hindi number word sequences in text with digit strings.
* Within a matched sequence, parses one number at a time so unrelated
* adjacent number words ("दो तीन") stay as separate digits ("2 3").
*/
export function convertNumbers(text) {
text = text.replace(_COMPOUND_SPLITS, (m) => _COMPOUND_SPLIT_MAP[m] || m)
return text.replace(_NUM_SEQ_RE, (m) => {
const words = m.split(/\s+/)
const out = []
let i = 0
while (i < words.length) {
const [consumed, val] = _parseOneNumber(words, i)
if (consumed === 0) {
out.push(words[i])
i += 1
} else {
out.push(String(val))
i += consumed
}
}
return out.join(' ')
})
}
/** Sorted longest-first medical term replacement */
const _MED_SORTED = Object.entries(MEDICAL_TERMS).sort((a, b) => b[0].length - a[0].length)
/**
* Full normalization pipeline for Whisper Hindi ASR output.
* 1. Fix Whisper repetition artifacts
* 2. Normalize medical abbreviations (बीपी → BP, etc.)
* 3. Convert Hindi number words → digits
* 4. Clean spacing around / and .
* 5. Line breaks at sentence boundaries (।)
* 6. Trim
*/
export function normalizeTranscript(transcript) {
// 1. Fix Whisper repetition bugs
transcript = transcript.replace(/(.{1,5}?)\1{3,}/g, '$1')
transcript = transcript.replace(/(\b\S+\b)(\s+\1){3,}/g, '$1')
// 2. Normalize medical abbreviations (longest first)
for (const [hi, en] of _MED_SORTED) {
transcript = transcript.split(hi).join(en)
}
// 3. Convert Hindi number words to digits
transcript = convertNumbers(transcript)
// 4. Clean up spacing around / and .
transcript = transcript.replace(/\s*\/\s*/g, '/')
transcript = transcript.replace(/(\d)\s*\.\s*(\d)/g, '$1.$2')
// 5. Add line breaks at sentence boundaries
transcript = transcript.replace(/।(?:\s+)/g, '।\n')
// 6. Trim
transcript = transcript.trim().replace(/[,.\s]+$/, '')
return transcript
}
|