chagatai-project's picture
Rename models to [stanza] convention, update README and benchmarks
9e8e1e1 verified
Raw History Blame Contribute Delete
5.37 kB
"""
Chagatai Sentence Boundary Detection (SBD) — Standalone ONNX Inference Engine.
Zero-dependency inference module (requires only `numpy` and `onnxruntime`).
Supports standalone SOTA and unified multi-model ensembles packaged into single .onnx files.
"""
import json
import re
from pathlib import Path
from typing import List, Tuple, Dict, Any, Union
import numpy as np
import onnxruntime as ort
NEWLINE_WHITESPACE_RE = re.compile(r'\n[\s\u0080-\u009f]*\n')
WHITESPACE_RE = re.compile(r'[\s\u0080-\u009f]')
NUMERIC_RE = re.compile(r'^[\d]+([,\.]+[\d]+)*[,\.]*$')
class ChagataiSBD:
"""
High-performance, dependency-free Sentence Boundary Detector for Chagatai text.
Loads a single unified ONNX model (either standalone or soft-voting ensemble)
and predicts sentence boundaries directly on unsegmented text.
"""
def __init__(
self,
model_path: Union[str, Path] = "models/onnx/chagatai_sbd_tri_hybrid_stanza.onnx",
vocab_path: Union[str, Path] = "models/onnx/vocab.json",
providers: List[str] = None
):
model_path = Path(model_path)
vocab_path = Path(vocab_path)
if not model_path.exists():
raise FileNotFoundError(f"Model file not found: {model_path}")
if not vocab_path.exists():
raise FileNotFoundError(f"Vocab file not found: {vocab_path}")
# Load vocabulary mapping
with open(vocab_path, "r", encoding="utf-8") as f:
v_data = json.load(f)
self.unit2id: Dict[str, int] = v_data["unit2id"]
self.pad_id: int = v_data["pad_id"]
self.unk_id: int = v_data["unk_id"]
# Initialize ONNX Runtime session
if providers is None:
providers = ['CPUExecutionProvider']
self.session = ort.InferenceSession(str(model_path), providers=providers)
def _preprocess_chunk(self, chunk: str) -> Tuple[List[str], np.ndarray, np.ndarray]:
# Substitute non-standard whitespaces
chars = [WHITESPACE_RE.sub(' ', c) for c in chunk]
# Collapse consecutive spaces (matches Stanza pipeline)
filtered_chars: List[str] = []
for i, c in enumerate(chars):
if i > 0 and c == ' ' and filtered_chars[-1] == ' ':
continue
filtered_chars.append(c)
N = len(filtered_chars)
char_ids = np.zeros(N + 1, dtype=np.int64)
feats = np.zeros((N + 1, 5), dtype=np.float32)
for i, c in enumerate(filtered_chars):
char_ids[i] = self.unit2id.get(c, self.unk_id)
feats[i, 0] = 1.0 if c.startswith(' ') else 0.0
feats[i, 1] = 1.0 if c[0].isupper() else 0.0
feats[i, 2] = 1.0 if NUMERIC_RE.match(c) else 0.0
feats[i, 3] = 1.0 if i == (N - 1) else 0.0
feats[i, 4] = 1.0 if i == 0 else 0.0
char_ids[N] = self.pad_id
feats[N, :] = 0.0
return filtered_chars, char_ids, feats
def segment(self, text: str) -> List[str]:
"""
Segment a document or multi-line text into individual sentences.
Args:
text: Raw Chagatai text string.
Returns:
List of segmented sentences.
"""
if not text or not text.strip():
return []
chunks = [pt.rstrip() for pt in NEWLINE_WHITESPACE_RE.split(text.rstrip()) if pt.rstrip()]
all_sentences: List[str] = []
for chunk in chunks:
filtered_chars, char_ids, feats = self._preprocess_chunk(chunk)
N = len(filtered_chars)
if N == 0:
continue
# Run ONNX inference
c_input = char_ids[np.newaxis, :] # shape: [1, N + 1]
f_input = feats[np.newaxis, :] # shape: [1, N + 1, 5]
p_sent, is_tok, is_eos = self.session.run(
None,
{'char_ids': c_input, 'feats': f_input}
)
eos_mask = is_eos[0, :N].copy()
# Guarantee last non-space character in paragraph closes the sentence
eos_mask[-1] = True
curr_sent = []
for c, is_end in zip(filtered_chars, eos_mask):
curr_sent.append(c)
if is_end:
s_str = "".join(curr_sent).strip()
if s_str:
all_sentences.append(s_str)
curr_sent = []
if curr_sent:
s_str = "".join(curr_sent).strip()
if s_str:
all_sentences.append(s_str)
return all_sentences
if __name__ == '__main__':
# Smoke test on a sample Chagatai paragraph
sbd = ChagataiSBD("models/onnx/chagatai_sbd_tri_hybrid_stanza.onnx", "models/onnx/vocab.json")
sample_text = (
"سلطان ابوسعید میرزا شهادت تاپقاندین سونگ سمرقند ولایتینی میرزا احمد آلدی "
"و هرات ولایتینی سلطان حسین میرزا ضبط قیلدی و اندیجان ولایتینی عمرشیخ میرزا توتتی"
)
print("\nOriginal text:")
print(sample_text)
sentences = sbd.segment(sample_text)
print(f"\nDetected {len(sentences)} sentences:")
for idx, s in enumerate(sentences, 1):
print(f" [{idx}] {s}")