sentences.py
Language Python2.9 KB86 lines
"""Sentence splitting, ported from the original JavaScript splitter.Splits on . ? ! followed by whitespace, with carve-outs for abbreviations, singleinitials, decimals, closing quotes and brackets, and an ellipsis followed by alowercase word (a pause mid-thought, not the end of a sentence). Each sentencecomes back with the character offset where it starts, which is what lets thepipeline map sentences onto timed words. The output must matchtests/fixtures/sentences-golden.json exactly."""from __future__ import annotationsimport re# Words that end in a period but do not end a sentence.ABBREVIATIONS = {"mr", "mrs", "ms", "dr", "prof", "sr", "jr", "st", "mt","inc", "ltd", "llc", "co", "corp", "dept", "est", "fig","vs", "etc", "ie", "eg", "approx", "no", "vol", "jan", "feb","mar", "apr", "jun", "jul", "aug", "sep", "sept", "oct", "nov", "dec","a.m", "p.m", "u.s", "u.k",}TERMINATORS = ".?!"CLOSERS = "\"'”’)]"_WS = re.compile(r"\s")_LAST_WORD = re.compile(r"([A-Za-z][A-Za-z.]*)$")def split_sentences(text: str) -> list[dict]:"""Return [{"text": sentence, "offset": start_index}, ...]."""out: list[dict] = []start = 0i = 0n = len(text)while i < n:ch = text[i]if ch not in TERMINATORS:i += 1continue# Absorb runs of terminators and any closing quote/bracket.end = iwhile end + 1 < n and text[end + 1] in TERMINATORS:end += 1while end + 1 < n and text[end + 1] in CLOSERS:end += 1# A sentence break needs whitespace (or end of text) after it.if end + 1 < n and not _WS.match(text[end + 1]):i = end + 1continue# A trailing ellipsis followed by a lowercase word is a pause, not an end.if end > i and text[i:end + 1] == "." * (end - i + 1):if re.match(r"\s*[a-z]", text[end + 1:]):i = end + 1continueif ch == ".":before = text[start:i]m = _LAST_WORD.search(before)if m:bare = m.group(1).lower()if bare.endswith("."):bare = bare[:-1]# Known abbreviation, or a single initial like "J."if bare in ABBREVIATIONS or re.fullmatch(r"[a-z]", bare):i = end + 1continue# A decimal point: a digit before and after.if re.search(r"\d$", before) and re.match(r"\s*\d", text[end + 1:]):i = end + 1continueraw = text[start:end + 1]trimmed = raw.strip()if trimmed:out.append({"text": trimmed, "offset": start + (len(raw) - len(raw.lstrip()))})start = end + 1i = end + 1tail = text[start:]if tail.strip():out.append({"text": tail.strip(), "offset": start + (len(tail) - len(tail.lstrip()))})return out