Skip to content

Commit 1162bee

Browse files
authored
feat: integrate PleIAs/CommonLingua byte-level LID model (#4)
Adds a `commonlingua` model wired into the registry, backed by a vendored copy of upstream's `model.py` (Apache 2.0, rev 43fe88d) so we don't need `weights_only=False` to load remote pickled code. The model is exposed via a new `[commonlingua]` optional extra that pulls only `torch` (no transformers stack); device selection mirrors AfroLID's MPS > CUDA > CPU. `requires_preprocessing = False` because the byte-level architecture relies on casing as a strong language signal — the OpenLID normer's lowercasing collapses Latin-script predictions. Eval on the full CommonLID dataset (373,230 samples) gives a micro accuracy of 77.58%, matching the model card's 77.63% claim.
1 parent 32bd116 commit 1162bee

10 files changed

Lines changed: 425 additions & 6 deletions

File tree

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ PACKAGE := src/commonlid
1313
.DEFAULT_GOAL := help
1414

1515
.PHONY: help venv \
16-
install install-all install-afrolid install-notebooks install-leaderboard \
16+
install install-all install-afrolid install-commonlingua install-notebooks install-leaderboard \
1717
lint format format-check typecheck \
1818
test test-slow test-all check \
1919
build clean \
@@ -24,6 +24,7 @@ help:
2424
@echo " venv Create a uv-managed virtualenv (.venv)"
2525
@echo " install Sync runtime + dev extras (lint/type/test)"
2626
@echo " install-afrolid install + the heavy [afrolid] extra (torch + transformers)"
27+
@echo " install-commonlingua install + the [commonlingua] extra (torch only)"
2728
@echo " install-notebooks install + the [notebooks] extra (jupyterlab + matplotlib)"
2829
@echo " install-leaderboard install + the [leaderboard] extra (gradio)"
2930
@echo " install-all install + every optional extra"
@@ -55,14 +56,17 @@ install:
5556
install-afrolid:
5657
uv sync --extra dev --extra afrolid $(PYTHON_FLAG)
5758

59+
install-commonlingua:
60+
uv sync --extra dev --extra commonlingua $(PYTHON_FLAG)
61+
5862
install-notebooks:
5963
uv sync --extra dev --extra notebooks $(PYTHON_FLAG)
6064

6165
install-leaderboard:
6266
uv sync --extra dev --extra leaderboard $(PYTHON_FLAG)
6367

6468
install-all:
65-
uv sync --extra dev --extra afrolid --extra notebooks --extra leaderboard $(PYTHON_FLAG)
69+
uv sync --extra dev --extra afrolid --extra commonlingua --extra notebooks --extra leaderboard $(PYTHON_FLAG)
6670

6771
lint:
6872
uv run ruff check $(SRC_DIRS)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ From PyPI:
3939
pip install commonlid # core deps + classical LID models
4040
pip install "commonlid[llm]" # + DSPy-based LLM evaluation
4141
pip install "commonlid[afrolid]" # + torch/transformers for AfroLID
42+
pip install "commonlid[commonlingua]" # + torch for the CommonLingua byte-level model
4243
pip install "commonlid[notebooks]" # + jupyterlab + matplotlib for paper_tables.ipynb
4344
pip install "commonlid[all]" # everything runtime-facing
4445
```
@@ -192,7 +193,7 @@ from commonlid import list_models, list_datasets
192193

193194
assert list_models() == [
194195
"AfroLID", "GlotLID", "OpenLID-v2", "cld2", "cld3",
195-
"fasttext", "funlangid", "pyfranc",
196+
"commonlingua", "fasttext", "funlangid", "pyfranc",
196197
]
197198
assert list_datasets() == [
198199
"bibles_300", "bibles_300_nano",
@@ -298,6 +299,7 @@ for line in preds_path.read_text().splitlines():
298299
| `fasttext` | [facebook/fasttext-language-identification](https://huggingface.co/facebook/fasttext-language-identification) | fasttext |
299300
| `pyfranc` | [pyfranc](https://pypi.org/project/pyfranc/) | Pure Python |
300301
| `AfroLID` | [UBC-NLP/afrolid_1.5](https://huggingface.co/UBC-NLP/afrolid_1.5) | Requires `[afrolid]` extra |
302+
| `commonlingua` | [PleIAs/CommonLingua](https://huggingface.co/PleIAs/CommonLingua) | 2.35M-param byte-level model, 334 languages; requires `[commonlingua]` extra |
301303
| `funlangid` | Vendored in `src/commonlid/vendor/fun_langid.py` | Simple char-4gram baseline |
302304

303305
LLM models are instantiated dynamically (`DSPyLLMModel`) and not

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ llm = [
5959
"botocore>=1.35",
6060
]
6161
cld3 = ["cld3-py>=3.1"]
62+
commonlingua = [
63+
# CommonLingua is a 2.35M-param byte-level model; needs torch but not the
64+
# transformers stack that [afrolid] pulls in.
65+
"torch>=2.4",
66+
]
6267
leaderboard = [
6368
# gradio 4.x imports HfFolder from huggingface_hub, which was removed in
6469
# huggingface-hub 1.0; gradio 5 dropped that import.
@@ -88,7 +93,7 @@ notebooks = [
8893
"nbclient>=0.10",
8994
]
9095
all = [
91-
"commonlid[afrolid,llm]",
96+
"commonlid[afrolid,llm,commonlingua]",
9297
]
9398

9499
[project.scripts]
@@ -208,6 +213,8 @@ omit = [
208213
# afrolid needs the heavy `[afrolid]` extra (torch + transformers); not
209214
# installed in dev and so exercised only via mocked unit tests.
210215
"src/commonlid/models/afrolid.py",
216+
# commonlingua needs the `[commonlingua]` extra (torch); same precedent.
217+
"src/commonlid/models/commonlingua.py",
211218
]
212219

213220
[tool.coverage.report]

src/commonlid/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from commonlid.models import afrolid as _afrolid # noqa: F401
1212
from commonlid.models import cld2 as _cld2 # noqa: F401
1313
from commonlid.models import cld3 as _cld3 # noqa: F401
14+
from commonlid.models import commonlingua as _commonlingua # noqa: F401
1415
from commonlid.models import fasttext_ft as _fasttext_ft # noqa: F401
1516
from commonlid.models import funlangid as _funlangid # noqa: F401
1617
from commonlid.models import glotlid as _glotlid # noqa: F401
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""CommonLingua: PleIAs' byte-level LID model (PleIAs/CommonLingua).
2+
3+
Requires the ``commonlid[commonlingua]`` extra (torch). The checkpoint
4+
embeds its own ``lang2idx`` map, so no separate metadata file is fetched.
5+
Device selection mirrors AfroLID: MPS > CUDA > CPU.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from collections.abc import Sequence
11+
from typing import TYPE_CHECKING, Any, ClassVar
12+
13+
from commonlid.core.lid_model import LIDModel
14+
from commonlid.core.registry import register_model
15+
16+
if TYPE_CHECKING:
17+
import torch
18+
19+
20+
@register_model
21+
class CommonLinguaModel(LIDModel):
22+
model_id = "commonlingua"
23+
# Byte-level model: casing carries strong language signal, so we feed
24+
# raw UTF-8 and skip the OpenLID normer (which lowercases everything).
25+
requires_preprocessing: ClassVar[bool] = False
26+
27+
_REPO_ID: ClassVar[str] = "PleIAs/CommonLingua"
28+
_CHECKPOINT_FILENAME: ClassVar[str] = "model.pt"
29+
_INTERNAL_BATCH: ClassVar[int] = 256
30+
31+
def __init__(self) -> None:
32+
super().__init__()
33+
self._model: Any = None
34+
self._idx2lang: dict[int, str] | None = None
35+
self._max_len: int | None = None
36+
self._device: str | None = None
37+
38+
def load(self) -> None:
39+
if self._loaded:
40+
return
41+
try:
42+
import torch
43+
except ImportError as exc:
44+
msg = "CommonLingua requires torch. Install with: pip install 'commonlid[commonlingua]'"
45+
raise ImportError(msg) from exc
46+
47+
from huggingface_hub import hf_hub_download
48+
49+
from commonlid.vendor.commonlingua.model import CONFIGS, ByteHybrid
50+
51+
ckpt_path = hf_hub_download(repo_id=self._REPO_ID, filename=self._CHECKPOINT_FILENAME)
52+
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
53+
54+
if torch.backends.mps.is_available():
55+
device = "mps"
56+
elif torch.cuda.is_available():
57+
device = "cuda"
58+
else:
59+
device = "cpu"
60+
61+
model = ByteHybrid( # type: ignore[no-untyped-call]
62+
num_classes=ckpt["num_classes"],
63+
max_len=ckpt["max_len"],
64+
**CONFIGS[ckpt["config"]],
65+
)
66+
model.load_state_dict(ckpt["model_state_dict"])
67+
model.eval().to(device)
68+
69+
self._model = model
70+
self._idx2lang = {v: k for k, v in ckpt["lang2idx"].items()}
71+
self._max_len = int(ckpt["max_len"])
72+
self._device = device
73+
super().load()
74+
75+
def _encode(self, texts: Sequence[str]) -> torch.Tensor:
76+
import numpy as np
77+
import torch
78+
79+
assert self._max_len is not None
80+
out = np.full((len(texts), self._max_len), 256, dtype=np.int64)
81+
for i, t in enumerate(texts):
82+
raw = t.encode("utf-8", errors="replace")[: self._max_len]
83+
if raw:
84+
out[i, : len(raw)] = np.frombuffer(raw, dtype=np.uint8)
85+
return torch.from_numpy(out)
86+
87+
def _predict_batch(self, texts: Sequence[str]) -> list[str | None]:
88+
import torch
89+
90+
if not self._loaded:
91+
self.load()
92+
assert self._idx2lang is not None
93+
assert self._device is not None
94+
95+
results: list[str | None] = []
96+
for start in range(0, len(texts), self._INTERNAL_BATCH):
97+
chunk = list(texts[start : start + self._INTERNAL_BATCH])
98+
batch = self._encode(chunk).to(self._device)
99+
with torch.no_grad():
100+
logits = self._model(batch)
101+
pred_idx = logits.argmax(dim=-1).cpu().tolist()
102+
results.extend(self._idx2lang[int(i)] for i in pred_idx)
103+
return results
104+
105+
def discover_supported_languages(self) -> frozenset[str]:
106+
"""Return every ISO 639-3 code in the model's ``lang2idx`` map."""
107+
if not self._loaded:
108+
self.load()
109+
assert self._idx2lang is not None
110+
codes: set[str] = set()
111+
for code in self._idx2lang.values():
112+
conformed = self._conform(code)
113+
if conformed is not None:
114+
codes.add(conformed)
115+
return frozenset(codes)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Vendored PleIAs/CommonLingua architecture (Apache 2.0).
2+
3+
Source: https://huggingface.co/PleIAs/CommonLingua
4+
"""

0 commit comments

Comments
 (0)