File size: 4,034 Bytes
56a8677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
---
license: mit
language:
- en
- es
- fr
- de
library_name: onnx
tags:
- time
- temporal-parsing
- date-parsing
- nlp
- sql
- multilingual
- onnx
- sundial
---

# sundial-encoder-full

**Time expressions (en / es / fr / de) → a portable IR → SQL / JS date ranges.** A tiny (~14 MB)
multi-head classifier that runs in the browser or on the server in a single forward pass.

- 💻 **Code (MIT):** https://github.com/ceritium/sundial
- 🕹️ **Live demo:** https://sundial.onvibe.run/playground
- 📊 **Benchmarks:** https://github.com/ceritium/sundial/blob/master/BENCHMARKS.md

## What it does

Given a phrase like `"last 7 days"`, `"último miércoles de 2027"`, `"first day of the month"` or
`"a fortnight ago"`, the model emits an **IR skeleton** — the *structure* only, no digits
(`last_n(N, day)`, `nth_weekday(last, wed, YEAR)`, `nth_day(1, month, this)`, `offset(-N, week)`, …).
A small **deterministic layer** (in the repo) then extracts the exact numbers, months and dates from
the input and materializes the full IR, which renders to a bound **SQLite / Postgres predicate** or a
JS `{start, end}` range.

> The model never generates a digit. Because the output space is a **finite set of skeletons**, this
> is **classification, not generation** — so a 14 M-parameter encoder with a few heads is enough, and
> it generalizes to years and phrasings it barely saw.

## Model

- Base: `google/electra-small-discriminator` (~14 M params), fine-tuned as a **multi-head classifier**
  with one head per IR slot: `op / unit / which / dow / ord / scope / sign / has_at`.
- Format: **int8 ONNX**, ~14 MB. Runtime: `onnxruntime` (WASM in the browser, single pass, ~1 ms/inf CPU).
- Trained on `en + es + fr + de`, basic + complex expressions, plus **LLM-distilled paraphrases** for
  phrasing robustness.

**Files:** `model_int8.onnx`, the tokenizer, `heads.json` (the per-head label vocabulary).

## Highlights

- Matches a T5 seq2seq (98.6% gold) at **14 MB** and ~30× faster per inference.
- One 14 MB English-vocab base handles **en/es/fr/de** at ~99% (a 135 MB multilingual base adds ~1%).
- vs rule engines: a *tie* with Duckling on the constructs it supports, but wins on **out-of-distribution
  phrasing** (91.7% vs 38.9% on paraphrases) — the distilled model generalizes the long tail.

## Usage

The model outputs the 8 head logits; you `argmax` them, compose the skeleton, then run the deterministic
`materialize` + renderers from the repo. The repo ships a ready-to-run script:

```bash
git clone https://github.com/ceritium/sundial && cd sundial
uv run --project train python train/predict_onnx.py \
    --hf ceritium/sundial-encoder-full \
    --inputs data/gold/gold_complex_en.jsonl --out models/pred.jsonl
node eval/run_eval.mjs --gold data/gold/gold_complex_en.jsonl --predictions models/pred.jsonl --skeleton
# -> 26 examples · 100.0%  (skeleton -> materialize -> execute vs gold)
```

Minimal inference (Python + onnxruntime):

```python
import json, numpy as np, onnxruntime as ort
from huggingface_hub import hf_hub_download
from transformers import AutoTokenizer

repo = "ceritium/sundial-encoder-full"
tok = AutoTokenizer.from_pretrained(repo)
heads = json.load(open(hf_hub_download(repo, "heads.json")))
sess = ort.InferenceSession(hf_hub_download(repo, "model_int8.onnx"))

enc = tok("last wednesday of 2027".lower(), return_tensors="np", truncation=True, max_length=32)
logits = sess.run(heads["head_names"], {"input_ids": enc["input_ids"].astype(np.int64),
                                        "attention_mask": enc["attention_mask"].astype(np.int64)})
slots = {n: heads["heads"][n][int(np.argmax(logits[i][0]))] for i, n in enumerate(heads["head_names"])}
print(slots)  # -> {op: nth_weekday, ord: last, dow: wed, scope: year, ...}  (compose + materialize in the repo)
```

## License

MIT. Experimental research model — see the [repo](https://github.com/ceritium/sundial) for the full IR
grammar, renderers (validated by execution), training/distillation pipeline, and benchmarks.