Spaces:
Running
Running
Fix examples
#3
by iamleonie - opened
- .dockerignore +0 -6
- README.md +21 -12
- server.py +28 -44
- static/index.html +15 -9
.dockerignore
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
.git
|
| 2 |
-
.gitattributes
|
| 3 |
-
__pycache__/
|
| 4 |
-
*.pyc
|
| 5 |
-
.venv/
|
| 6 |
-
*.md
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,13 +1,22 @@
|
|
| 1 |
-
---
|
| 2 |
title: English spellchecker
|
| 3 |
-
emoji: ✍️
|
| 4 |
-
colorFrom: purple
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 7860
|
| 8 |
-
header: mini
|
| 9 |
-
pinned: false
|
| 10 |
-
private: true
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
title: English spellchecker
|
| 3 |
+
emoji: ✍️
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
header: mini
|
| 9 |
+
pinned: false
|
| 10 |
+
private: true
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# English spellchecker
|
| 14 |
+
|
| 15 |
+
A focused grammar and spelling correction demo powered by [`LiquidAI/LFM2.5-Spellchecker-350M`](https://huggingface.co/LiquidAI/LFM2.5-Spellchecker-350M).
|
| 16 |
+
|
| 17 |
+
The FastAPI backend loads the private model on CPU and exposes word-level corrections to the custom frontend. The interface checks text automatically, visualizes edits in context, provides confidence and iteration controls, and includes concise examples.
|
| 18 |
+
|
| 19 |
+
- `server.py` — model loading, correction API, health endpoint, and static serving
|
| 20 |
+
- `static/index.html` — application structure and interaction logic
|
| 21 |
+
- `static/style.css` — shared LiquidAI demo design and animations
|
| 22 |
+
- `Dockerfile` and `requirements.txt` — reproducible CPU runtime
|
server.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
"""FastAPI backend for the LFM2.5 Spellchecker demo (Docker Space).
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
uvicorn server:app --host 0.0.0.0 --port 7860
|
| 4 |
"""
|
| 5 |
import difflib
|
|
@@ -13,43 +20,17 @@ from fastapi.staticfiles import StaticFiles
|
|
| 13 |
from pydantic import BaseModel
|
| 14 |
from transformers import AutoModel
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
try: # cgroup v2
|
| 21 |
-
raw = open("/sys/fs/cgroup/cpu.max").read().split()
|
| 22 |
-
if raw and raw[0] != "max":
|
| 23 |
-
return max(1, round(int(raw[0]) / int(raw[1])))
|
| 24 |
-
except (OSError, ValueError):
|
| 25 |
-
pass
|
| 26 |
-
try: # cgroup v1
|
| 27 |
-
q = int(open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").read())
|
| 28 |
-
p = int(open("/sys/fs/cgroup/cpu/cpu.cfs_period_us").read())
|
| 29 |
-
if q > 0:
|
| 30 |
-
return max(1, q // p)
|
| 31 |
-
except (OSError, ValueError):
|
| 32 |
-
pass
|
| 33 |
-
try:
|
| 34 |
-
return max(1, len(os.sched_getaffinity(0)))
|
| 35 |
-
except AttributeError:
|
| 36 |
-
return os.cpu_count() or 1
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
_CPUS = _effective_cpus()
|
| 40 |
-
torch.set_num_threads(_CPUS)
|
| 41 |
-
try:
|
| 42 |
-
torch.set_num_interop_threads(1)
|
| 43 |
-
except RuntimeError:
|
| 44 |
-
pass
|
| 45 |
-
|
| 46 |
-
MODEL_ID = os.environ.get("SPELLCHECKER_MODEL", "LiquidAI/LFM2.5-Encoder-350M-Spellchecker")
|
| 47 |
-
MODEL_REV = os.environ.get("SPELLCHECKER_REVISION", "main")
|
| 48 |
STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
| 49 |
|
| 50 |
-
print(f"[server] loading {MODEL_ID}@{MODEL_REV}
|
|
|
|
|
|
|
| 51 |
_model = AutoModel.from_pretrained(MODEL_ID, revision=MODEL_REV, trust_remote_code=True,
|
| 52 |
-
token=os.environ.get("HF_TOKEN")).
|
| 53 |
_mem_bytes = sum(t.numel() * t.element_size() for t in (*_model.parameters(), *_model.buffers()))
|
| 54 |
_mem_human = (f"{_mem_bytes / 1024**3:.2f} GB" if _mem_bytes >= 1024**3
|
| 55 |
else f"{_mem_bytes / 1024**2:.0f} MB")
|
|
@@ -66,9 +47,9 @@ _ATTACH_LEFT = re.compile(r'\s+([.,!?;:%)\]}»…])')
|
|
| 66 |
_ATTACH_RIGHT = re.compile(r'([(\[{«])\s+')
|
| 67 |
_NT = re.compile(r"(\w+?)(n['’]t)\b", re.I) # don't->do n't, can't->ca n't
|
| 68 |
_CONTR = re.compile(r"(\w)(['’](?:s|re|ve|ll|d|m))\b", re.I) # let's->let 's, I'm->I 'm
|
| 69 |
-
_REJOIN_NT = re.compile(r"\s+(n['’]t)\b", re.I) # do n't->don't
|
| 70 |
-
_REJOIN_CONTR = re.compile(r"\s+(['’](?:s|re|ve|ll|d|m))\b", re.I) # let 's->let's
|
| 71 |
-
_REJOIN_SPLIT_APOSTROPHE = re.compile(r"(?<=\w)\s+(['’])\s+(?=(?:s|t|d|m|re|ve|ll)\b)", re.I)
|
| 72 |
|
| 73 |
|
| 74 |
def tokenize(text: str) -> str:
|
|
@@ -78,15 +59,18 @@ def tokenize(text: str) -> str:
|
|
| 78 |
return re.sub(r"\s+", " ", text).strip()
|
| 79 |
|
| 80 |
|
| 81 |
-
def detok(text: str) -> str:
|
| 82 |
-
text = _ATTACH_LEFT.sub(r"\1", text)
|
| 83 |
-
text = _ATTACH_RIGHT.sub(r"\1", text)
|
| 84 |
-
text = _REJOIN_NT.sub(r"\1", text) # do n't->don't
|
| 85 |
-
text = _REJOIN_CONTR.sub(r"\1", text) # let 's->let's
|
| 86 |
-
text = _REJOIN_SPLIT_APOSTROPHE.sub(r"\1", text) # don ' t->don't
|
| 87 |
-
return re.sub(r"\s+", " ", text).strip()
|
| 88 |
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
_PROBE_IN = "That's a fair point, let's discuss it tomorrow."
|
| 91 |
try:
|
| 92 |
_PROBE_OUT = detok(_model.correct([tokenize(_PROBE_IN)], max_iter=3)[0])
|
|
|
|
| 1 |
"""FastAPI backend for the LFM2.5 Spellchecker demo (Docker Space).
|
| 2 |
|
| 3 |
+
Loads the published model from the Hub (pinned to `main`, so the Space always serves the current best
|
| 4 |
+
checkpoint), exposes POST /api/correct, and serves the static frontend in static/. No Gradio.
|
| 5 |
+
|
| 6 |
+
The model repo is private, so HF_TOKEN (a Space secret) is needed to pull it. Pinned library versions
|
| 7 |
+
(see requirements.txt) match the environment the model was validated against — the encoder's custom
|
| 8 |
+
bidirectional-mask code is sensitive to the transformers version.
|
| 9 |
+
|
| 10 |
uvicorn server:app --host 0.0.0.0 --port 7860
|
| 11 |
"""
|
| 12 |
import difflib
|
|
|
|
| 20 |
from pydantic import BaseModel
|
| 21 |
from transformers import AutoModel
|
| 22 |
|
| 23 |
+
MODEL_ID = os.environ.get("SPELLCHECKER_MODEL", "LiquidAI/LFM2.5-Spellchecker-350M")
|
| 24 |
+
# Pin to the EXACT published commit so the container can never serve stale cached weights/remote-code
|
| 25 |
+
# (the bug we hit: a rebuild kept serving old, tagger-only behaviour). Bump on each publish, or override.
|
| 26 |
+
MODEL_REV = os.environ.get("SPELLCHECKER_REVISION", "65a4a90af31205d2f7ef66b6a68d7b3d276adfdd")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
| 28 |
|
| 29 |
+
print(f"[server] loading {MODEL_ID}@{MODEL_REV} ...", flush=True)
|
| 30 |
+
# fp16 (half the memory; the published weights are fp16). Casting the whole model uniformly avoids the
|
| 31 |
+
# mixed-dtype error — torch 2.12 runs fp16 matmul on CPU fine.
|
| 32 |
_model = AutoModel.from_pretrained(MODEL_ID, revision=MODEL_REV, trust_remote_code=True,
|
| 33 |
+
token=os.environ.get("HF_TOKEN")).half().eval()
|
| 34 |
_mem_bytes = sum(t.numel() * t.element_size() for t in (*_model.parameters(), *_model.buffers()))
|
| 35 |
_mem_human = (f"{_mem_bytes / 1024**3:.2f} GB" if _mem_bytes >= 1024**3
|
| 36 |
else f"{_mem_bytes / 1024**2:.0f} MB")
|
|
|
|
| 47 |
_ATTACH_RIGHT = re.compile(r'([(\[{«])\s+')
|
| 48 |
_NT = re.compile(r"(\w+?)(n['’]t)\b", re.I) # don't->do n't, can't->ca n't
|
| 49 |
_CONTR = re.compile(r"(\w)(['’](?:s|re|ve|ll|d|m))\b", re.I) # let's->let 's, I'm->I 'm
|
| 50 |
+
_REJOIN_NT = re.compile(r"\s+(n['’]t)\b", re.I) # do n't->don't
|
| 51 |
+
_REJOIN_CONTR = re.compile(r"\s+(['’](?:s|re|ve|ll|d|m))\b", re.I) # let 's->let's
|
| 52 |
+
_REJOIN_SPLIT_APOSTROPHE = re.compile(r"(?<=\w)\s+(['’])\s+(?=(?:s|t|d|m|re|ve|ll)\b)", re.I)
|
| 53 |
|
| 54 |
|
| 55 |
def tokenize(text: str) -> str:
|
|
|
|
| 59 |
return re.sub(r"\s+", " ", text).strip()
|
| 60 |
|
| 61 |
|
| 62 |
+
def detok(text: str) -> str:
|
| 63 |
+
text = _ATTACH_LEFT.sub(r"\1", text)
|
| 64 |
+
text = _ATTACH_RIGHT.sub(r"\1", text)
|
| 65 |
+
text = _REJOIN_NT.sub(r"\1", text) # do n't->don't
|
| 66 |
+
text = _REJOIN_CONTR.sub(r"\1", text) # let 's->let's
|
| 67 |
+
text = _REJOIN_SPLIT_APOSTROPHE.sub(r"\1", text) # don ' t->don't
|
| 68 |
+
return re.sub(r"\s+", " ", text).strip()
|
| 69 |
|
| 70 |
|
| 71 |
+
# Startup self-test over the REAL user path (tokenize -> correct -> detok), logged + exposed at
|
| 72 |
+
# /api/health: a correctly-deployed full system leaves this clean sentence UNCHANGED. If it changes,
|
| 73 |
+
# the deploy is wrong (stale model, reranker inactive, or contraction handling broken) — no guessing.
|
| 74 |
_PROBE_IN = "That's a fair point, let's discuss it tomorrow."
|
| 75 |
try:
|
| 76 |
_PROBE_OUT = detok(_model.correct([tokenize(_PROBE_IN)], max_iter=3)[0])
|
static/index.html
CHANGED
|
@@ -7,22 +7,29 @@
|
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 10 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
</head>
|
| 12 |
<body>
|
| 13 |
<div class="wrap">
|
| 14 |
<header class="site-intro">
|
| 15 |
<h1>English <em>spellchecker</em></h1>
|
| 16 |
-
<p class="sub">Correct English spelling and grammar with LFM2.5.</p>
|
| 17 |
</header>
|
| 18 |
|
| 19 |
<section class="model-strip is-loading" id="model-strip" aria-label="Model status">
|
| 20 |
<span class="model-logo-frame" aria-hidden="true">
|
| 21 |
-
<img id="model-logo" class="model-logo" width="56" height="59" alt=""
|
| 22 |
</span>
|
| 23 |
<div class="model-copy">
|
| 24 |
<div class="model-name-row">
|
| 25 |
-
<a href="https://huggingface.co/LiquidAI/LFM2.5-
|
| 26 |
<span class="model-variant">350M parameters</span>
|
| 27 |
</div>
|
| 28 |
</div>
|
|
@@ -33,7 +40,9 @@
|
|
| 33 |
</div>
|
| 34 |
<div class="load-track" role="progressbar" aria-label="Model loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="load-progress"></span></div>
|
| 35 |
</div>
|
|
|
|
| 36 |
</section>
|
|
|
|
| 37 |
|
| 38 |
<main>
|
| 39 |
<section class="spell-workspace" id="spell-workspace" aria-label="Spellchecker">
|
|
@@ -157,13 +166,10 @@ const EXAMPLES = [
|
|
| 157 |
];
|
| 158 |
|
| 159 |
const $ = id => document.getElementById(id);
|
| 160 |
-
|
| 161 |
-
// Incognito / Safari block the third-party .hf.space cookie, so carry that token
|
| 162 |
-
// on same-origin API requests, else /api/* returns 404.
|
| 163 |
-
const __sign = new URLSearchParams(location.search).get("__sign");
|
| 164 |
const signedUrl = path => {
|
| 165 |
const url = new URL(path, location.href);
|
| 166 |
-
if (
|
| 167 |
return url;
|
| 168 |
};
|
| 169 |
const input = $("input"), output = $("output"), copy = $("copy"), workspace = $("spell-workspace");
|
|
|
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 10 |
+
<script>
|
| 11 |
+
(() => {
|
| 12 |
+
const sign = new URLSearchParams(location.search).get("__sign");
|
| 13 |
+
const suffix = sign ? `?__sign=${encodeURIComponent(sign)}` : "";
|
| 14 |
+
window.__STATIC_ASSET_SUFFIX = suffix;
|
| 15 |
+
document.write(`<link rel="stylesheet" href="./style.css${suffix}">`);
|
| 16 |
+
})();
|
| 17 |
+
</script>
|
| 18 |
</head>
|
| 19 |
<body>
|
| 20 |
<div class="wrap">
|
| 21 |
<header class="site-intro">
|
| 22 |
<h1>English <em>spellchecker</em></h1>
|
| 23 |
+
<p class="sub">Correct English spelling and grammar privately with LFM2.5.</p>
|
| 24 |
</header>
|
| 25 |
|
| 26 |
<section class="model-strip is-loading" id="model-strip" aria-label="Model status">
|
| 27 |
<span class="model-logo-frame" aria-hidden="true">
|
| 28 |
+
<img id="model-logo" class="model-logo" width="56" height="59" alt="">
|
| 29 |
</span>
|
| 30 |
<div class="model-copy">
|
| 31 |
<div class="model-name-row">
|
| 32 |
+
<a href="https://huggingface.co/LiquidAI/LFM2.5-Spellchecker-350M" target="_blank" rel="noopener">LFM2.5 Spellchecker</a>
|
| 33 |
<span class="model-variant">350M parameters</span>
|
| 34 |
</div>
|
| 35 |
</div>
|
|
|
|
| 40 |
</div>
|
| 41 |
<div class="load-track" role="progressbar" aria-label="Model loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="load-progress"></span></div>
|
| 42 |
</div>
|
| 43 |
+
<span id="gpu" hidden></span>
|
| 44 |
</section>
|
| 45 |
+
<script>document.getElementById("model-logo").src = `./lliquid.gif${window.__STATIC_ASSET_SUFFIX || ""}`;</script>
|
| 46 |
|
| 47 |
<main>
|
| 48 |
<section class="spell-workspace" id="spell-workspace" aria-label="Spellchecker">
|
|
|
|
| 166 |
];
|
| 167 |
|
| 168 |
const $ = id => document.getElementById(id);
|
| 169 |
+
const sign = new URLSearchParams(location.search).get("__sign");
|
|
|
|
|
|
|
|
|
|
| 170 |
const signedUrl = path => {
|
| 171 |
const url = new URL(path, location.href);
|
| 172 |
+
if (sign) url.searchParams.set("__sign", sign);
|
| 173 |
return url;
|
| 174 |
};
|
| 175 |
const input = $("input"), output = $("output"), copy = $("copy"), workspace = $("spell-workspace");
|