| --- |
| title: LREC 2026 LLM-as-Annotator |
| emoji: ✒️ |
| colorFrom: indigo |
| colorTo: purple |
| sdk: docker |
| app_port: 7860 |
| pinned: false |
| license: mit |
| short_description: Annotate historical and low-resource languages with LLMs |
| --- |
| |
| # LREC 2026 — LLM-as-Annotator Workbench |
|
|
| A **corpus-centered** annotation app built around the LLM-as-annotator pipeline |
| described in the LREC 2026 tutorial and the companion LoResLM 2026 paper. The |
| text is the focal point; everything else (task schema, models, prompt, ICL |
| pool, exports) lives in popups behind toolbar pills. |
|
|
| ## What it does |
|
|
| - Loads a corpus (paste, file, or sandbox example from the four historical languages of the paper). |
| - Annotates **token by token** with one or more LLMs (single inference or Mixture-of-Experts). |
| - Highlights MoE **disagreements** so the reviewer focuses on contested tokens first. |
| - Lets you correct any token in a focused popup with per-model votes, keyboard navigation, bulk operations, and a "re-ask one model" action. |
| - Bootstrap loop: corrected sentences feed back into the few-shot pool (filtered by `(language, schema_hash)` to avoid task contamination). |
| - Exports as TSV (PIE-baseline round-trip), JSON (schema-conformant), CoNLL-U (UD standard), or JSONL (fine-tune format). |
|
|
| ## Companion paper |
|
|
| Vidal-Gorène, C., Kindt, B., & Cafiero, F. (2026). *Under-resourced studies of |
| under-resourced languages: lemmatization and POS-tagging with LLM annotators |
| for historical Armenian, Georgian, Greek and Syriac.* **LoResLM 2026**. |
| [https://aclanthology.org/2026.loreslm-1.28/](https://aclanthology.org/2026.loreslm-1.28/) |
|
|
| Tutorial repo: [floriancafiero/lrec2026-llm-as-annotator-tutorial](https://github.com/floriancafiero/lrec2026-llm-as-annotator-tutorial) |
|
|
| ## Stack |
|
|
| - **Backend**: FastAPI + httpx (async OpenRouter client). |
| - **Frontend**: single static HTML page + Alpine.js (15 KB, CDN) + Tailwind CSS (CDN). No build step. |
|
|
| ## Run locally |
|
|
| ```bash |
| cd app |
| pip install -r requirements.txt |
| python app.py # or: uvicorn app:app --reload --port 7860 |
| # open http://127.0.0.1:7860 |
| ``` |
|
|
| The app expects the two sibling repos at: |
|
|
| ``` |
| LREC-tutorial/ |
| ├── code/ |
| │ ├── EACL2026-historical-languages/ # sandbox corpora + tagsets |
| │ └── lrec2026-llm-as-annotator-tutorial/ # JSON schema + system prompts |
| └── app/ # this directory |
| ``` |
|
|
| ## Workflow |
|
|
| 1. **Sidebar → quick start** — click an example corpus (Ancient Greek, Old Armenian, Syriac). The toolbar updates the task, language, and models. |
| 2. **Top bar → 🔑 OpenRouter** — paste your API key (kept in this browser session only). |
| 3. **Top bar → ▶ Annotate all** — runs every model in parallel (Mixture-of-Experts if 2+ models). Tokens are colored by status: indigo = consensus, amber ⚠ = disagreement. |
| 4. **Click any token** → popup with editable fields, per-model votes, keyboard navigation, "adopt from <model>" and "re-ask one model" shortcuts. |
| 5. **📥 to ICL** on a sentence — pushes the corrected annotation into the few-shot pool. The next run re-injects it. |
| 6. **Top bar → export** — TSV / JSON / CoNLL-U / JSONL. |
|
|
| ### Keyboard shortcuts |
|
|
| | Key | Action | |
| |---|---| |
| | `j` / `k` | next / previous token | |
| | `e` or `↵` | edit focused token | |
| | `1`–`9` | (in editor) assign the i-th visible tag | |
| | `x` | toggle selection of focused token | |
| | `r` | re-annotate the focused sentence | |
| | `↵` | save edit & advance to next disagreement | |
| | `Esc` | close popup / clear selection | |
| | `shift+click` | multi-select tokens (then "Apply tag…") | |
| | `right-click` | per-token context menu | |
|
|
| ## Deploy on HuggingFace Spaces |
|
|
| This `app/` directory is **self-contained**: the tagsets, schemas, system |
| prompts, cheatsheet and a slice of the four sandbox corpora are vendored under |
| [data/](data/) (≈ 900 KB). You do not need to push the parent repo or use git |
| submodules. |
|
|
| ### One-shot deploy |
|
|
| ```bash |
| cd app |
| # Create a new Space (Docker SDK) at https://huggingface.co/new-space |
| # Then push this directory as the Space's root: |
| git init && git add . && git commit -m "init" |
| git remote add space https://huggingface.co/spaces/<your-user>/<space-name> |
| git push --force space main |
| ``` |
|
|
| The Space builds from `Dockerfile`, boots `uvicorn` on port 7860, and serves |
| the SPA at `/`. |
|
|
| ### ⚠ Single-user demo |
|
|
| `SESSION` is module-global. **The Space serves one user at a time** — if two |
| people open it simultaneously, they share the same corpus, the same selected |
| models, and (briefly) the same API key. For the LREC tutorial we recommend: |
|
|
| > 🦆 **Each attendee clicks the "⋮ → Duplicate this Space" button** in the |
| > top-right of the Space page. They get a free private clone, isolated state, |
| > their own API key in their own browser. |
|
|
| This is the simplest way to fan out the tutorial. Document this prominently on |
| the Space's README. |
|
|
| ### Optional: ship a default OpenRouter key |
|
|
| If you want attendees to start without entering a key (e.g., a shared demo |
| key with a rate limit), set a Space **Secret** named `OPENROUTER_API_KEY`. |
| The backend reads it at startup; users can still override it from the UI. |
|
|
| API keys entered through the UI are **never persisted** — they live only in |
| the in-memory `SESSION` dict and are forgotten on restart. |
|
|
| ## File map |
|
|
| | File | Role | |
| |---|---| |
| | [app.py](app.py) | FastAPI app: state + REST endpoints | |
| | [static/index.html](static/index.html) | SPA layout: toolbar, sidebar, corpus panel, modals | |
| | [static/app.js](static/app.js) | Alpine.js state + handlers + keyboard shortcuts | |
| | [static/styles.css](static/styles.css) | Token chips, modals, polish | |
| | [provider.py](provider.py) | OpenRouter async client (JSON-Schema response_format + retry) | |
| | [moe.py](moe.py) | Pure `aggregate()` — vote / LCS / min / priority | |
| | [schemas.py](schemas.py) | `AnnotationSchema` + 8 presets | |
| | [prompts.py](prompts.py) | Templates from tutorial repo + `ICLPool` | |
| | [io_utils.py](io_utils.py) | Tokenizer + TSV / JSON / CoNLL-U / JSONL I/O | |
| | [tutorial.py](tutorial.py) | 3 guided examples prefilling the corpus | |
| | [paths.py](paths.py) | Resolves sibling repos (read-only) | |
| |
| ## License |
| |
| MIT for this app code. Sandbox data and prompt templates remain under their |
| upstream licenses (see the two `code/` repositories). |