Upload folder using huggingface_hub
Browse files- .gitattributes +6 -0
- __pycache__/model.cpython-312.pyc +0 -0
- app.py +5 -2
- model.py +50 -0
- static/assets/long-context-extraction.png +3 -0
- static/assets/page@b4824da6aaeb63052ccfb891e538eea3.webm +3 -0
- static/assets/teaser-gliner25-v2.gif +3 -0
- static/assets/teaser-gliner25-v2.html +89 -0
- static/assets/teaser-gliner25-v3.gif +3 -0
- static/assets/teaser-gliner25-v4.gif +3 -0
- static/assets/teaser-gliner25.gif +3 -0
- static/assets/teaser-gliner25.svg +42 -0
- static/assets/transparent-dark-64.png +0 -0
- static/index.html +416 -54
.gitattributes
CHANGED
|
@@ -34,3 +34,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
static/assets/fox-laptop.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
static/assets/fox-laptop.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
static/assets/long-context-extraction.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
static/assets/page@b4824da6aaeb63052ccfb891e538eea3.webm filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
static/assets/teaser-gliner25-v2.gif filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
static/assets/teaser-gliner25-v3.gif filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
static/assets/teaser-gliner25-v4.gif filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
static/assets/teaser-gliner25.gif filter=lfs diff=lfs merge=lfs -text
|
__pycache__/model.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/model.cpython-312.pyc and b/__pycache__/model.cpython-312.pyc differ
|
|
|
app.py
CHANGED
|
@@ -55,8 +55,11 @@ def static_file(name):
|
|
| 55 |
|
| 56 |
@app.route("/api/config")
|
| 57 |
def config():
|
| 58 |
-
"""Demo content for the page: title, subtitle, example chips. No results.
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
@app.route("/api/ready")
|
|
|
|
| 55 |
|
| 56 |
@app.route("/api/config")
|
| 57 |
def config():
|
| 58 |
+
"""Demo content for the page: title, subtitle, example chips. No results.
|
| 59 |
+
Demos whose examples carry derived fields (inlined texts, result previews)
|
| 60 |
+
expose model.config(); others are served straight from model.DEMO."""
|
| 61 |
+
cfg = model.config() if hasattr(model, "config") else model.DEMO
|
| 62 |
+
return {k: v for k, v in cfg.items() if k != "internal"}
|
| 63 |
|
| 64 |
|
| 65 |
@app.route("/api/ready")
|
model.py
CHANGED
|
@@ -63,6 +63,56 @@ DEMO = {
|
|
| 63 |
|
| 64 |
_HERE = os.path.dirname(os.path.abspath(__file__))
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
def warmup():
|
| 68 |
if MOCK:
|
|
|
|
| 63 |
|
| 64 |
_HERE = os.path.dirname(os.path.abspath(__file__))
|
| 65 |
|
| 66 |
+
def config():
|
| 67 |
+
"""DEMO for /api/config, with result previews baked from fixtures."""
|
| 68 |
+
out = {k: v for k, v in DEMO.items() if k != "code"}
|
| 69 |
+
out["mock"] = MOCK
|
| 70 |
+
out["examples"] = []
|
| 71 |
+
for ex in DEMO["examples"]:
|
| 72 |
+
ex2 = {**ex, "code": DEMO["code"]}
|
| 73 |
+
ex2["result"] = _result_preview(ex["fixture"])
|
| 74 |
+
out["examples"].append(ex2)
|
| 75 |
+
return out
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _result_preview(fixture_name, per_type=2):
|
| 79 |
+
"""Notebook-style Out[1] preview: the API response shape, truncated per label."""
|
| 80 |
+
with open(os.path.join(_HERE, "fixtures", fixture_name)) as f:
|
| 81 |
+
out = json.load(f)
|
| 82 |
+
entities = {}
|
| 83 |
+
for sp in out["spans"]:
|
| 84 |
+
entities.setdefault(sp["label"], [])
|
| 85 |
+
if len(entities[sp["label"]]) < per_type:
|
| 86 |
+
entities[sp["label"]].append({
|
| 87 |
+
"text": sp["text"],
|
| 88 |
+
"confidence": sp["score"],
|
| 89 |
+
"start": sp["start"],
|
| 90 |
+
"end": sp["end"],
|
| 91 |
+
**({"attributes": {g: [{ "label": a["label"], "score": a["score"]} for a in es]
|
| 92 |
+
for g, es in sp["attributes"].items()}} if sp.get("attributes") else {}),
|
| 93 |
+
})
|
| 94 |
+
counts = {}
|
| 95 |
+
for sp in out["spans"]:
|
| 96 |
+
counts[sp["label"]] = counts.get(sp["label"], 0) + 1
|
| 97 |
+
body = []
|
| 98 |
+
for label, entries in entities.items():
|
| 99 |
+
entry_strs = []
|
| 100 |
+
for e in entries:
|
| 101 |
+
base = (f'{{"text": {json.dumps(e["text"])}, "confidence": {e["confidence"]}, '
|
| 102 |
+
f'"start": {e["start"]}, "end": {e["end"]}')
|
| 103 |
+
if e.get("attributes"):
|
| 104 |
+
attrs = json.dumps(e["attributes"])
|
| 105 |
+
base += f', "attributes": {attrs}'
|
| 106 |
+
base += "}"
|
| 107 |
+
entry_strs.append(base)
|
| 108 |
+
more = counts[label] - len(entries)
|
| 109 |
+
parts = ", ".join(entry_strs)
|
| 110 |
+
if more > 0:
|
| 111 |
+
parts += f",\n # ... {more} more {label} span{'' if more == 1 else 's'}"
|
| 112 |
+
body.append(f' {json.dumps(label)}: [\n {parts}\n ]')
|
| 113 |
+
return f'{{\n "entities": {{\n' + ",\n".join(body) + "\n }\n}"
|
| 114 |
+
|
| 115 |
+
|
| 116 |
|
| 117 |
def warmup():
|
| 118 |
if MOCK:
|
static/assets/long-context-extraction.png
ADDED
|
Git LFS Details
|
static/assets/page@b4824da6aaeb63052ccfb891e538eea3.webm
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1998fa63026722c2990bb651bed66a078634d3145b613446c3571724ceeb4884
|
| 3 |
+
size 157063
|
static/assets/teaser-gliner25-v2.gif
ADDED
|
Git LFS Details
|
static/assets/teaser-gliner25-v2.html
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 6 |
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 7 |
+
<link href="https://www.nerdfonts.com/assets/css/webfont.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--paper: #F4F6F8;
|
| 11 |
+
--ink: #100E1C;
|
| 12 |
+
--c0: #FF6A2A; /* orange: product */
|
| 13 |
+
--c1: #7038FF; /* purple: person */
|
| 14 |
+
--c2: #0B7A78; /* teal: event */
|
| 15 |
+
--c3: #C2183B; /* red: location */
|
| 16 |
+
--ease: cubic-bezier(0.23, 1, 0.32, 1);
|
| 17 |
+
--mono: "0xProto Nerd Font", "0xProto Nerd", "JetBrains Mono", ui-monospace, Menlo, monospace;
|
| 18 |
+
}
|
| 19 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 20 |
+
html, body { width: 1252px; height: 374px; }
|
| 21 |
+
body {
|
| 22 |
+
background: var(--paper); overflow: hidden; position: relative;
|
| 23 |
+
font-family: "Geist", "Helvetica Neue", Arial, sans-serif;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/* ============ sentence gets extracted, loops ============ */
|
| 27 |
+
.sentence {
|
| 28 |
+
position: absolute; inset: 0;
|
| 29 |
+
display: flex; align-items: center; justify-content: center;
|
| 30 |
+
animation: sentence-fade 7s linear infinite;
|
| 31 |
+
}
|
| 32 |
+
.sentence p {
|
| 33 |
+
font-size: 42px; font-weight: 500; color: var(--ink);
|
| 34 |
+
letter-spacing: -0.015em; white-space: nowrap;
|
| 35 |
+
}
|
| 36 |
+
@keyframes sentence-fade {
|
| 37 |
+
0% { opacity: 0; }
|
| 38 |
+
7% { opacity: 1; } /* fully in by 0.49s */
|
| 39 |
+
70% { opacity: 1; } /* long hold until 4.9s */
|
| 40 |
+
82% { opacity: 0; } /* out by 5.74s */
|
| 41 |
+
100% { opacity: 0; } /* rest until loop */
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.ent {
|
| 45 |
+
position: relative; white-space: nowrap;
|
| 46 |
+
border-radius: 6px; padding: 0 3px; margin: 0 1px;
|
| 47 |
+
border-bottom: 3px solid transparent;
|
| 48 |
+
animation: ent-hl 7s var(--ease) infinite;
|
| 49 |
+
animation-delay: calc(var(--d) * 1s);
|
| 50 |
+
animation-fill-mode: both;
|
| 51 |
+
}
|
| 52 |
+
/* the highlight lands 525ms after the entity's stagger tick */
|
| 53 |
+
@keyframes ent-hl {
|
| 54 |
+
0% { background-color: rgba(0,0,0,0); border-bottom-color: rgba(0,0,0,0); }
|
| 55 |
+
7.5% { background-color: var(--bg); border-bottom-color: var(--c); }
|
| 56 |
+
100% { background-color: var(--bg); border-bottom-color: var(--c); }
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.chip {
|
| 60 |
+
position: absolute; left: 3px; bottom: calc(100% + 6px);
|
| 61 |
+
font-family: var(--mono); font-size: 15px; font-weight: 600; line-height: 1.5;
|
| 62 |
+
color: var(--c); background: var(--bg);
|
| 63 |
+
border: 1.5px solid var(--c); border-radius: 5px; padding: 0 7px;
|
| 64 |
+
opacity: 0; transform: translateY(4px) scale(0.94);
|
| 65 |
+
animation: chip-in 7s var(--ease) infinite;
|
| 66 |
+
animation-delay: calc((var(--d) + 0.2) * 1s);
|
| 67 |
+
animation-fill-mode: both;
|
| 68 |
+
}
|
| 69 |
+
@keyframes chip-in {
|
| 70 |
+
0% { opacity: 0; transform: translateY(4px) scale(0.94); }
|
| 71 |
+
6% { opacity: 1; transform: translateY(0) scale(1); }
|
| 72 |
+
100% { opacity: 1; transform: translateY(0) scale(1); }
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* stagger ticks: 0.9s, 1.5s, 2.1s. orange=company(identity), purple=model, teal=date */
|
| 76 |
+
.e0 { --c: var(--c0); --bg: rgba(255,106,42,0.18); --d: 0.9; }
|
| 77 |
+
.e1 { --c: var(--c1); --bg: rgba(112,56,255,0.16); --d: 1.5; }
|
| 78 |
+
.e2 { --c: var(--c2); --bg: rgba(11,122,120,0.16); --d: 2.1; }
|
| 79 |
+
|
| 80 |
+
</style>
|
| 81 |
+
</head>
|
| 82 |
+
<body>
|
| 83 |
+
|
| 84 |
+
<div class="sentence">
|
| 85 |
+
<p><span class="ent e0">Fastino Labs<span class="chip">company</span></span> launches <span class="ent e1"> <span class="chip">model</span></span> on <span class="ent e2">Monday, August 24th<span class="chip">date</span></span>.</p>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
</body>
|
| 89 |
+
</html>
|
static/assets/teaser-gliner25-v3.gif
ADDED
|
Git LFS Details
|
static/assets/teaser-gliner25-v4.gif
ADDED
|
Git LFS Details
|
static/assets/teaser-gliner25.gif
ADDED
|
Git LFS Details
|
static/assets/teaser-gliner25.svg
ADDED
|
|
static/assets/transparent-dark-64.png
ADDED
|
static/index.html
CHANGED
|
@@ -9,6 +9,250 @@
|
|
| 9 |
<meta property="og:description" content="Sentiment, role, assertion and impact attached to every extracted span.">
|
| 10 |
<meta property="og:image" content="og-card.png">
|
| 11 |
<link rel="stylesheet" href="brand.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</head>
|
| 13 |
<body>
|
| 14 |
<div class="wrap">
|
|
@@ -17,7 +261,6 @@
|
|
| 17 |
<h1 id="title"></h1>
|
| 18 |
<p class="sub" id="subtitle"></p>
|
| 19 |
</div>
|
| 20 |
-
<img class="intro-mascot" src="assets/fox-laptop.png" alt="Fastino fox mascot on a laptop" width="132" height="132">
|
| 21 |
</header>
|
| 22 |
|
| 23 |
<main>
|
|
@@ -26,6 +269,12 @@
|
|
| 26 |
<div class="example-list" id="examples"></div>
|
| 27 |
</section>
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
<div class="workspace">
|
| 30 |
<section class="card">
|
| 31 |
<div class="panel-header"><div><h2>Text</h2><p>Edit below, or pick an example.</p></div></div>
|
|
@@ -34,37 +283,57 @@
|
|
| 34 |
|
| 35 |
<section class="card">
|
| 36 |
<div class="panel-header">
|
| 37 |
-
<div><h2>Spans and attributes</h2><p>Click a span to inspect its attribute groups.</p></div>
|
| 38 |
-
<
|
|
|
|
|
|
|
| 39 |
</div>
|
| 40 |
<div class="code-cell" id="code-cell" hidden>
|
| 41 |
-
<div class="code-cell-bar">
|
| 42 |
-
<
|
| 43 |
-
<button type="button" class="
|
|
|
|
| 44 |
<span class="copy-label">Copy</span>
|
| 45 |
<span class="copy-done" hidden>Copied</span>
|
| 46 |
</button>
|
| 47 |
</div>
|
| 48 |
<pre class="code-panel" id="code-panel"></pre>
|
| 49 |
-
<
|
| 50 |
</div>
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
<div class="stats">
|
| 56 |
-
<span>Latency<strong id="stat-ms">-</strong></span>
|
| 57 |
<span>Spans<strong id="stat-n">-</strong></span>
|
|
|
|
|
|
|
| 58 |
</div>
|
| 59 |
</div>
|
| 60 |
</section>
|
| 61 |
</div>
|
| 62 |
|
| 63 |
-
<p class="footnote">
|
| 64 |
-
|
| 65 |
role on people, assertion on conditions. Built by
|
| 66 |
<a class="labs-mark" href="https://fastino.ai" target="_blank" rel="noopener">Fastino Labs</a>.
|
| 67 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
</main>
|
| 69 |
</div>
|
| 70 |
|
|
@@ -73,6 +342,10 @@
|
|
| 73 |
const $ = (id) => document.getElementById(id);
|
| 74 |
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 75 |
let READY = false, timer = null, version = 0, labels = [], lastSpans = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
/* Badge tone. Semantic hues for sentiment/assertion/impact; everything else
|
| 78 |
falls back to the span's own label color at low alpha. All pairs AA. */
|
|
@@ -101,20 +374,26 @@ function attrBadges(attributes, i, spanColor) {
|
|
| 101 |
return html;
|
| 102 |
}
|
| 103 |
|
| 104 |
-
function spanColor(label) { return `var(--c${labels.indexOf(label) % 8})`; }
|
| 105 |
-
|
| 106 |
function render(text, spans) {
|
| 107 |
let html = "", prev = 0, i = 0;
|
| 108 |
for (const sp of spans) {
|
| 109 |
html += esc(text.slice(prev, sp.start));
|
| 110 |
const c = spanColor(sp.label);
|
| 111 |
-
|
| 112 |
-
|
|
|
|
| 113 |
prev = sp.end; i++;
|
| 114 |
}
|
| 115 |
return html + esc(text.slice(prev));
|
| 116 |
}
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
function renderLegend(spans) {
|
| 119 |
const counts = {};
|
| 120 |
for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
|
|
@@ -124,69 +403,115 @@ function renderLegend(spans) {
|
|
| 124 |
</span>`).join("");
|
| 125 |
}
|
| 126 |
|
| 127 |
-
function
|
| 128 |
-
|
| 129 |
-
if (idx == null || !lastSpans[idx]) { box.hidden = true; box.innerHTML = ""; return; }
|
| 130 |
-
const sp = lastSpans[idx];
|
| 131 |
-
const c = spanColor(sp.label);
|
| 132 |
-
const groups = Object.entries(sp.attributes || {});
|
| 133 |
-
box.innerHTML = `
|
| 134 |
-
<div class="inspector-head">
|
| 135 |
-
<span class="span-text">“${esc(sp.text)}”</span>
|
| 136 |
-
<span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span>
|
| 137 |
-
<span class="span-score">span score ${sp.score.toFixed(2)} · chars ${sp.start}–${sp.end}</span>
|
| 138 |
-
</div>
|
| 139 |
-
${groups.length ? `<div class="inspector-groups">${groups.map(([group, entries]) => `
|
| 140 |
-
<div class="attr-group">
|
| 141 |
-
<div class="attr-group-name">${esc(group)}</div>
|
| 142 |
-
${entries.map((e) => `
|
| 143 |
-
<div class="attr-row">
|
| 144 |
-
<span class="attr-label">${esc(e.label)}</span>
|
| 145 |
-
<span class="attr-score">${e.score.toFixed(2)}</span>
|
| 146 |
-
</div>
|
| 147 |
-
<div class="attr-meter"><i style="width:${(e.score * 100).toFixed(0)}%;--span-color:${c}"></i></div>`).join("")}
|
| 148 |
-
</div>`).join("")}</div>`
|
| 149 |
-
: `<div class="output-empty">No attributes on this span.</div>`}`;
|
| 150 |
-
box.hidden = false;
|
| 151 |
}
|
| 152 |
|
| 153 |
function setEmpty(state) {
|
| 154 |
const html = {
|
| 155 |
-
idle:
|
| 156 |
-
none:
|
| 157 |
-
|
|
|
|
|
|
|
| 158 |
}[state];
|
| 159 |
$("output").innerHTML = html;
|
| 160 |
}
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
async function infer() {
|
| 163 |
if (!READY) return;
|
| 164 |
const v = ++version, text = $("input").value;
|
| 165 |
-
if (!text.trim()) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
let data;
|
| 167 |
try {
|
| 168 |
const r = await fetch("api/infer", { method: "POST",
|
| 169 |
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
|
| 170 |
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 171 |
data = await r.json();
|
| 172 |
-
} catch (e) {
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
const spans = data.spans || [];
|
| 175 |
lastSpans = spans;
|
| 176 |
labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
|
| 177 |
$("output").innerHTML = spans.length ? render(text, spans) : "";
|
| 178 |
if (!spans.length) setEmpty("none");
|
|
|
|
| 179 |
renderLegend(spans);
|
| 180 |
-
renderInspector(null);
|
| 181 |
$("stat-ms").textContent = `${data.ms} ms`;
|
| 182 |
$("stat-n").textContent = spans.length;
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
-
function inferSoon(delay
|
| 186 |
$("input").addEventListener("input", () => inferSoon());
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
$("copy-btn").addEventListener("click", async () => {
|
| 189 |
-
|
|
|
|
| 190 |
$("copy-btn").querySelector(".copy-label").hidden = true;
|
| 191 |
$("copy-btn").querySelector(".copy-done").hidden = false;
|
| 192 |
setTimeout(() => {
|
|
@@ -194,6 +519,34 @@ $("copy-btn").addEventListener("click", async () => {
|
|
| 194 |
$("copy-btn").querySelector(".copy-done").hidden = true;
|
| 195 |
}, 1400);
|
| 196 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
$("output").addEventListener("click", (e) => {
|
| 198 |
const el = e.target.closest(".span");
|
| 199 |
if (!el) return;
|
|
@@ -207,10 +560,10 @@ $("output").addEventListener("click", (e) => {
|
|
| 207 |
|
| 208 |
(async () => {
|
| 209 |
const cfg = await fetchConfig();
|
|
|
|
| 210 |
document.title = `${cfg.title.replace(/<[^>]+>/g, "")} - GLiNER 2.5`;
|
| 211 |
$("title").innerHTML = cfg.title;
|
| 212 |
$("subtitle").innerHTML = cfg.subtitle;
|
| 213 |
-
$("code-panel").textContent = cfg.code || "";
|
| 214 |
|
| 215 |
cfg.examples.forEach((ex, i) => {
|
| 216 |
const b = document.createElement("button");
|
|
@@ -219,13 +572,22 @@ $("output").addEventListener("click", (e) => {
|
|
| 219 |
b.addEventListener("click", () => {
|
| 220 |
document.querySelectorAll(".ex").forEach((x) => x.classList.toggle("active", x === b));
|
| 221 |
$("input").value = ex.text;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
inferSoon(40);
|
| 223 |
});
|
| 224 |
$("examples").append(b);
|
| 225 |
});
|
| 226 |
|
| 227 |
setEmpty("idle");
|
| 228 |
-
if (cfg.examples[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
READY = await waitForServer();
|
| 230 |
if (READY) infer();
|
| 231 |
else setEmpty("error");
|
|
|
|
| 9 |
<meta property="og:description" content="Sentiment, role, assertion and impact attached to every extracted span.">
|
| 10 |
<meta property="og:image" content="og-card.png">
|
| 11 |
<link rel="stylesheet" href="brand.css">
|
| 12 |
+
<style>
|
| 13 |
+
/* demo-specific: fixed-height workbench. Both cards share one silhouette and
|
| 14 |
+
scroll internally; the rail is a minimap of the whole document (aria-hidden -
|
| 15 |
+
the annotated text itself is the accessible output). */
|
| 16 |
+
/* page fits one viewport: taller top margin, shorter cards, tighter footer.
|
| 17 |
+
Header mascot removed: header sits tighter, cards take the reclaimed space.
|
| 18 |
+
Column widened to 1280 so the boxes run wide; the "white space" lives inside
|
| 19 |
+
the boxes as extra padding instead of big outer margins. */
|
| 20 |
+
.wrap { max-width: 1280px; padding-top: 40px; padding-bottom: 26px; }
|
| 21 |
+
textarea#input, .output-body { padding-left: 24px; padding-right: 24px; }
|
| 22 |
+
.examples-panel { margin-top: 30px; }
|
| 23 |
+
.site-intro .sub { max-width: 92ch; }
|
| 24 |
+
.workspace > .card { height: min(59vh, 488px); }
|
| 25 |
+
textarea#input { resize: none; }
|
| 26 |
+
.doc-cols { display: grid; grid-template-columns: 1fr 42px; flex: 1; min-height: 0; position: relative; overflow: hidden; }
|
| 27 |
+
.doc-cols[hidden] { display: none; }
|
| 28 |
+
.output-body { min-height: 0; }
|
| 29 |
+
.rail { position: relative; margin: 14px 12px 14px 8px; background: var(--ash); border-radius: 6px; min-height: 120px; }
|
| 30 |
+
.rail-tick {
|
| 31 |
+
position: absolute; left: 0; right: 0; height: 3px; border-radius: 2px;
|
| 32 |
+
background: var(--span-color);
|
| 33 |
+
opacity: 0; animation: span-in 380ms var(--ease-out) forwards;
|
| 34 |
+
animation-delay: calc(var(--i) * 55ms);
|
| 35 |
+
}
|
| 36 |
+
.rail-caption, .rail-end {
|
| 37 |
+
position: absolute; left: 0; white-space: nowrap;
|
| 38 |
+
font-family: var(--font-mono); font-size: 10px; color: var(--ink-faint);
|
| 39 |
+
}
|
| 40 |
+
.rail-caption { top: -17px; }
|
| 41 |
+
.rail-end { bottom: -17px; }
|
| 42 |
+
@media (max-width: 860px) {
|
| 43 |
+
.workspace > .card { height: auto; }
|
| 44 |
+
.workspace > .card:last-child { height: min(59vh, 488px); }
|
| 45 |
+
textarea#input { min-height: 380px; }
|
| 46 |
+
.doc-cols { grid-template-columns: 1fr; }
|
| 47 |
+
.rail { display: none; }
|
| 48 |
+
}
|
| 49 |
+
.model-note { font-size: 11px; color: var(--ink-faint); }
|
| 50 |
+
.model-note a { color: var(--ink-soft); text-decoration: underline; text-decoration-color: var(--ash); text-underline-offset: 2px; }
|
| 51 |
+
.model-note a:hover { color: var(--orange-deep); text-decoration-color: var(--orange-deep); }
|
| 52 |
+
/* model attribution moved to the footnote: linked model name, code styling kept */
|
| 53 |
+
.footnote a[href*="huggingface.co"] { color: var(--ink-soft); text-decoration-color: var(--ash); text-underline-offset: 2px; }
|
| 54 |
+
.footnote a[href*="huggingface.co"]:hover { color: var(--orange-deep); text-decoration-color: var(--orange-deep); }
|
| 55 |
+
/* footer link row: centered, fox logo as separator */
|
| 56 |
+
.footer-links {
|
| 57 |
+
display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
|
| 58 |
+
gap: 14px; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--ash);
|
| 59 |
+
}
|
| 60 |
+
.footer-links a {
|
| 61 |
+
font-size: 13px; font-weight: 500; color: var(--ink-soft); text-decoration: none;
|
| 62 |
+
transition: color 140ms var(--ease-out);
|
| 63 |
+
}
|
| 64 |
+
.footer-links a:hover { color: var(--orange-deep); }
|
| 65 |
+
.footer-links a:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
|
| 66 |
+
.footer-links img { width: 16px; height: 16px; display: block; opacity: 0.9; }
|
| 67 |
+
/* local override of brand.css uppercase transform: mixed-case "Examples" */
|
| 68 |
+
.examples-label { text-transform: none; letter-spacing: 0.04em; }
|
| 69 |
+
/* controls back at the brand-default bottom: the capped output scrolls
|
| 70 |
+
internally, so the footer is always reachable (that's why they were on top) */
|
| 71 |
+
/* notebook Out[1] block replaced by tabs: code | result */
|
| 72 |
+
.code-tab {
|
| 73 |
+
font: inherit; font-family: var(--font-mono); font-size: 11px; font-weight: 600;
|
| 74 |
+
color: var(--ink-faint); background: none; border: 0; border-bottom: 2px solid transparent;
|
| 75 |
+
padding: 4px 10px 5px; cursor: pointer; letter-spacing: 0.02em;
|
| 76 |
+
transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
|
| 77 |
+
}
|
| 78 |
+
.code-tab:hover { color: var(--ink-soft); }
|
| 79 |
+
.code-tab.active { color: var(--ink); border-bottom-color: var(--orange); }
|
| 80 |
+
.code-tab:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
|
| 81 |
+
.cell-result { color: var(--ink-soft); }
|
| 82 |
+
.cell-result .jk { color: var(--purple); }
|
| 83 |
+
.cell-result .js { color: var(--ok); }
|
| 84 |
+
.cell-result .jn { color: var(--orange-deep); }
|
| 85 |
+
.cell-result .jc { color: var(--ink-faint); font-style: italic; }
|
| 86 |
+
/* wider side padding on highlighted spans (local override of brand.css 1px 2px) */
|
| 87 |
+
#output .span { padding: 1px 7px; }
|
| 88 |
+
/* grid children must not let wide pre content inflate the tracks */
|
| 89 |
+
.workspace > .card { min-width: 0; }
|
| 90 |
+
.code-cell { max-width: 100%; margin: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
| 91 |
+
.code-cell[hidden] { display: none; }
|
| 92 |
+
.code-panel { max-width: 100%; flex: 1; min-height: 0; overflow: auto; }
|
| 93 |
+
/* mono only for actual code: the code panel + result JSON. Everything else
|
| 94 |
+
(span labels, tabs, stats, legend counts, copy button) uses the body font. */
|
| 95 |
+
#output .span-label { font-family: var(--font-body); font-weight: 700; letter-spacing: 0.06em; }
|
| 96 |
+
.code-tab { font-family: var(--font-body); font-weight: 600; letter-spacing: 0.04em; }
|
| 97 |
+
.copy-btn { font-family: var(--font-body); }
|
| 98 |
+
.legend-count { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
|
| 99 |
+
.stats strong { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
|
| 100 |
+
.rail-caption, .rail-end { font-family: var(--font-body); font-variant-numeric: tabular-nums; }
|
| 101 |
+
/* label rides inside the span: all-caps bold, vertically centered, right after the text */
|
| 102 |
+
.span-label {
|
| 103 |
+
font-family: var(--font-mono); font-size: 8.5px; font-weight: 700;
|
| 104 |
+
text-transform: uppercase; letter-spacing: 0.05em;
|
| 105 |
+
color: var(--span-color);
|
| 106 |
+
margin-left: 4px; vertical-align: middle; line-height: 1;
|
| 107 |
+
white-space: nowrap;
|
| 108 |
+
opacity: 0; animation: chip-in 280ms var(--ease-out) forwards;
|
| 109 |
+
animation-delay: calc(var(--i) * 40ms + 200ms);
|
| 110 |
+
}
|
| 111 |
+
@keyframes chip-in { from { opacity: 0; } to { opacity: 1; } }
|
| 112 |
+
/* frosted hover pill (approved mock V1-frosted): one line, type | score | chars.
|
| 113 |
+
Above the span, centered; flips below when the span sits near the panel top.
|
| 114 |
+
pointer-events none so it never blocks the neighbouring span. */
|
| 115 |
+
.tip {
|
| 116 |
+
position: absolute; bottom: calc(100% + 7px); left: 50%;
|
| 117 |
+
transform: translateX(-50%) translateY(2px);
|
| 118 |
+
background: color-mix(in srgb, var(--greige) 78%, transparent);
|
| 119 |
+
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
| 120 |
+
color: var(--ink); font-family: var(--font-mono); font-size: 10.5px; line-height: 1;
|
| 121 |
+
padding: 6px 10px; border-radius: 6px; white-space: nowrap;
|
| 122 |
+
border: 1px solid color-mix(in srgb, var(--ink) 10%, transparent);
|
| 123 |
+
box-shadow: 0 2px 10px rgba(16, 14, 28, 0.10);
|
| 124 |
+
opacity: 0; pointer-events: none;
|
| 125 |
+
transition: opacity 120ms var(--ease-out), transform 120ms var(--ease-out);
|
| 126 |
+
z-index: 2;
|
| 127 |
+
}
|
| 128 |
+
.span:hover .tip {
|
| 129 |
+
opacity: 1;
|
| 130 |
+
transform: translateX(-50%) translateY(0);
|
| 131 |
+
}
|
| 132 |
+
/* flip below when there is no room above (span near the panel top) */
|
| 133 |
+
.span.flip-below .tip {
|
| 134 |
+
bottom: auto; top: calc(100% + 7px);
|
| 135 |
+
transform: translateX(-50%) translateY(-2px);
|
| 136 |
+
}
|
| 137 |
+
.span.flip-below:hover .tip { transform: translateX(-50%) translateY(0); }
|
| 138 |
+
/* real inference: dim + blur the previous output while the next pass runs
|
| 139 |
+
(blur masks the seam between old results and the incoming scan) */
|
| 140 |
+
.output-body { transition: opacity 160ms var(--ease-out), filter 160ms var(--ease-out); }
|
| 141 |
+
.output-body.busy { opacity: 0.55; filter: blur(1.5px); }
|
| 142 |
+
/* scan line (v3): thin solid 2px line sweeping smoothly up and down with a
|
| 143 |
+
light trail behind it. Two full-height layers — one per direction — painted
|
| 144 |
+
with a short gradient ending in the line; motion is transform-only. Both
|
| 145 |
+
share one cycle: down sweep 0-50%, up sweep 50-100%, so the line turns at
|
| 146 |
+
the top and bottom like the reference. --scan is the scanner's own color
|
| 147 |
+
(FF5E00), independent of the brand --orange. */
|
| 148 |
+
.doc-cols { --scan: #FF5E00; }
|
| 149 |
+
.scan-line {
|
| 150 |
+
position: absolute; inset: 0 42px 0 0;
|
| 151 |
+
pointer-events: none; opacity: 0; z-index: 1;
|
| 152 |
+
background-repeat: no-repeat;
|
| 153 |
+
transition: opacity 200ms var(--ease-out);
|
| 154 |
+
}
|
| 155 |
+
/* downward sweep: line leads at the bottom, trail above it */
|
| 156 |
+
.scan-line.down {
|
| 157 |
+
background-image: linear-gradient(to bottom, transparent,
|
| 158 |
+
color-mix(in srgb, var(--scan) 5%, transparent) 55%,
|
| 159 |
+
color-mix(in srgb, var(--scan) 13%, transparent) calc(100% - 2px),
|
| 160 |
+
var(--scan) calc(100% - 2px));
|
| 161 |
+
background-size: 100% 180px; background-position: 0 100%;
|
| 162 |
+
}
|
| 163 |
+
/* upward sweep: line leads at the top, trail below it */
|
| 164 |
+
.scan-line.up {
|
| 165 |
+
background-image: linear-gradient(to top, transparent,
|
| 166 |
+
color-mix(in srgb, var(--scan) 5%, transparent) 55%,
|
| 167 |
+
color-mix(in srgb, var(--scan) 13%, transparent) calc(100% - 2px),
|
| 168 |
+
var(--scan) calc(100% - 2px));
|
| 169 |
+
background-size: 100% 180px; background-position: 0 0;
|
| 170 |
+
}
|
| 171 |
+
.doc-cols.busy .scan-line.down { opacity: 1; animation: scan-down 2.8s linear infinite; }
|
| 172 |
+
.doc-cols.busy .scan-line.up { opacity: 1; animation: scan-up 2.8s linear infinite; }
|
| 173 |
+
/* layers are doc-height, so translateY(±100%) is one full column. The two
|
| 174 |
+
keyframe sets hand over at 50% with a ~30ms crossfade at each turn. */
|
| 175 |
+
@keyframes scan-down {
|
| 176 |
+
0% { transform: translateY(-100%); opacity: 0; }
|
| 177 |
+
1%, 49% { opacity: 1; }
|
| 178 |
+
50% { transform: translateY(0); opacity: 0; }
|
| 179 |
+
100% { transform: translateY(0); opacity: 0; }
|
| 180 |
+
}
|
| 181 |
+
@keyframes scan-up {
|
| 182 |
+
0%, 50% { transform: translateY(100%); opacity: 0; }
|
| 183 |
+
51%, 99% { opacity: 1; }
|
| 184 |
+
100% { transform: translateY(0); opacity: 0; }
|
| 185 |
+
}
|
| 186 |
+
/* rail minimap: two glows track the line, same cycle (pseudos survive
|
| 187 |
+
renderRail's innerHTML rewrite) */
|
| 188 |
+
.rail::before, .rail::after {
|
| 189 |
+
content: ""; position: absolute; inset: 0;
|
| 190 |
+
background: linear-gradient(180deg, transparent 0, var(--scan) 30%,
|
| 191 |
+
var(--scan) 70%, transparent 100%);
|
| 192 |
+
background-size: 100% 22px; background-repeat: no-repeat;
|
| 193 |
+
opacity: 0; pointer-events: none;
|
| 194 |
+
transition: opacity 180ms var(--ease-out);
|
| 195 |
+
}
|
| 196 |
+
.rail::before { background-position: 0 0; }
|
| 197 |
+
.rail::after { background-position: 0 100%; }
|
| 198 |
+
.doc-cols.busy .rail::before { opacity: 0.85; animation: rail-down 2.8s linear infinite; }
|
| 199 |
+
.doc-cols.busy .rail::after { opacity: 0.85; animation: rail-up 2.8s linear infinite; }
|
| 200 |
+
@keyframes rail-down {
|
| 201 |
+
0% { transform: translateY(0); opacity: 0; }
|
| 202 |
+
1%, 49% { opacity: 0.85; }
|
| 203 |
+
50% { transform: translateY(calc(100% - 22px)); opacity: 0; }
|
| 204 |
+
100% { transform: translateY(calc(100% - 22px)); opacity: 0; }
|
| 205 |
+
}
|
| 206 |
+
@keyframes rail-up {
|
| 207 |
+
0%, 50% { transform: translateY(calc(100% - 22px)); opacity: 0; }
|
| 208 |
+
51%, 99% { opacity: 0.85; }
|
| 209 |
+
100% { transform: translateY(0); opacity: 0; }
|
| 210 |
+
}
|
| 211 |
+
@media (prefers-reduced-motion: reduce) {
|
| 212 |
+
/* drop the movement, keep the state feedback: dimmed output + glow parked
|
| 213 |
+
mid-rail. A static line across the doc would read as an artifact: hide. */
|
| 214 |
+
.doc-cols.busy .scan-line { animation: none; opacity: 0; }
|
| 215 |
+
.doc-cols.busy .rail::before { animation: none; opacity: 0; }
|
| 216 |
+
.doc-cols.busy .rail::after { animation: none; opacity: 0.85; transform: translateY(calc(50% - 11px)); }
|
| 217 |
+
}
|
| 218 |
+
/* mobile (placed after .scan-line: same specificity, source order must win):
|
| 219 |
+
no rail column, so the line spans the full doc width */
|
| 220 |
+
@media (max-width: 860px) {
|
| 221 |
+
.doc-cols .scan-line { inset: 0 0 0 0; }
|
| 222 |
+
}
|
| 223 |
+
/* legend + stats stack: chips wrap freely, stats always sit beneath them */
|
| 224 |
+
.controls { flex-direction: column; align-items: stretch; gap: 0; }
|
| 225 |
+
.controls[hidden] { display: none; } /* hidden attr loses to display:flex */
|
| 226 |
+
.legend { display: flex; flex-wrap: wrap; gap: 8px; }
|
| 227 |
+
/* compact chips + denser text: more room for the colored spans section */
|
| 228 |
+
.legend-chip { font-size: 11px; padding: 1px 8px; gap: 5px; }
|
| 229 |
+
.legend-swatch { width: 7px; height: 7px; }
|
| 230 |
+
.legend-count { font-family: var(--font-body); font-variant-numeric: tabular-nums; font-size: 9.5px; }
|
| 231 |
+
textarea#input { font-size: 13.5px; line-height: 1.7; }
|
| 232 |
+
.output-body { font-size: 13.5px; line-height: 1.7; }
|
| 233 |
+
.stats { font-size: 11px; }
|
| 234 |
+
/* footnote code (model id, chunk params) in the kit mono, not browser default */
|
| 235 |
+
.footnote code { font-family: var(--font-mono); }
|
| 236 |
+
.stats { margin-left: 0; margin-top: 10px; }
|
| 237 |
+
/* model strip: mascot + linked model name/size, between examples and workspace */ .model-strip {
|
| 238 |
+
display: flex; align-items: center; gap: 10px;
|
| 239 |
+
background: var(--card); border: 1px solid var(--ash); border-radius: var(--radius);
|
| 240 |
+
padding: 7px 14px; margin: 0 0 18px;
|
| 241 |
+
}
|
| 242 |
+
.model-strip img { width: 26px; height: 26px; display: block; }
|
| 243 |
+
.model-strip-name {
|
| 244 |
+
font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--ink);
|
| 245 |
+
text-decoration: none; border-bottom: 1px solid var(--ash); padding-bottom: 1px;
|
| 246 |
+
transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
|
| 247 |
+
}
|
| 248 |
+
.model-strip-name:hover { color: var(--orange-deep); border-bottom-color: var(--orange-deep); }
|
| 249 |
+
.model-strip-name:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
|
| 250 |
+
.model-strip-size { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); }
|
| 251 |
+
/* code/counts buttons: bunched together, were 55px apart under space-between */
|
| 252 |
+
.header-actions { display: flex; align-items: center; gap: 8px; }
|
| 253 |
+
/* counts toggle lit state when open */
|
| 254 |
+
/* counts toggle lit state when open */
|
| 255 |
+
</style>
|
| 256 |
</head>
|
| 257 |
<body>
|
| 258 |
<div class="wrap">
|
|
|
|
| 261 |
<h1 id="title"></h1>
|
| 262 |
<p class="sub" id="subtitle"></p>
|
| 263 |
</div>
|
|
|
|
| 264 |
</header>
|
| 265 |
|
| 266 |
<main>
|
|
|
|
| 269 |
<div class="example-list" id="examples"></div>
|
| 270 |
</section>
|
| 271 |
|
| 272 |
+
<section class="model-strip" aria-label="Model">
|
| 273 |
+
<img src="assets/fox-laptop.png" alt="" width="26" height="26">
|
| 274 |
+
<a class="model-strip-name" href="https://huggingface.co/fastino/gliner2.5-multi-v1" target="_blank" rel="noopener">GLiNER2.5-Multi-v1</a>
|
| 275 |
+
<span class="model-strip-size">(0.3B)</span>
|
| 276 |
+
</section>
|
| 277 |
+
|
| 278 |
<div class="workspace">
|
| 279 |
<section class="card">
|
| 280 |
<div class="panel-header"><div><h2>Text</h2><p>Edit below, or pick an example.</p></div></div>
|
|
|
|
| 283 |
|
| 284 |
<section class="card">
|
| 285 |
<div class="panel-header">
|
| 286 |
+
<div><h2>Spans and attributes</h2><p>Click a span to inspect its attribute groups. Each span carries its exact position along with a confidence score.</p></div>
|
| 287 |
+
<div class="header-actions">
|
| 288 |
+
<button type="button" class="code-toggle" id="code-toggle" aria-expanded="false" aria-label="Code">code</button>
|
| 289 |
+
</div>
|
| 290 |
</div>
|
| 291 |
<div class="code-cell" id="code-cell" hidden>
|
| 292 |
+
<div class="code-cell-bar" role="tablist" aria-label="Code and result">
|
| 293 |
+
<button type="button" class="code-tab active" id="tab-code" role="tab" aria-selected="true">code</button>
|
| 294 |
+
<button type="button" class="code-tab" id="tab-result" role="tab" aria-selected="false">result</button>
|
| 295 |
+
<button type="button" class="copy-btn" id="copy-btn" aria-label="Copy">
|
| 296 |
<span class="copy-label">Copy</span>
|
| 297 |
<span class="copy-done" hidden>Copied</span>
|
| 298 |
</button>
|
| 299 |
</div>
|
| 300 |
<pre class="code-panel" id="code-panel"></pre>
|
| 301 |
+
<pre class="code-panel cell-result" id="cell-result" hidden></pre>
|
| 302 |
</div>
|
| 303 |
+
<div class="doc-cols">
|
| 304 |
+
<div id="output" class="output-body"></div>
|
| 305 |
+
<div class="inspector" id="inspector" hidden></div>
|
| 306 |
+
<div class="rail" id="rail" aria-hidden="true"></div>
|
| 307 |
+
<div class="scan-line down" aria-hidden="true"></div>
|
| 308 |
+
<div class="scan-line up" aria-hidden="true"></div>
|
| 309 |
+
</div>
|
| 310 |
+
<div class="controls" id="controls">
|
| 311 |
+
<div class="legend" id="legend"></div>
|
| 312 |
<div class="stats">
|
| 313 |
+
<span title="End-to-end time for one pass over the document.">Latency<strong id="stat-ms">-</strong></span>
|
| 314 |
<span>Spans<strong id="stat-n">-</strong></span>
|
| 315 |
+
<span>Chars<strong id="stat-chars">-</strong></span>
|
| 316 |
+
<span title="Number of inference passes over the document.">Chunks<strong id="stat-chunks">-</strong></span>
|
| 317 |
</div>
|
| 318 |
</div>
|
| 319 |
</section>
|
| 320 |
</div>
|
| 321 |
|
| 322 |
+
<p class="footnote" style="margin-top: 28px;">
|
| 323 |
+
Extraction runs <a href="https://huggingface.co/fastino/gliner2.5-multi-v1" target="_blank" rel="noopener"><code>fastino/gliner2.5-multi-v1</code></a> live on CPU. Attribute groups are scoped with <code>applies_to</code>: sentiment on products,
|
| 324 |
role on people, assertion on conditions. Built by
|
| 325 |
<a class="labs-mark" href="https://fastino.ai" target="_blank" rel="noopener">Fastino Labs</a>.
|
| 326 |
</p>
|
| 327 |
+
|
| 328 |
+
<nav class="footer-links" aria-label="GLiNER 2.5 links">
|
| 329 |
+
<a href="https://huggingface.co/collections/fastino/gliner25-models" target="_blank" rel="noopener">GLiNER2.5 models</a>
|
| 330 |
+
<img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
|
| 331 |
+
<a href="https://huggingface.co/collections/fastino/gliner25-demos" target="_blank" rel="noopener">GLiNER2.5 demos</a>
|
| 332 |
+
<img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
|
| 333 |
+
<a href="https://github.com/fastino-ai/GLiNER2" target="_blank" rel="noopener">GitHub</a>
|
| 334 |
+
<img src="assets/transparent-dark-64.png" alt="" width="16" height="16">
|
| 335 |
+
<a href="https://fastino.ai/blog/gliner2-5-span-free-information-extraction" target="_blank" rel="noopener">Release blog</a>
|
| 336 |
+
</nav>
|
| 337 |
</main>
|
| 338 |
</div>
|
| 339 |
|
|
|
|
| 342 |
const $ = (id) => document.getElementById(id);
|
| 343 |
const esc = (v) => v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 344 |
let READY = false, timer = null, version = 0, labels = [], lastSpans = [];
|
| 345 |
+
let REAL = false; // live model (vs instant mock): longer debounce + pending state
|
| 346 |
+
|
| 347 |
+
function spanColor(label) { return `var(--c${labels.indexOf(label) % 8})`; }
|
| 348 |
+
|
| 349 |
|
| 350 |
/* Badge tone. Semantic hues for sentiment/assertion/impact; everything else
|
| 351 |
falls back to the span's own label color at low alpha. All pairs AA. */
|
|
|
|
| 374 |
return html;
|
| 375 |
}
|
| 376 |
|
|
|
|
|
|
|
| 377 |
function render(text, spans) {
|
| 378 |
let html = "", prev = 0, i = 0;
|
| 379 |
for (const sp of spans) {
|
| 380 |
html += esc(text.slice(prev, sp.start));
|
| 381 |
const c = spanColor(sp.label);
|
| 382 |
+
// frosted hover pill: type | score | chars
|
| 383 |
+
const tip = `<span class="tip"><span class="t-type">${esc(sp.label)}</span><span class="t-sep">|</span>${sp.score.toFixed(2)}<span class="t-sep">|</span><span class="t-off">chars ${sp.start} to ${sp.end}</span></span>`;
|
| 384 |
+
html += `<span class="span-wrap"><span class="span" data-idx="${i}" style="--span-color:${c};--i:${i}" data-start="${sp.start}" data-end="${sp.end}" data-score="${sp.score}">${esc(sp.text)}<span class="span-label" style="--i:${i}">${esc(sp.label)}</span>${tip}</span>${attrBadges(sp.attributes, i, c)}</span>`;
|
| 385 |
prev = sp.end; i++;
|
| 386 |
}
|
| 387 |
return html + esc(text.slice(prev));
|
| 388 |
}
|
| 389 |
|
| 390 |
+
function renderRail(text, spans) {
|
| 391 |
+
// Minimap: ticks land at char offset / doc length down the fixed-height rail.
|
| 392 |
+
$("rail").innerHTML = spans.map((sp, i) =>
|
| 393 |
+
`<span class="rail-tick" style="top:${(sp.start / text.length * 100).toFixed(1)}%;--span-color:${spanColor(sp.label)};--i:${i}"></span>`
|
| 394 |
+
).join("");
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
function renderLegend(spans) {
|
| 398 |
const counts = {};
|
| 399 |
for (const sp of spans) counts[sp.label] = (counts[sp.label] || 0) + 1;
|
|
|
|
| 403 |
</span>`).join("");
|
| 404 |
}
|
| 405 |
|
| 406 |
+
function renderStatChunks(data) {
|
| 407 |
+
$("stat-chunks").textContent = data.chunks ? data.chunks.n_chunks : "-";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
}
|
| 409 |
|
| 410 |
function setEmpty(state) {
|
| 411 |
const html = {
|
| 412 |
+
idle: "",
|
| 413 |
+
none: REAL
|
| 414 |
+
? '<div class="output-empty"><strong>No spans found.</strong>The model read the whole document and found no matches. Try different text or switch examples.</div>'
|
| 415 |
+
: '<div class="output-empty"><strong>No spans found.</strong>Free-typed text returns no result in mock mode. Click an example to see extractions.</div>',
|
| 416 |
+
error: '<div class="output-empty"><strong>Inference failed.</strong>The server did not respond. Check that the model is still running and try again.</div>',
|
| 417 |
}[state];
|
| 418 |
$("output").innerHTML = html;
|
| 419 |
}
|
| 420 |
|
| 421 |
+
function setPending(on) {
|
| 422 |
+
$("output").classList.toggle("busy", on);
|
| 423 |
+
document.querySelector(".doc-cols").classList.toggle("busy", on);
|
| 424 |
+
$("output").setAttribute("aria-busy", on ? "true" : "false");
|
| 425 |
+
if (on) $("stat-ms").textContent = "\u2026";
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
async function infer() {
|
| 429 |
if (!READY) return;
|
| 430 |
const v = ++version, text = $("input").value;
|
| 431 |
+
if (!text.trim()) {
|
| 432 |
+
setPending(false);
|
| 433 |
+
setEmpty("idle"); renderLegend([]); renderRail(" ", []);
|
| 434 |
+
$("stat-ms").textContent = "-"; $("stat-n").textContent = "-";
|
| 435 |
+
$("stat-chars").textContent = "-"; $("stat-chunks").textContent = "-";
|
| 436 |
+
return;
|
| 437 |
+
}
|
| 438 |
+
setPending(true);
|
| 439 |
let data;
|
| 440 |
try {
|
| 441 |
const r = await fetch("api/infer", { method: "POST",
|
| 442 |
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
|
| 443 |
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
| 444 |
data = await r.json();
|
| 445 |
+
} catch (e) {
|
| 446 |
+
if (v === version) { setPending(false); setEmpty("error"); $("stat-ms").textContent = "-"; }
|
| 447 |
+
return;
|
| 448 |
+
}
|
| 449 |
+
if (v !== version) return; // a newer pass is in flight; it owns the pending state
|
| 450 |
+
setPending(false);
|
| 451 |
const spans = data.spans || [];
|
| 452 |
lastSpans = spans;
|
| 453 |
labels = [...new Set([...labels, ...spans.map((s) => s.label)])];
|
| 454 |
$("output").innerHTML = spans.length ? render(text, spans) : "";
|
| 455 |
if (!spans.length) setEmpty("none");
|
| 456 |
+
renderRail(text, spans);
|
| 457 |
renderLegend(spans);
|
|
|
|
| 458 |
$("stat-ms").textContent = `${data.ms} ms`;
|
| 459 |
$("stat-n").textContent = spans.length;
|
| 460 |
+
$("stat-chars").textContent = text.length.toLocaleString();
|
| 461 |
+
renderStatChunks(data);
|
| 462 |
}
|
| 463 |
|
| 464 |
+
function inferSoon(delay) { clearTimeout(timer); timer = setTimeout(infer, delay ?? (REAL ? 700 : 300)); }
|
| 465 |
$("input").addEventListener("input", () => inferSoon());
|
| 466 |
+
|
| 467 |
+
/* flip the tooltip below when the span has no room above (live: the output
|
| 468 |
+
panel scrolls, so a one-shot class at render time would go stale) */
|
| 469 |
+
$("output").addEventListener("mouseover", (e) => {
|
| 470 |
+
const sp = e.target.closest(".span");
|
| 471 |
+
if (!sp) return;
|
| 472 |
+
const outTop = $("output").getBoundingClientRect().top;
|
| 473 |
+
sp.classList.toggle("flip-below", sp.getBoundingClientRect().top - outTop < 34);
|
| 474 |
+
});
|
| 475 |
+
|
| 476 |
+
/* code view replaces the annotated output; toggle swaps between them */
|
| 477 |
+
function setCodeView(open) {
|
| 478 |
+
$("code-cell").hidden = !open;
|
| 479 |
+
document.querySelector(".doc-cols").hidden = open;
|
| 480 |
+
$("controls").hidden = open;
|
| 481 |
+
const btn = $("code-toggle");
|
| 482 |
+
btn.textContent = open ? "close" : "code";
|
| 483 |
+
btn.setAttribute("aria-expanded", open);
|
| 484 |
+
btn.setAttribute("aria-label", open ? "Close code" : "Code");
|
| 485 |
+
}
|
| 486 |
+
$("code-toggle").addEventListener("click", () => setCodeView($("code-cell").hidden));
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
function renderResult(resultText) {
|
| 490 |
+
// light JSON tinting: keys, strings, numbers, trailing comments
|
| 491 |
+
let html = esc(resultText)
|
| 492 |
+
.replace(/("(?:[^"\\]|\\.)*?")(\s*:)/g, '<span class="jk">$1</span>$2')
|
| 493 |
+
.replace(/: ("(?:[^"\\]|\\.)*?")/g, ': <span class="js">$1</span>')
|
| 494 |
+
.replace(/(:\s)(\d+\.?\d*)/g, '$1<span class="jn">$2</span>')
|
| 495 |
+
.replace(/(#[^\n]*)/g, '<span class="jc">$1</span>');
|
| 496 |
+
$("cell-result").innerHTML = html;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
let CODE_TAB = "code";
|
| 500 |
+
function setCodeTab(tab) {
|
| 501 |
+
CODE_TAB = tab;
|
| 502 |
+
const isCode = tab === "code";
|
| 503 |
+
$("tab-code").classList.toggle("active", isCode);
|
| 504 |
+
$("tab-result").classList.toggle("active", !isCode);
|
| 505 |
+
$("tab-code").setAttribute("aria-selected", isCode);
|
| 506 |
+
$("tab-result").setAttribute("aria-selected", !isCode);
|
| 507 |
+
$("code-panel").hidden = !isCode;
|
| 508 |
+
$("cell-result").hidden = isCode;
|
| 509 |
+
}
|
| 510 |
+
$("tab-code").addEventListener("click", () => setCodeTab("code"));
|
| 511 |
+
$("tab-result").addEventListener("click", () => setCodeTab("result"));
|
| 512 |
$("copy-btn").addEventListener("click", async () => {
|
| 513 |
+
const text = CODE_TAB === "code" ? $("code-panel").textContent : $("cell-result").textContent;
|
| 514 |
+
try { await navigator.clipboard.writeText(text); } catch (e) { return; }
|
| 515 |
$("copy-btn").querySelector(".copy-label").hidden = true;
|
| 516 |
$("copy-btn").querySelector(".copy-done").hidden = false;
|
| 517 |
setTimeout(() => {
|
|
|
|
| 519 |
$("copy-btn").querySelector(".copy-done").hidden = true;
|
| 520 |
}, 1400);
|
| 521 |
});
|
| 522 |
+
|
| 523 |
+
|
| 524 |
+
function renderInspector(idx) {
|
| 525 |
+
const box = $("inspector");
|
| 526 |
+
if (idx == null || !lastSpans[idx]) { box.hidden = true; box.innerHTML = ""; return; }
|
| 527 |
+
const sp = lastSpans[idx];
|
| 528 |
+
const c = spanColor(sp.label);
|
| 529 |
+
const groups = Object.entries(sp.attributes || {});
|
| 530 |
+
box.innerHTML = `
|
| 531 |
+
<div class="inspector-head">
|
| 532 |
+
<span class="span-text">“${esc(sp.text)}”</span>
|
| 533 |
+
<span class="span-label" style="--span-color:${c}">${esc(sp.label)}</span>
|
| 534 |
+
<span class="span-score">span score ${sp.score.toFixed(2)} · chars ${sp.start}–${sp.end}</span>
|
| 535 |
+
</div>
|
| 536 |
+
${groups.length ? `<div class="inspector-groups">${groups.map(([group, entries]) => `
|
| 537 |
+
<div class="attr-group">
|
| 538 |
+
<div class="attr-group-name">${esc(group)}</div>
|
| 539 |
+
${entries.map((e) => `
|
| 540 |
+
<div class="attr-row">
|
| 541 |
+
<span class="attr-label">${esc(e.label)}</span>
|
| 542 |
+
<span class="attr-score">${e.score.toFixed(2)}</span>
|
| 543 |
+
</div>
|
| 544 |
+
<div class="attr-meter"><i style="width:${(e.score * 100).toFixed(0)}%;--span-color:${c}"></i></div>`).join("")}
|
| 545 |
+
</div>`).join("")}</div>`
|
| 546 |
+
: `<div class="output-empty">No attributes on this span.</div>`}`;
|
| 547 |
+
box.hidden = false;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
$("output").addEventListener("click", (e) => {
|
| 551 |
const el = e.target.closest(".span");
|
| 552 |
if (!el) return;
|
|
|
|
| 560 |
|
| 561 |
(async () => {
|
| 562 |
const cfg = await fetchConfig();
|
| 563 |
+
REAL = cfg.mock === false;
|
| 564 |
document.title = `${cfg.title.replace(/<[^>]+>/g, "")} - GLiNER 2.5`;
|
| 565 |
$("title").innerHTML = cfg.title;
|
| 566 |
$("subtitle").innerHTML = cfg.subtitle;
|
|
|
|
| 567 |
|
| 568 |
cfg.examples.forEach((ex, i) => {
|
| 569 |
const b = document.createElement("button");
|
|
|
|
| 572 |
b.addEventListener("click", () => {
|
| 573 |
document.querySelectorAll(".ex").forEach((x) => x.classList.toggle("active", x === b));
|
| 574 |
$("input").value = ex.text;
|
| 575 |
+
labels = []; // per-example schema: reset color mapping
|
| 576 |
+
// old example's spans no longer match the new document: clear before scan
|
| 577 |
+
$("output").innerHTML = ""; renderLegend([]); renderRail(" ", []);
|
| 578 |
+
$("code-panel").textContent = ex.code || "";
|
| 579 |
+
renderResult(ex.result || "");
|
| 580 |
inferSoon(40);
|
| 581 |
});
|
| 582 |
$("examples").append(b);
|
| 583 |
});
|
| 584 |
|
| 585 |
setEmpty("idle");
|
| 586 |
+
if (cfg.examples[0]) {
|
| 587 |
+
$("input").value = cfg.examples[0].text;
|
| 588 |
+
$("code-panel").textContent = cfg.examples[0].code || "";
|
| 589 |
+
renderResult(cfg.examples[0].result || "");
|
| 590 |
+
}
|
| 591 |
READY = await waitForServer();
|
| 592 |
if (READY) infer();
|
| 593 |
else setEmpty("error");
|