Skip to content

About

Python NiceGui data annotation tool.

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

spanselector

CI License: MIT Python 3.11–3.14

A small, Python-native workbench for turning unstructured text into reviewable span annotations.

Alpha: spanselector is a local, single-user prototype. Drafts live in process memory until you export them. It is not yet a hosted labeling platform or a persistence layer.

Why this exists

Human-in-the-loop experiments often need a focused review surface long before they need a full annotation platform. spanselector keeps that first loop deliberately small:

  1. Load text records from a common local format.
  2. Define labels in a readable YAML schema.
  3. Select a span and assign a label.
  4. Review annotations in context.
  5. Export the labeled dataset as JSONL.

The useful part to showcase is the seam between Python data models and a human review workflow, not infrastructure around accounts, queues, consensus, or model training.

Features

  • CSV, TSV, JSONL, NDJSON, and plain-text input
  • Schema-driven labels with validated, deterministic colors
  • Read-only schema preview with label colors and number-key mappings
  • Best-effort and strict annotation policies with explicit event outcomes
  • Offset-based span annotations with overlap protection
  • Automatically retained in-session drafts and multiple-event navigation
  • UTF-8 JSONL export containing source text, metadata, schema, and annotations
  • Environment-based configuration through Pydantic Settings
  • A responsive NiceGUI interface with reduced-motion support
  • Contextual label actions with focus-scoped keyboard shortcuts and Undo

Quick start

The project uses uv, but a regular editable install also works.

git clone https://github.com/wiperpaul/span-annotator.git
cd span-annotator
uv sync --extra dev
uv run spanselector

Open http://localhost:8080. To use pip instead:

python -m venv .venv
python -m pip install -e ".[dev]"
spanselector

Docker

docker compose up --build

The container runs as a non-root user and exposes the app on port 8080.

Configure the dataset

Copy .env.example to .env, then point DATA_PATH at a local input file. CSV, TSV, JSONL, NDJSON, and TXT are supported. The legacy SAMPLE_CSV name is still accepted.

SCHEMA_NAME=Customer Support Ticket
SCHEMA_DIR=src/spanselector/schemas
DATA_PATH=src/spanselector/data/sample.csv
HOST=127.0.0.1
PORT=8080

Structured files need a text column named text, message, log, content, body, raw, event, or payload. IDs are read from event_id, eventid, or id; otherwise stable row-based IDs are generated. Duplicate IDs are rejected because they would make annotation ownership ambiguous.

Define a schema

Place YAML files in SCHEMA_DIR:

name: Customer Support Ticket
annotation_mode: strict
fields:
  - CustomerName
  - Product
  - IssueSummary
  - Priority
required_fields:
  - CustomerName
  - IssueSummary
colors:
  Priority: "#f59e0b"

annotation_mode can be best_effort or strict and defaults to best_effort. Best-effort schemas cannot declare required fields; their unlabelled fields remain not_reviewed. Strict schemas must declare at least one required field, and every required field must also appear in fields. Colors are optional and assigned deterministically when missing. Field names must be non-empty and unique, and colors must be six-digit hex values.

The bundled Customer Support schema demonstrates strict review; Project Meeting Notes demonstrates best-effort annotation.

Label spans

Drag across the source text to open the contextual label tray, then click a label to apply it. For schemas with up to nine labels, press 1–9 while the source text has focus to apply the matching visible label directly. Escape clears a pending selection and Ctrl+Z (or Cmd+Z) undoes the most recent assignment on the current event. Draft changes are retained automatically in process memory.

In best-effort mode, Done & next accepts the useful spans collected so far without interpreting unlabelled fields as negative evidence. In strict mode, each required field needs an explicit conclusion: either assign at least one matching span or Confirm absent. Complete & next validates those conclusions. Ctrl+Enter (or Cmd+Enter) performs the mode's primary action. Skip for now defers the draft until the remaining events have been reviewed. Unable to label records a terminal outcome and reason for unusable or inconclusive source text.

The compact helper at the bottom of the page opens a complete shortcut reference. Workspace shortcuts can be disabled there; label number keys are focus-scoped and shortcuts are ignored in editable controls and dialogs. Native text navigation always wins in the source: combinations such as Ctrl+Left/Right and Ctrl+Shift+Left/Right continue to move or extend the selection by word.

Export format

Export JSONL includes every input event, including drafts, deferred events, and unlabelable events. Each record includes the annotation mode, outcome, and a three-valued conclusion for every schema field: labelled, confirmed_absent, or not_reviewed. Offsets use Python slice semantics: start is inclusive and end is exclusive.

{"id":"ticket-17","text":"Alex cannot sign in","metadata":{"source":"email"},"schema":"Customer Support Ticket","annotation_mode":"strict","status":"completed","deferred":false,"field_conclusions":{"CustomerName":"labelled","Product":"not_reviewed","IssueSummary":"confirmed_absent","Priority":"not_reviewed"},"annotations":[{"start":0,"end":4,"text":"Alex","field":"CustomerName"}]}

Including the selected text alongside offsets makes downstream integrity checks straightforward.

Project boundary

Included now: a trustworthy local annotation loop, clear import/export contracts, and extension points for schemas and data sources.

Intentionally deferred: authentication, multi-user isolation, server-side persistence, annotation queues, reviewer consensus, model inference, active learning, and deployment hardening. Those are valuable only after the local workflow proves useful.

Development

uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy src/spanselector
uv build

Selection is also covered by a real pointer-drag test in Chromium and Firefox. Browser binaries are an explicit extra so the regular unit-test setup stays lightweight:

uv sync --extra dev --extra e2e
uv run playwright install chromium firefox
uv run pytest tests/e2e --run-e2e --browser chromium --browser firefox --no-cov

Warning

Text selection is currently unreliable in VS Code's Integrated Browser even though the same app works in external Firefox and Chromium. Treat the integrated viewer as unsupported for annotation testing for now.

For a manual pass in VS Code, choose NiceGUI: Python + External Browser in Run and Debug and press F5. The launch configuration opens the app in your system browser. Drag across the source text and confirm that both the range and selected text update; repeat with Ctrl+A to cover keyboard selection.

See CONTRIBUTING.md for the contribution workflow and ROADMAP.md for the next scoped increments.

Credits and design references

spanselector is built with NiceGUI, Pydantic, Pydantic Settings, and PyYAML. Direct dependencies and their licenses are listed in THIRD_PARTY.md; the lockfile records the complete resolved dependency graph.

The interface takes principle-level inspiration from Motion, Revolut's published design principles, and Kokonut UI: purposeful motion, strong hierarchy, quiet surfaces, and clear action states. No source components, brand assets, or proprietary typefaces are copied, and this project is not affiliated with those projects or companies.

License

Released under the MIT License.

About

Python NiceGui data annotation tool.

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages