README.md · last modified 2026-05-27 17:49
FastAPI-based sound annotation tool with two web apps:
- Client app for annotators (audio playback + label selection)
- Admin app for session management, config/template editing, and in-window client previews
.mp3 clips for annotation in the browsertestingannotation_sessionfree_onlinenot_openzoneinfo from the standard library)requirements.txt:fastapiuvicorn[standard]tomli_wjinja2tomlimarkdownpython-multipart (required for form endpoints)python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Start the apps in separate terminals.
python3 main.py
The client listens on the port configured in session_config.toml ([session].port, default 5020).
python3 admin.py
Admin runs on 127.0.0.1:5021.
http://localhost:5020/ (or configured port)/self-testhttp://127.0.0.1:5021//loginIf base_path is configured (see below), client routes are served under that prefix (for example /sa-annotation).
Edit.When saving config/template for the active session, admin rewrites root session_config.toml so the client immediately uses updated values.
/sa-annotation-admin and log in as admin-tjeerd.Default) or select an existing one.session.audio_dir to that folder./sa-annotation (welcome → start → playback → submit).annotations folder./Users/tjeerd/Library/Mobile Documents/com~apple~CloudDocs/sa-annotator-data.mp3 files only00123.mp3, city_045.mp3)clip_id used in annotations is the filename without .mp3.sessions/<session_id>/.sessions/.active.session_config.toml is the runtime config used by client and is synced from the active session by admin.session_config.toml)Example structure:
[session]
name = "Stadsgeluiden in Haarlem"
location = "Haarlem, venue X"
timezone = "Europe/Amsterdam"
audio_dir = "/absolute/path/to/mp3s"
annotation_template = "acoustic_annotations_urban_NL.toml"
annotations_dir = "annotations"
port = 5020
base_path = "/sa-annotation"
language = "nl"
languages = ["nl", "en"]
[welcome]
text_nl = "Welkom bij de annotatiesessie."
text_en = "Welcome to the annotation session."
[state.testing]
start = "2026-04-22T09:00:00+02:00"
end = "2026-04-29T19:30:00+02:00"
welcome_text_nl = "Annotatietest om je computer en geluid te testen."
welcome_text_en = "Test annotation to verify your computer and audio setup."
max_clips = 3
annotations_dir = "annotations_testing"
[state.annotation_session]
start = "2026-04-29T19:30:00+02:00"
end = "2026-04-29T23:30:00+02:00"
welcome_text_nl = "Annotatie sessie van stadse geluiden in het centrum van Haarlem"
welcome_text_en = "Annotation session for urban sounds in the center of Haarlem."
[state.free_online]
start = "2026-04-29T23:30:00+02:00"
end = "2026-05-20T23:59:00+02:00"
welcome_text_nl = "Open online annotatie."
welcome_text_en = "Open online annotation."
[state.not_open]
start = "2026-05-20T23:59:00+02:00"
end = "2026-06-30T23:59:00+02:00"
welcome_text_nl = "Op dit moment is er niets te annoteren."
welcome_text_en = "There is currently nothing to annotate."
[email]
enabled = true
message_nl = "Als je je email adres achterlaat kunnen we je op de hoogte houden van ontwikkeling."
message_en = "If you leave your email address, we can keep you informed about developments."
placeholder_nl = "naam@voorbeeld.nl"
placeholder_en = "name@example.com"
timezone should be an IANA timezone string, e.g. Europe/Amsterdam.timezone is set, state windows are still evaluated from ISO timestamps, and display is converted to that timezone.testing defaults to 3 clips if max_clips is omitted.not_open → testing → annotation_session → free_online (avoid overlapping windows).base_path is optional. Use empty/omitted for root deployment, or a prefix like /sa-annotation for reverse-proxy/Funnel path deployment.Annotation labels are loaded from a TOML file under nested timescale/category sections:
[second.human]
labels = ["speaker", "laughter"]
[minute.transport]
labels = ["traffic", "train"]
annotations/ for normal statesannotations_testing/ for testing stateannotated_at, updated_at)_session_meta)[session].languages.[session].language.lang switch route (/set-language?lang=...)Accept-Language is intentionally ignored for first-load defaults)For deployment at a subpath (for example https://<node>.ts.net/sa-annotation):
1. Set [session].base_path = "/sa-annotation" in session_config.toml.
2. Start the client app on the configured port (default 5020).
3. Configure Funnel path mapping:
tailscale funnel --bg --https=443 --set-path=/sa-annotation localhost:5020
tailscale funnel status
static/vendor/htmx.min.js./self-test checks:python3 -m py_compile main.py admin.py config.py i18n.py
python3 - <<'PY'
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
for p in ["session_config.toml", "sessions/Default/session_config.toml"]:
with open(p, "rb") as f:
tomllib.load(f)
print("TOML OK")
PY
Use the included deploy script to sync updates to the mini, restart client/admin services, and run health checks.
./deploy_to_mini.sh
Optional: provide a different SSH host alias (default is mac-mini):
./deploy_to_mini.sh <ssh-host-alias>
progress(5021)=403 in deploy health checks means the route is available, but not authenticated yet.http://localhost:5021/login first, then http://localhost:5021/progress/Default.mac-mini not reachable:~/.ssh/config host alias and Tailscale hostname../deploy_to_mini.sh <ssh-host-alias> with a different reachable host./Users/tjeerd/miniconda3/envs/SA_3.13/bin/python).