bashkir-lid / lid.py
failed09's picture
Upload folder using huggingface_hub
30d39c9 verified
Raw History Blame Contribute Delete
5.75 kB
"""Portable Unicode char_wb preprocessing and sparse ONNX inference."""
import hashlib
import json
import re
from collections import Counter
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parent
def sha256(path):
with open(path, "rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def char_wb(text, ngram_range):
# Match sklearn's _char_wb_ngrams, including short-word handling.
text = re.sub(r"\s\s+", " ", text.lower())
for word in text.split():
word = " " + word + " "
for n in range(ngram_range[0], ngram_range[1] + 1):
offset = 0
yield word[offset:offset + n]
while offset + n < len(word):
offset += 1
yield word[offset:offset + n]
if offset == 0:
break
WORD_RE = re.compile(r"(?u)\b\w\w+\b")
def word_ngrams(text, ngram_range):
tokens = WORD_RE.findall(text.lower())
for n in range(ngram_range[0], ngram_range[1] + 1):
for i in range(len(tokens) - n + 1):
yield " ".join(tokens[i:i + n])
class LanguageIdentifier:
def __init__(self, model_dir=None, batch_size=256):
import onnxruntime as ort
if model_dir is not None:
self.model_dir = Path(model_dir)
else:
self.model_dir = ROOT / "model" if (ROOT / "model" / "META.json").is_file() else ROOT
config_path = self.model_dir / "config.json"
self.config = json.loads(config_path.read_text(encoding="utf-8")) if config_path.is_file() else None
self.meta = json.loads((self.model_dir / "META.json").read_text(encoding="utf-8"))
for name in ("model.onnx", "vectorizer.json"):
if name in self.meta.get("sha256", {}):
digest = self.meta["sha256"][name]
if sha256(self.model_dir / name) != digest:
raise ValueError(f"LID artifact checksum mismatch: {name}")
cfg = json.loads((self.model_dir / "vectorizer.json").read_text(encoding="utf-8"))
self.char_vocab = cfg.get("char_vocabulary", cfg.get("vocabulary", {}))
self.char_ngram_range = tuple(cfg.get("char_ngram_range", cfg.get("ngram_range", (2, 5))))
self.word_vocab = cfg.get("word_vocabulary", {})
self.word_ngram_range = tuple(cfg.get("word_ngram_range", (1, 2)))
self.classes = np.asarray(self.meta["classes"])
if self.config is not None:
expected = {"classes": self.classes.tolist(), "model_file": "model.onnx",
"vectorizer_file": "vectorizer.json", "metadata_file": "META.json"}
if any(self.config.get(k) != v for k, v in expected.items()):
raise ValueError("LID config does not match model artifacts")
if batch_size < 1:
raise ValueError("batch_size must be positive")
self.batch_size = batch_size
options = ort.SessionOptions()
options.intra_op_num_threads = 1
self.session = ort.InferenceSession(str(self.model_dir / "model.onnx"), options,
providers=["CPUExecutionProvider"])
def features(self, texts):
char_rows = []
word_rows = []
for text in texts:
if not isinstance(text, str):
raise TypeError("LID input must contain strings")
c_counts = Counter(self.char_vocab[g] for g in char_wb(text, self.char_ngram_range)
if g in self.char_vocab)
char_rows.append(c_counts)
if self.word_vocab:
w_counts = Counter(self.word_vocab[w] for w in word_ngrams(text, self.word_ngram_range)
if w in self.word_vocab)
word_rows.append(w_counts)
w_c = max(1, max(map(len, char_rows), default=0))
c_ids = np.zeros((len(char_rows), w_c), dtype=np.int64)
c_counts = np.zeros((len(char_rows), w_c), dtype=np.float32)
for i, row in enumerate(char_rows):
if row:
c_ids[i, :len(row)] = list(row.keys())
c_counts[i, :len(row)] = list(row.values())
if self.word_vocab:
w_w = max(1, max(map(len, word_rows), default=0))
w_ids = np.zeros((len(word_rows), w_w), dtype=np.int64)
w_counts = np.zeros((len(word_rows), w_w), dtype=np.float32)
for i, row in enumerate(word_rows):
if row:
w_ids[i, :len(row)] = list(row.keys())
w_counts[i, :len(row)] = list(row.values())
return {"char_ids": c_ids, "char_counts": c_counts, "word_ids": w_ids, "word_counts": w_counts}
return {"char_ids": c_ids, "char_counts": c_counts}
def predict_proba(self, texts):
texts = list(texts)
if not texts:
return np.empty((0, len(self.classes)), dtype=np.float32)
results = []
for start in range(0, len(texts), self.batch_size):
batch_texts = texts[start:start + self.batch_size]
results.append(self.session.run(["probabilities"], self.features(batch_texts))[0])
return np.concatenate(results) if results else np.empty((0, len(self.classes)), dtype=np.float32)
def predict(self, texts):
texts = list(texts)
if not texts:
return np.empty(0, dtype=str)
return self.classes[self.predict_proba(texts).argmax(axis=1)]
def describe(self):
return {"name": self.meta["id"], "version": self.meta["version"],
"sha256": self.meta["sha256"], "backend": "onnxruntime",
"decision_rule": "argmax probabilities; classes ordered as in model passport"}