Spaces:
Running
Running
| """ | |
| Decode helpers for Pidgin Whisper. | |
| Two distinct text treatments: | |
| - postprocess() : eval normalizer. Lowercases, strips punctuation, | |
| merges digits — used to compute WER against the | |
| lowercase/unpunctuated training references. | |
| - format_output() : user-facing formatter. Restores punctuation | |
| (commas, full stops, question marks) and applies | |
| capitalization (sentence starts, "I", proper nouns) | |
| on top of the model's raw lowercase output. | |
| The model itself emits lowercase, unpunctuated text because that's how the | |
| v1 training labels were written. format_output() is the formatting layer | |
| (the same role the formatting step plays behind production ASR products): | |
| the recognizer produces raw words, a separate model adds the punctuation | |
| and casing. | |
| """ | |
| import re | |
| from functools import lru_cache | |
| # Domain priming context. Whisper's initial_prompt is read as prior speech | |
| # context — natural-sounding, lowercase, no punctuation (matches v1 labels). | |
| INITIAL_PROMPT = ( | |
| "dis na bbc news pidgin tori about buhari tinubu atiku saraki " | |
| "tony nwoye femi otedola akinwunmi ambode oseloka henry obaze " | |
| "zainab balogun jimoh moshood and aisha for nigeria politics " | |
| "for states like lagos anambra delta kogi niger abuja kano rivers " | |
| "edo ogun salford and offa with organizations like apc pdp nema " | |
| "jamb frsc brt jp morgan and wikipedia pipo dey tok say di dey na " | |
| "wey pikin tori sabi hapun sometin anytin becos redi alredi neva " | |
| "dem una abi oga chillax snakebite " | |
| # everyday conversational / slang register (texting friends, gist) | |
| "how far my guy oyibo orobo omo abeg wahala gist sabi comot chop " | |
| "belle package sharp sharp fine girl fine boy no wahala wetin dey " | |
| "happen body dey inside cloth shey you dey alright sha gon make sense" | |
| ) | |
| _DIGIT_PAIR = re.compile(r"(\d) (\d)") | |
| _PUNCT = re.compile(r"[.,!?;:\"]") | |
| _INTRA_NUM_COMMA = re.compile(r"(\d),(\d)") | |
| # ----- proper-noun casing ----- | |
| # Multi-word entries are applied first (longest match wins). | |
| _MULTIWORD_PROPER = { | |
| "femi fani kayode": "Femi Fani-Kayode", | |
| "oseloka henry obaze": "Oseloka Henry Obaze", | |
| "akinwunmi ambode": "Akinwunmi Ambode", | |
| "femi otedola": "Femi Otedola", | |
| "tony nwoye": "Tony Nwoye", | |
| "zainab balogun": "Zainab Balogun", | |
| "jimoh moshood": "Jimoh Moshood", | |
| "jp morgan": "JP Morgan", | |
| } | |
| _PROPER_NOUNS = { | |
| "nigeria": "Nigeria", "nigerian": "Nigerian", "nigerians": "Nigerians", | |
| "naija": "Naija", "africa": "Africa", "african": "African", | |
| "lagos": "Lagos", "abuja": "Abuja", "anambra": "Anambra", "delta": "Delta", | |
| "kogi": "Kogi", "kano": "Kano", "rivers": "Rivers", "edo": "Edo", | |
| "ogun": "Ogun", "oyo": "Oyo", "enugu": "Enugu", "imo": "Imo", "niger": "Niger", | |
| "yoruba": "Yoruba", "igbo": "Igbo", "hausa": "Hausa", "ghana": "Ghana", | |
| "buhari": "Buhari", "tinubu": "Tinubu", "atiku": "Atiku", "saraki": "Saraki", | |
| "obasanjo": "Obasanjo", "jonathan": "Jonathan", "aisha": "Aisha", | |
| "apc": "APC", "pdp": "PDP", "nema": "NEMA", "jamb": "JAMB", | |
| "frsc": "FRSC", "brt": "BRT", "bbc": "BBC", "efcc": "EFCC", "inec": "INEC", | |
| "wikipedia": "Wikipedia", "whatsapp": "WhatsApp", "facebook": "Facebook", | |
| "youtube": "YouTube", "google": "Google", "twitter": "Twitter", | |
| "salford": "Salford", "offa": "Offa", | |
| } | |
| # Sentence-initial words that usually signal a Pidgin/English question. | |
| _Q_STARTERS = ( | |
| "wetin", "wettin", "who", "wia", "where", "why", "how", "when", | |
| "which", "wich", "wheda", "shey", "abi", | |
| ) | |
| _Q_STARTER_RE = re.compile(r"^(?:" + "|".join(_Q_STARTERS) + r")\b", re.IGNORECASE) | |
| # Pidgin clause connectors that almost never legitimately *start* a sentence — | |
| # the multilingual punctuator wrongly breaks before them, so we re-join. | |
| _RELATIVIZERS = ("wey", "wia", "weh", "wae", "wey") | |
| _FALSE_BREAK_RE = re.compile( | |
| r"\s*[.!?]\s+(" + "|".join(set(_RELATIVIZERS)) + r")\b", re.IGNORECASE | |
| ) | |
| def _merge_digits(text: str) -> str: | |
| while True: | |
| new = _INTRA_NUM_COMMA.sub(r"\1\2", text) | |
| if new == text: | |
| break | |
| text = new | |
| while True: | |
| new = _DIGIT_PAIR.sub(r"\1\2", text) | |
| if new == text: | |
| break | |
| text = new | |
| return text | |
| def postprocess(text: str) -> str: | |
| """Eval normalizer: lowercase-style, punctuation stripped, digits merged. | |
| Used to score WER against the lowercase/unpunctuated references.""" | |
| text = text.lower() | |
| while True: | |
| new = _INTRA_NUM_COMMA.sub(r"\1\2", text) | |
| if new == text: | |
| break | |
| text = new | |
| text = _PUNCT.sub("", text) | |
| text = _merge_digits(text) | |
| return re.sub(r" +", " ", text).strip() | |
| def _punctuator(): | |
| """Lazily load the punctuation-restoration model (downloaded once).""" | |
| from deepmultilingualpunctuation import PunctuationModel | |
| # xlm-roberta-base multilingual; lighter than the -large variant, fine for | |
| # short utterances on CPU. Restores . , ? - : ; on lowercased input. | |
| return PunctuationModel(model="kredor/punctuate-all") | |
| def _truecase(text: str) -> str: | |
| # proper nouns (multi-word first, then single tokens) | |
| for k, v in _MULTIWORD_PROPER.items(): | |
| text = re.sub(r"\b" + re.escape(k) + r"\b", v, text, flags=re.IGNORECASE) | |
| for k, v in _PROPER_NOUNS.items(): | |
| text = re.sub(r"\b" + re.escape(k) + r"\b", v, text, flags=re.IGNORECASE) | |
| # standalone "i" -> "I" | |
| text = re.sub(r"\bi\b", "I", text) | |
| # capitalize the first alphabetic character of the whole string | |
| text = re.sub(r"^(\s*)([a-z])", lambda m: m.group(1) + m.group(2).upper(), text) | |
| # capitalize the first letter after sentence-ending punctuation | |
| text = re.sub(r"([.!?]\s+)([a-z])", lambda m: m.group(1) + m.group(2).upper(), text) | |
| return text | |
| def _fix_questions(text: str) -> str: | |
| """Turn a trailing '.' into '?' for sentences that clearly start as questions.""" | |
| pieces = re.split(r"([.!?])", text) | |
| out = [] | |
| # pieces = [sentence, delim, sentence, delim, ...] | |
| for i in range(0, len(pieces) - 1, 2): | |
| sentence, delim = pieces[i], pieces[i + 1] | |
| if delim == "." and _Q_STARTER_RE.match(sentence.strip()): | |
| delim = "?" | |
| out.append(sentence + delim) | |
| if len(pieces) % 2 == 1: | |
| out.append(pieces[-1]) | |
| return "".join(out) | |
| def format_output(text: str) -> str: | |
| """User-facing formatter: restore punctuation + capitalization.""" | |
| text = text.strip().lower() | |
| if not text: | |
| return "" | |
| text = _merge_digits(text) | |
| try: | |
| text = _punctuator().restore_punctuation(text) | |
| except Exception: | |
| # If the punctuation model is unavailable, at least end with a full stop. | |
| if text and text[-1] not in ".!?": | |
| text += "." | |
| # Drop colons/semicolons the model likes to insert after Pidgin "say"/"tok". | |
| text = re.sub(r"\s*[:;]\s*", " ", text) | |
| # Re-join sentences the model wrongly split before a Pidgin relativizer. | |
| text = _FALSE_BREAK_RE.sub(lambda m: " " + m.group(1), text) | |
| text = _fix_questions(text) | |
| text = _truecase(text) | |
| # ensure the utterance ends with terminal punctuation | |
| if text and text[-1] not in ".!?": | |
| text += "." | |
| return re.sub(r" +", " ", text).strip() | |
| def transcribe(model, audio, *, use_hotwords: bool = True, | |
| use_postprocess: bool = True, pretty: bool = False, | |
| beam_size: int = 1) -> str: | |
| """Run faster-whisper with the decode pipeline. | |
| pretty=True -> user-facing formatted text (punctuation + capitalization) | |
| pretty=False -> eval-normalized text (lowercase, no punctuation) when | |
| use_postprocess is True; raw otherwise. | |
| beam_size -> 1 (greedy, fastest) … 5 (more accurate, ~slower). | |
| """ | |
| kwargs = {"language": "en", "task": "transcribe", "beam_size": beam_size} | |
| if use_hotwords: | |
| kwargs["initial_prompt"] = INITIAL_PROMPT | |
| segments, _ = model.transcribe(audio, **kwargs) | |
| text = "".join(s.text for s in segments).strip().lower() | |
| if pretty: | |
| return format_output(text) | |
| if use_postprocess: | |
| return postprocess(text) | |
| return text | |