Rescoped 2026-10-17 (Caspar). v1.6.0 was to be the public release. Announcing it and piloting it just before the viewer is redesigned would leave the public version in an awkward state, and pilot labs would report on a viewer about to change. So v1.6.0 is now a quiet consolidation release: tagged, citable and installable, not announced. The public release, with the redesigned viewer, the tutorial, the pilot and the announcements, is v1.7.0 (
roadmap_v1.7.0.md); the plugin ecosystem moved to v1.8.0. Phases 4–7 below were moved there, except the model-default refresh (Phase 5c), which stays so the public version's outputs are stable. New here: Phase 3b, the pipeline guide (spec 026). References to Phases 4, 5, 6 and 7 in earlier entries mean the plan before this date; in v1.7.0 they are Phase 5 (Agents), Phase 2 (Connectors, old 5a), spec 029 (Corpus overview) and Phase 3 (Docs).
VideoAnnotator v1.6.0 is the Consolidation Release: one repository, current dependencies, a clean first contact, and results out in formats researchers use. It is released quietly; the public release is v1.7.0.
v1.5.0 made the tool usable by us without a terminal: in-app pipeline installs, batches, folder
ingest, the VLM pipeline, and the viewer bundled at /viewer (see
roadmap_v1.5.0.md). v1.6.0 is what it takes for a lab we have never met
to install it, run it on their own videos, and get the results into their own analysis without
asking us.
Why this shape (written for the public release, which v1.7.0 now is): on 2026-09-24 the JOSS submission (#10182) was sent for an editorial scope check because no one outside the author team has used or contributed to it yet. JOSS accepts publications, outside use, integration with other research tools, or benchmarks with reproducible materials as evidence. JOSS declined on scope (2026-10-06) and is no longer a goal; the shape stands for its own reason, outside use. The pilot follows the release, and a benchmark is not in v1.6.0 (see Phase 5b).
Until 2026-09-24 this file held the plugin-ecosystem plan. That plan is now
roadmap_v1.8.0.md(v1.7.0 until 2026-10-17), with its git history.
Target Release: as soon as Phases 3b and 5c are done. No deadline (BCCCD dropped 2026-10-14). The
pilot (and spec 024's T083) moved to v1.7.0's release candidates.
Current Status: Phases 0–1 done bar non-blocking items; Phase 2 done bar the long-video checks
(spec 024 walkthroughs done 2026-10-17); Phase 3 (Results Out, spec 025) built 2026-10-16; Phase 3b
(pipeline guide, spec 026) built 2026-10-17; Phase 5c (spec 030) done 2026-10-18
Main Goal: everything an outside lab will need under v1.7.0's new viewer, done and stable:
install, pipelines, results out
Prerequisites: v1.5.0 tagged, after spec 011's SC-001 manual run
(tests/manual/pipeline_readiness_e2e.md)
- ✅ A stranger can do it. Each feature is judged by whether a researcher outside the team can use it on their own machine with only the docs.
- ✅ Local by default. No video or frame leaves the user's machine unless they switch on a hosted backend, and switching one on says so plainly.
- ✅ Results leave in formats researchers already use: CSV/Parquet for R and pandas,
.eaffor ELAN. - ✅ Agents are users too. Anything a person can do in the viewer, an agent can do through the CLI or MCP.
- ✅ No model change without a reason on record. A pipeline default changes on the upstream
model's published evaluation, after its output on the demo clip has been checked against the
baseline (
tests/integration/test_output_baseline.py). The CHANGELOG and model card say why. - ✅ Pilot on what will be announced. Outside testing runs on v1.7.0's release candidates, not on a viewer about to be replaced (2026-10-17).
Phases 0–3 made the release candidates; Phases 3b and 5c finish the release.
Problem: VideoAnnotator and Video Annotation Viewer are one product in two repositories. The
viewer is built in one repo and hand-copied into src/videoannotator/viewer_static/ in the other.
That copy went stale twice (July to August, then August to September) and hid finished features
from users both times. Cross-cutting work needs paired commits, handoff documents that went
unactioned (spec 008's viewer-handoff.md), two changelogs and two CI setups, and debugging
happens from outside either repo.
Solution:
- Import
video-annotation-viewerintoviewer/with its full history (rewrite paths withgit filter-repo --to-subdirectory-filter viewer, then merge with--allow-unrelated-histories), taken fromfeature/vlm-annotation-support. Done 2026-09-30 from the viewer'smainat its v0.7.0 release, which includes that branch; its tags are kept asviewer-v0.x.x. - One build step produces
viewer_static/fromviewer/. CI fails when the committed bundle doesn't match the source, so the copy can't be forgotten.scripts/build_viewer.sh(--checkin CI'sviewerjob, which also runs the viewer's lint and unit tests; releases wait for it). The viewer's Playwright and Lighthouse jobs, both informational, aren't in CI yet. - One dev command runs the API server and the Vite dev server (API proxied, hot reload), plus VS Code tasks for it.
- The viewer takes VideoAnnotator's version number from v1.6.0: one product, one version, one changelog. Standalone mode (drop in files, no server) stays, and is still published.
- The TypeScript half of the viewer contract test, deferred in v1.5.0, runs in CI.
viewer/src/test/contract/videoannotator-outputs.test.tsloads real job outputs (tests/fixtures/viewer_contract/) the way the artifacts zip is loaded. It found that*_openface3_analysis.jsonwas detected as person tracking and, sorting first in the zip, replaced the real person tracks with OpenFace's face boxes, while*_openface3_detailed.jsonwas detected as face analysis. Cause: every detectorJSON.parsed a 5–15 KB prefix, which throws on any larger file, so a looser substring check claimed it. Detection now classifies the whole parsed file by its annotation fields first. - One JS package manager (the viewer has both
bun.lockandpackage-lock.json). Bun;package-lock.jsonremoved. - Merge the two
AGENTS.md/CLAUDE.mdfiles. The viewer's guidance isAGENTS.md§23 (corrected for Bun,127.0.0.1, the shared version and the contract fixtures); the viewer'sAGENTS.md,CLAUDE.md, Copilot instructions and Claude settings are removed. The viewer's constitution is folded into.specify/memory/constitution.md(v1.1.0, 2026-10-02: new Principle VI, Faithful Annotation Display; viewer clauses in I, II, IV, V and Engineering Standards), and the viewer's own spec-kit setup is removed. Making "no telemetry" product-wide found Ultralytics sending a Google Analytics event per predict; off. - Move the viewer's open issues, archive the old repo with a pointer README, and update links
in
README.md,paper/paper.mdandCITATION.cff. Links done (2026-10-01):README.md,docs/usage/GETTING_STARTED.md, and the viewer's README, docs,CONTRIBUTING.mdandpackage.jsonpoint here;viewer/CITATION.cff(a separate v0.7.0 that no longer exists) removed.paper/paper.mdandCITATION.cffhad none. Archives, the frozen viewer changelog and the sent cover letter keep the old links. The old repo has no open issues, so nothing to move. Pointer README pushed and the repo archived 2026-10-02.
Viewer bugs found in the v1.5.0 end-to-end run (2026-09-26), to fix once viewer/ is in
this repo:
- VLM prompt sent as
base_url: submitting a job withvlm_annotationstored the prompt text in bothpromptandbase_url, so the job failed with "Port could not be cast to integer value as ' 2, child: True'". Likely the wizard's config form binding the prompt textarea's value to thebase_urlfield too. Since610aea1the server rejects a non-URLbase_urlat submission (400INVALID_URL), so it now fails fast, but the form is still wrong. Fixed early in the viewer (4ec8d0b): the stored job'sbase_urlis the prompt with its newlines stripped, i.e. pasted into the single-line Base Url box, not a binding bug. URL fields now show an inline error and block Next/Submit. - Misleading error on a rejected job: when
POST /api/v1/jobsreturns 400 with a clear message (e.g. "Unknown pipeline 'speaker_diarization'"), the viewer shows "All job submissions failed" with a generic tip that the server may not be running or the token may be invalid. Show the server'smessage/hintinstead; keep connection tips for actual connection failures. Fixed early in the viewer (4ec8d0b). - No library folder selected → flickering dialog on "View jobs": the dialog flickers and
never explains what a library folder is or how to choose one. Show a steady prompt with a
"Choose folder" action (and why it's needed), or let job viewing work without one.
Fixed: cause was a loop. The results page starts a download whenever its state is
idle; a folder picker that was cancelled, or refused because no click preceded it, setidleagain, so it restarted at once. Now it waits inneeds_folder: a card says what the library folder is for, with "Choose folder" (a real click, so the picker is allowed) and "View without saving". A folder already granted is reused without asking. Found alongside: the exportedapiClientproxy didn't bind methods, soapiClient.updateConfig()changed nothing and Settings' "Test Connection" always tested the saved configuration, not the one on screen. - Scene detection always shows "(No data)":
parseSceneDetection(src/lib/parsers/scene.ts) accepts a bare array,resultsorscenes, but not COCO'sannotations, which is what the backend writes. It throws, and the scenes panel is empty for every job, however many scenes there are. Found 2026-09-28 on a clip with one scene (0–7.32 s, "nursery") the backend had detected correctly. Fixed early in the viewer (4ec8d0b). - Failed pipelines' reasons are hard to find: a batch says "N with errors … the reason is
on each video's row", but the reason is only a hover tooltip on the status badge, and the
job page doesn't list failed pipelines with their
error_messagefromGET /jobs/{id}/results. Show per-pipeline errors on the job page and inline in the batch row. (Server side: since 2026-09-28 the job-levelerror_messageincludes each failed pipeline's error, not only its name.) Fixed: each batch/jobs row shows the reasons under its status badge (two lines, full text on hover); a job that failed outright lists each pipeline's error on its page, as a job completed with errors already did. -
localhostvs127.0.0.1: viewer can't connect, or connects without its token (recurring). The server side (start_server.sh, thesetup-db/generate-tokenlogin links,CORS_AND_AUTH_PROTOCOL.md) sayslocalhost. The viewer defaults its API URL to127.0.0.1and rewrites a savedlocalhostto127.0.0.1. The browser treats them as two sites with separate storage, so after logging in via alocalhostlink the viewer either can't reach the API ("Cannot Connect", seen 2026-09-28 in the devcontainer on Windows) or reaches it with no token (401 on every call). Server side fixed in v1.5.0 (2026-09-28): the server redirectslocalhost/viewer…and/viewer-connectto127.0.0.1, and every link it prints uses127.0.0.1. Left for the viewer: when the server serves it, use the page's own origin as the API URL, with no rewriting, and drop the redirect. Fixed (viewer/src/lib/apiConnection.ts): an empty API URL, or one naming the page's own origin, stays relative, so the served viewer always calls its own server; Settings no longer pre-fillshttp://127.0.0.1:18011there (saving that made the call cross-origin). Another server atlocalhostis still sent to127.0.0.1(the server binds IPv4). The redirect stays: the printed links save the key under127.0.0.1, and the redirect is what makes a viewer opened atlocalhostfind it. Also fixed: "Test Connection" with an empty key tested the previously saved key, because the client treated''as "keep". - Settings page doesn't say how to get a token (2026-09-30): the welcome box says "Get your
API token from the server console or administrator", and its default URL is
http://localhost:18011(see the item above). A new user doesn't know which console, what the token looks like, or what to do if they missed it. Replace the Quick Start with: 1. First start: the server prints[API KEY] VIDEOANNOTATOR API KEY GENERATED, then ava_…key and a one-clickhttp://127.0.0.1:18011/viewer-connect?token=…link. Opening the link logs the viewer in; there's no need to paste anything. 2. Missed it, or need another key: in a terminal on the server machine, runvideoannotator generate-token(in auvcheckout,uv run videoannotator generate-token). It prints a new key and a one-click link. Keys can't be shown again once printed. 3. Someone else runs the server: ask them to run step 2 for you with--user <your email>. 4. Auth off (AUTH_REQUIRED=false): leave the token blank. Include a copy button for the command, and show the key format (va_+ 43 characters) as placeholder text so a pasted wrong value (for example the wholeBearer …line) is caught before "Test Connection". Server side (done 2026-09-30): the first-run banner now prints the viewer-connect link andvideoannotator generate-token. Before this it pointed atlocalhost:8000and ascripts.manage_tokensmodule that pip installs don't have. Done:TokenHelp(the four steps, copy button) on the Settings page and its Help tab, the key format as placeholder, and an inline check that blocks saving a pastedBearer …, a truncated key ordev-token. Everydev-tokeninstruction is gone; the client rejects it. - Face boxes and OpenFace landmarks almost never show (2026-09-30): both overlays draw a
face only within ±0.1 s of its timestamp (
JD(face_analysis, t, 0.1)and theopenface3_facesfilter). The face pipelines sample about once a second (0.97 s apart on a 30 fps clip), so a face flashes for 0.2 s per second during playback and never shows when paused between samples. The data is in the job's*_face_detections.jsonand*_openface3_analysis.json, and it parses. Hold each face until the next sample of its track, capped at about 1.5× the sample interval, which is taken from the data itself rather than a constant, so a face that disappears doesn't stay on screen. Server side (fixed 2026-09-30): DeepFace's "no face found" stand-in, a full-frame box with confidence 0 and an invented emotion, was saved as a real face (143 of 255 boxes in the e2e jobs). Jobs run before the fix still contain those boxes. Fixed:viewer/src/lib/sampledAtTime.tsdraws the latest sampled frame's detections until the next sample, for at most 1.5× the median sample interval. Pose uses it too: its ±0.5 s window stacked several frames' skeletons when sampling was dense. -
openface3_detailed.jsondetected as face analysis: the face check ("emotions"in the first 8 KB) matches itsmetadata.model_info.features, and it runs before the OpenFace check. The file parses to nothing, so today the face boxes survive only becauseface_detections.jsonsorts first in the zip. Check for OpenFace before face analysis, or match on structure (metadata.pipeline+faces) instead of substrings. Fixed (47cc786), found by the contract test along with a worse one:openface3_analysiswas taken as person tracking and replaced it (see the contract test item above). - Results view isn't batch-aware: opening a video from a batch loses the batch. Show which
batch and video (n of N) you're on, previous/next between the batch's videos, and a way back
to the batch page. Overlaps Phase 6's previous/next item; do the navigation here, before
release.
Done: the results header shows the batch's name and "n of N" (a link to the batch page),
with previous/next to the nearest videos that have results, from any entry point (the job
carries its
batch_id); Back goes to the batch. - First-run download total double-counts shared weights: the "Preparing… downloads about
1.4 GB" line sums each pipeline's
weights_not_cachednotes, so a model two pipelines share (pyannote/speaker-diarization-3.1, for audio_processing and speaker_diarization) counts twice. Deduplicate by the note'snamebefore summing. Fixed early in the viewer (4ec8d0b).
Why first: every later phase writes docs and tutorials full of repository links. Moving after that means rewriting them, and breaking links pilot labs have already saved.
Problem: both the set of pipelines and most version pins are historical, not decisions. The
pipelines are whatever got added along the way, and no one has asked which tools a developmental
researcher actually needs. Most pins are the same: torch==2.6.0 is pinned exactly in
five extras groups; pyannote.audio<4.0, transformers<5.0 and opencv-python<5.0 were
defensive bounds against breakage we never tested for; Python is locked to 3.12
(>=3.12,<3.13), which has had security fixes only since 2025 and reaches end of life in October
2028. Every one of these gets more expensive after release: once outside labs have results, an
upgrade that changes outputs breaks their comparisons. With no users yet, now is the cheapest time
it will ever be.
Already known about dependencies (uv resolution of every extras group with the historical pins lifted, 2026-09-26):
- Python 3.13 resolves cleanly for every group, with current releases (torch 2.14, pyannote.audio 4, transformers 5, TensorFlow 2.21).
- Python 3.14 resolves for every group except
face: deepface needs TensorFlow, which has only a release candidate for 3.14. openface-testmust stay at==0.1.13: every later release pins Pillow 9.4, numpy 1.26 and scipy 1.13, which have to be built from source on 3.13+.- pyannote.audio 4 changes the pipeline API, reads audio through torchcodec (FFmpeg shared
libraries at runtime) and brings
speaker-diarization-community-1, the free open-weights successor tospeaker-diarization-3.1(the paid "precision" models are pyannoteAI's hosted service and stay out: local by default). - A newer torch leaves the cu124 wheel index in
[tool.uv.sources], which raises the minimum GPU driver.
Also known about the pipelines (Hugging Face, 2026-09-26):
- LAION Empathic-Insight Voice (
laion_voice): the model repositories are 16 GB (Small) and 32 GB (Large), last updated in May 2025, with 1 and 4 downloads in the last 30 days. The face models (face_laion_clip) are small (30 MB / 300 MB) but had no downloads in that period. Both look abandoned upstream. - deepface (
face_analysis) is the only reason TensorFlow is in the dependency tree, and so the only thing holding back Python 3.14. Its age and gender outputs say nothing useful about infants. face_openface3_embeddinginstalls OpenFace 3 throughopenface-test, a third-party repackaging whose releases after 0.1.13 pin old Pillow, numpy and scipy.
Solution, in four steps:
- Pipeline review (
docs/development/pipeline_review_v1.6.0.md). Start from what the field asks, not from what we have: caregiver speech, infant vocalisations, who is speaking, faces and expressions, movement, gaze and joint attention, touch. For each question, which tool answers it best today. For each existing pipeline: which question it answers, whether its upstream is maintained, download and install cost, licence, how it does on infants and young children, and overlap with other pipelines. Decide for each: keep, replace, mark experimental, or drop (a dropped pipeline can come back as a v1.7.0 plugin). The review sets the scope of the audit and of Phase 5's model work, and gives Phase 7 its "which pipeline for which question" page. With no users yet, dropping a pipeline breaks no one. - Dependency audit (
docs/development/dependency_audit_v1.6.0.md), for the pipelines the review keeps. For every direct dependency of the core package, each extras group and the viewer (merged in Phase 0): current constraint, latest release, why it is pinned (git history, CHANGELOG), what upgrading changes (API, outputs, install: wheels, CUDA, system libraries), and a decision: upgrade, keep with a stated reason, replace, or drop. Also covers the Python version, the CUDA index and driver floor, Docker base images, Node and the JS package manager, pre-commit hooks and GitHub Actions versions. - Specs: one spec-kit spec per coherent change the audit calls for (
/speckit-specify). Expected, subject to the review and audit: removing dropped pipelines; Python 3.13 (with a 3.14 CI job, made required once TensorFlow ships for it, or once deepface is dropped) and the core dependencies; the torch stack and CUDA index; the pyannote.audio 4 migration; the viewer's dependencies. (Done 2026-10-01: specs 012 Python 3.13, 013 core dependencies and tooling, 014 drop the LAION pipelines, 015 torch 2.11 + pyannote.audio 4, 016 one models directory.) - Implement the specs before rc1. Re-baseline the v1.4.x acceptance fixtures once, on
purpose, and record the before/after differences on the demo video in the CHANGELOG.
(Specs 012–016 implemented and passing CI on
1.6-dev. Re-baselined 2026-10-02 against v1.5.0, not v1.4.4: the v1.4.4 fixtures were never captured, and v1.5.0 is the release these upgrades follow. Outputs unchanged up to GPU noise (CHANGELOG). The baseline is the viewer contract fixtures, checked bytests/integration/test_output_baseline.py, real models.) - Docker image size: build the image slim and with
--build-arg EXTRAS=all, and record both sizes against the v1.4.3 baseline. v1.5.0 set a target of 80% smaller but couldn't measure it. 2026-10-02: the three Dockerfiles became oneDockerfileonubuntu:24.04(nonvidia/cudabase: torch's wheels carry CUDA 12.6 and cuDNN). Measured on the host (handover_docker_images.md): slim 1.35 GB (347 MB compressed), everything 14.9 GB (4.78 GB), v1.4.3 26.1 GB (8.89 GB): slim −95%, everything −43%. The 80% target holds for the slim default only. TensorFlow and torch both reach the GPU without the CUDA base; a six-pipeline job completes on GPU and CPU. The run found two install bugs (fixed):httpxmissing from core, and no first API key on a fresh database. A CPU-wheel variant (~3.6 GB smaller) is blocked: TensorFlow then open_clip segfaults on CPU torch wheels; revisit when Phase 5 replaces DeepFace. - Windows froze with the dev container running (2026-10-02; handover in
docs/development/handover_windows_devcontainer_freeze.md). Cause: memory exhaustion. The host runs at ~80% of its 31 GB with everyday apps, and the WSL VM (uncapped, up to 15.6 GB) is the one thing that can grow by ~11 GB: page cache and process memory count as VM memory until the VM hands them back. A resume from hibernate added the last burst; the machine sat at ~99% memory until a power-button reset. No test run was involved. Done (merged 2026-10-02):.venv, models andviewer/node_modulesin named volumes; a start-up warning for a Windows-drive workspace (scripts/check_workspace_mount.sh); the container capped at--memory=12gindevcontainer.json. Measured under that cap in a volume clone (handover, Update 6): installing the extras alone fills the cap with page cache (reclaimed, no OOM kills); the full defaultpytestpeaks at 5.4 GiB and takes 96 s; a six-pipeline job on the demo video peaks at 6.45 GiB; test collection is 10–25× faster than over the bind mount. - Follow-ups from the freeze, not blocking:
- Size the container cap from one long-video job (likely 8g or 10g; 12g until then).
- Check whether CUDA's "Shared GPU memory" sits outside both caps (Task Manager during a job).
- ~~Docs (Phase 2): the volume clone, the
uv sync --inexact --extra allstep a fresh clone needs (~5 min), the 16 GB-machine guidance, and the OOM symptom (Killed, exit 137) with the cap indevcontainer.json.~~ Done 2026-10-05:INSTALLATION.md"Dev Container" and the troubleshooting entry rewritten around the volume clone and the container's own cap, with the measured numbers;.wslconfigis now a fallback, not the first step.Docker.mdhas the measured image sizes.
Also in this phase: one place for model weights. Today the weights end up wherever each
library puts them by default. Whisper, YOLO and the LAION pipelines use ./models/<name>
(relative to the server's working directory, so a different directory means another download).
Hugging Face uses ~/.cache/huggingface, pyannote 3.x uses ~/.cache/torch/pyannote, and
DeepFace uses ~/.deepface. Users can't find them, and in a container they're lost on every
rebuild. The devcontainer points all of them into models/ with environment variables (2026-09-28),
but only the devcontainer does.
- One setting,
VIDEOANNOTATOR_MODELS_DIR(default decided in the spec:./modelsor a per-user data directory), resolved to an absolute path once at startup. Under it, one subdirectory per source:huggingface/,pyannote/,torch/,deepface/,whisper/,yolo/, and so on. - The server and CLI set
HF_HUB_CACHE(notHF_HOME, which holds the login token),TORCH_HOME,PYANNOTE_CACHEandDEEPFACE_HOMEfrom it before any pipeline library is imported, unless the user has set them. Pipelines' owncache_dirdefaults come from the same place. - Readiness (
api/readiness.py) finds weights through the same resolver, so "downloads about N MB" is never wrong about where it looked. - Moving the default means existing installs download once more. Say so in the CHANGELOG and print the old and new locations the first time the server starts. Drop the devcontainer's own environment variables once the app sets them.
-
videoannotator diagnoseshows where the models directory is and how much it holds. (Done in spec 016, 2026-10-01: default is the per-user data directory.) - Logs go to
./logsrelative to the working directory, like the models used to: put them under a per-user directory too (found during spec 016). (Done 2026-10-02:VIDEOANNOTATOR_LOG_DIR, per-user default;/app/logsin Docker,<repo>/logsin the dev container.) - The database lives in
./videoannotator.dbtoo, under wherever the server starts, so starting it from another directory shows an empty job list (found 2026-10-02). There are two database layers: the storage backend (api/database.py, honoursVIDEOANNOTATOR_DB_PATH) and the SQLAlchemy one (database/database.py: users, tokens, jobs routes, datasets, presets), which reads onlyDATABASE_URL. SetVIDEOANNOTATOR_DB_PATHalone and the second still opens./videoannotator.db. (Done 2026-10-02, no spec: with no users yet, an existing database isn't migrated, only mentioned in the CHANGELOG. One resolver,database_location.py, for both layers; per-user default;/app/databaseplus a named volume in Docker;<repo>/videoannotator.dbin the dev container.) - Download weights ahead of time (spec 011's FR-017, deferred there): an action per pipeline, in the viewer, the API and the CLI, that downloads its declared weights into the models directory before the first job, reusing the extras install job's table and statuses. Needs a download step per model library (Hugging Face, torch hub, Ultralytics, DeepFace, Whisper), which this phase's one models directory makes practical.
Library versions vs default models: this phase upgrades libraries. Switching a pipeline's
default model (for example to speaker-diarization-community-1) is Phase 5c's. If a library upgrade can't keep the current default model runnable, the audit says so and
the spec decides.
Why second: the install instructions, tutorials and docs written from Phase 2 onwards name the pipelines, the Python version, the extras and the system requirements. Changing those after writing the docs means rewriting them.
Problem: the README and docs are written for developers and have drifted. The README's
quickstart still installs pipelines with curl and says every install needs a restart, both
replaced by spec 011. Internal team material sits in the public docs. The viewer shows a
half-built page.
Solution:
- README rewritten for researchers: what it does, one screenshot, install in three steps per
OS, open the viewer. Done 2026-10-05 (471 lines to about 190); the start script and shell
aliases moved to
docs/usage/GETTING_STARTED.md, API details to the docs. - Internal material out of the public docs:
docs/testing/team handoffs (Jerry-issues.md,Issues for Server Team.md,TEAM_HANDOFF_PACKAGE.mdand others) anddocs/Figure 1.docxmove todocs/archive/or go. Done 2026-10-05: those, the dated fix notes, the coding-agent setup guide and the v1.3 scripts audit are indocs/archive/. - Docs site (mkdocs-material on GitHub Pages) built from
docs/, archive excluded.mkdocs.yml; CI'sdocsjob builds it with--strict,docs-site.ymlpublishes it from master. Links that leavedocs/become GitHub links in the site (scripts/mkdocs_hooks.py). Pages must be switched on once (Settings → Pages → Source: GitHub Actions). - Docs have one entry page (
docs/README.md) and a link check in CI, so moving files can't leave dead links. (Unfinished since spec 001: T055, T056, T059.)docs/README.mdrewritten by task (install, use, run it for a group, contribute);scripts/validate_docs_links.pychecks relative links in README, CONTRIBUTING anddocs/(archive aside). - No placeholder pages. The Datasets page says "Coming Soon" behind a disabled button, although
spec 007's backend shipped in v1.5.0: wire it up or hide it.
(Preset load/save in the job wizard landed in v1.5.0; the Datasets page, saved datasets and
import/export from spec 007's viewer handoff are what's left.)
Done 2026-10-04: hidden. The page had no menu link, only a URL;
/datasetsnow goes to the Library. Wiring up saved datasets is a feature, so it gets a spec (below). - Saved datasets in the viewer (spec 007's viewer handoff): list, create, rename and delete
the server's saved datasets (
/api/v1/datasets, shipped in v1.5.0), start a job from one, and import/export them. Write a spec first. Decide with Caspar whether it's before rc1. Spec:specs/018-saved-datasets-viewer/(2026-10-04). Caspar: before rc1. Done 2026-10-04: Datasets page, wizard save/reuse with differences shown, export/import for datasets and presets,videoannotator datasetCLI,GET /ingest/scan. Checked in Chromium against a real server. -
videoannotator job submit|status|results|listsend no API key (found 2026-10-04): with authentication on, the default, they get 401. Give them--api-key/VIDEOANNOTATOR_API_KEYlikepipelines installanddataset, and default--serverto127.0.0.1. Done 2026-10-04 with spec 019. - Viewer: zero
tsc --noEmiterrors (24 on 2026-09-26), with typechecking in CI. Constitution 1.1.0 makes this an Engineering Standard, as are a 300 KB gzipped initial bundle (304 KB on 2026-10-02) and overlays naming the pipeline and version that drew them (Principle VI; not shown yet). All three are open follow-ups in its Sync Impact Report. Note (2026-10-01): plainbunx tsc --noEmit, the documented check, compiles nothing (the roottsconfig.jsonhas"files": []and only references); run-p tsconfig.app.json. Still 24 errors in 13 files, e.g. the OpenAPIpathstype insrc/api/client.tslacks/api/v1/jobs. Done 2026-10-04: zero errors (bun run typecheck, app and node configs) and a CI step. The types are regenerated from the server (scripts/gen_viewer_api_types.sh). One was a real bug: the job page'srefetchIntervalread.statusoff the query, so the page never polled. The bundle size and overlay labels are split into the next item. - Viewer: the constitution's other two open Engineering Standards. A 300 KB gzipped initial
bundle (304 KB on 2026-10-02), and overlays naming the pipeline and version that drew them
(Principle VI).
Overlay labels: spec
specs/017-output-provenance/(2026-10-04), which also records the provenance Phase 3's methods paragraph needs. Done 2026-10-04 (spec 017): every output carries a provenance record, the job keeps it per pipeline, and the viewer labels every overlay with its pipeline and version (details on demand). Older files say "version not recorded". Bundle done 2026-10-04: every page but Home is lazy-loaded (App.tsx). The initial download fell from 301 KB JS + 14 KB CSS gzipped to 165 + 14 KB. Every route was checked in Chromium against the production build. Overlay labels are still open. - Viewer: one function decides which pipeline produced a file. Today there are four
(
merger.ts,fileUtils.ts, and two arrays inFileUploader.tsx) and they disagree. Start frommerger.ts'sdetectJSONStructure(2026-10-01), which classifies a parsed file by its fields and is covered by the contract test against real outputs. Done 2026-10-04:lib/fileDetection.ts(detectFileType, re-exported frommerger.ts).fileUtils.tsand merger's prefix-sampling checks are gone. A file-name suffix is used only when the JSON is valid but says nothing (no annotations). Fixed along the way: the upload screen called every JSON "unknown", so it warned on every result set, and it couldn't open a video with only ELAN ground truth. - A structural pass along the one path users take: install → add videos → run → review →
export. Anything off that path moves or goes. A Playwright "first-time user" run on a clean
machine files each point of friction as an issue.
2026-10-05: first run done (Linux, fresh clone and home, README steps, Scene Detection):
docs/development/handover_first_time_user_run.md. It found two blockers, both fixed: the first-start key wasn't admin, and Linux extras installs couldn't resolve torch. Job folders then moved beside the database (they followed the start directory). The other points are tracked in that note, not as GitHub issues, while the work is in-house (Caspar, 2026-10-05); some need a decision (results-folder prompt, what moves or goes). Windows, macOS, GPU, a long video and the VLM and datasets paths are still to run. - OpenFace 3 results are sensitive to GPU nondeterminism (found 2026-10-01, spec 013): a
1-pixel difference in RetinaFace's box between two runs of the same video changes that
face's gaze and action-unit intensities by up to ~2.4. Offer a deterministic mode
(
torch.use_deterministic_algorithms, cuDNN deterministic) and record it in provenance, and measure run-to-run spread for every pipeline. Also order-dependent (found 2026-10-04):scene_detection's CLIP scores on the demo clip match the baseline exactly when it runs afterface_openface3_embeddingandperson_trackingin the same job, but differ in the third decimal (0.6352 vs 0.6340) when it runs alone. Something an earlier pipeline loads changes global torch/cuDNN state. A deterministic mode should pin those settings at the start of each pipeline. Done 2026-10-04. The cause of both was OpenFace's STARset_environment(), which turned oncudnn.benchmark(also anomaly detection and denormal flushing) for the whole process.utils/torch_settings.py: every pipeline starts from the same settings, re-applied after itsinitialize(), and restored afterwards."deterministic": truein the job config adds cuDNN deterministic anduse_deterministic_algorithms(warn_only=True). It is recorded in the job config and logged per pipeline. Not yet in the output files: that comes with Phase 3's provenance. Measured on the demo clip: two normal runs, two deterministic runs, and single-pipeline vs five-pipeline jobs all give identical files. Scene and OpenFace fixtures re-baselined (README). Not measured: spread across GPUs and drivers. - A pipeline that fails must say so.
speech_recognitioncatches errors in transcription, logs them only to the pipeline log, and returns no annotations, so the job reports the pipeline as completed with an empty transcript (found 2026-10-01 through a Triton cache error). Raise instead, so the job shows the pipeline as failed with its error; check the other pipelines for the same pattern. Done 2026-10-03. Every pipeline was audited, and seven had the pattern (see CHANGELOG). The worst wasscene_detection, which invented one whole-video scene on failure. Frame loops keep skipping single bad frames, butFrameFailures(base_pipeline.py) fails the run when every frame fails. Tests:tests/unit/pipelines/test_pipeline_failures.py. Left as is: per-face enrichment failures still only log a warning, so those faces lack the fields (OpenFace 3 landmarks/AUs, DeepFace emotion falling back to a bare box, person_tracking's size-based labels). Surfacing degraded fields belongs with the provenance work. - The API stops answering while a job runs (found 2026-10-03): during the real-model
baseline job,
GET /api/v1/jobs/{id}timed out (over 30 s) or returned a body with nostatus. The host was using 6.4 GB of the 8 GB GPU, so every pipeline ran 2–4× slower than usual. Jobs run in an executor thread (background_tasks.py), so the likely cause is CPU or GIL starvation. Reproduce on an idle machine, and make the status route stay responsive. It is what the viewer polls. 2026-10-04: not reproduced. The same test passed through the server in 32 s with the host still holding 5.3 GB of the GPU. STAR's anomaly detection, which the job used to inherit, was measured and doesn't slow inference. Keep this open until the Playwright run (E) has polled during a long job. 2026-10-05, first-time-user run: 344 polls during a scene-detection job, median 25 ms, slowest 349 ms. Still to do with a long video and the heavy pipelines. -
GET /api/v1/jobs/{id}/results/files/{pipeline}returnsOUTPUT_FILE_MISSINGfor every result stored in the database (output_file: database:/annotations/...), which is what each job's results list advertises as itsdownload_url. Same in v1.5.0 (found 2026-10-02). Serve the file from the job folder, or stop advertising the URL. Done 2026-10-04: served from the job folder by the suffix in each pipeline's registryoutputs[].file; results list every file (files,?name=) and advertise only files that exist. Tests:tests/api/test_result_files.py. -
videoannotator process <video>is listed in--helpbut only prints "Direct processing is not yet implemented" (found 2026-10-01). Implement it on the shared job-execution path, or remove it. Done 2026-10-04 (batch/local_job.py): validated as a submission is, recorded in the server's database, run throughrun_job_pipelines.--pipelinesis required: a default of every pipeline would be slow and need every extra. Tests:tests/unit/cli/test_cli_process.py. - Queue position: a pending job shows how many jobs are ahead of it ("3rd in queue"), in
the job list, the job page and
GET /api/v1/jobs/{id}. Today a queued job looks the same as a stuck one. (Planned since spec 001's T066.) Done 2026-10-04. The API already returnedqueue_position(oldest pending first, the order the worker takes them) on submit, status and list; the viewer now shows it (lib/queuePosition.ts) inJobsTable(job and batch lists) and on the job page.
Run it again: the path doesn't end at review. A researcher who likes a result wants the same
settings on more videos; one who doesn't wants to tweak and rerun. Today both mean rebuilding the
job in the wizard from memory, although every job already stores its selected_pipelines and
config.
-
Rerun a job: same videos, same settings, one click, as a new job that links back to the original (
rerun_of). "Edit and rerun" opens the wizard prefilled. Spec (with the next two items):specs/019-run-it-again/. Done 2026-10-04 (all three items): rerun endpoints and CLI, job/batch page actions, wizard edit/settings modes, recent jobs and presets on the first step. Checked in Chromium against a real server. The Playwright first-time-user run (E) still has to confirm the cues are found. -
Reuse settings on new videos: "Use these settings" on a job or batch opens the wizard at "Choose videos" with pipelines and config filled in; "Save as preset" writes the existing
saved_pipeline_presetstable. -
UX cues that lead there, not only buttons in a menu: the actions sit on the job/batch result page where the user is looking when they decide; the wizard's first step offers "Start from a previous job" and recent presets before the blank form; a failed or partial job says "Fix settings and rerun"; a completed batch suggests "Run on more videos". Check each in the Playwright first-time-user run.
-
Prompt library: every VLM prompt used, in a job or a preview, saved to the database once (deduplicated by SHA-256, the same hash as the methods paragraph's provenance) with its model, first/last used and the jobs that used it. A browser to search, view, diff, name, star and reuse them; reusing one fills the prompt field. Spec (with the workbench):
specs/020-prompt-library-workbench/. Done 2026-10-04 (with the workbench): library tables and API, recording from jobs and previews, Prompts page, Workbench page, CLI. Tested for real with Ollama (gemma4:e4b, qwen3.5:9b): a VLM job's provenance carries the model digest and quantisation, and a 2 × 2 × 2 workbench round ran in 109 s. That test found the CRLF bug above. -
Prompt workbench: the "test prompt" panel from inside the VLM pipeline config as a standalone page, since prompt design is iterative and deserves more room than a wizard step. Pick a video and frame (or burst), a model and a prompt; run; compare responses side by side across prompt versions or models; send the winner to a job or preset. Backend exists (
POST /api/v1/vlm/preview,GET /api/v1/vlm/models); new work is the page, the prompt table and its endpoints. Also through the CLI/MCP (Phase 4). -
Compare two VLM jobs on the same video: their labels on one timeline, with the frames where they disagree listed, and ELAN ground truth as a third row when there is one. The workbench compares prompts on single frames; this compares whole runs. Asked for in spec 009's viewer handoff; comparing across a whole dataset stays in Phase 6. Spec:
specs/021-compare-vlm-jobs/. Done 2026-10-04. Tested for real: gemma4:e4b every 5 s vs qwen3.5:9b every 2 s on the demo clip. 3 moments paired, 3 unpaired, all 3 disagreeing (TOUCH vs NO_TOUCH); the video seeks to a selected moment. -
Videos read where they are, results in one visible folder (the first user is a researcher on their own laptop): "My folders" is the wizard's default on the server's own computer, with individual videos selectable and nothing copied; every run's results go to
~/VideoAnnotator/<run> (<date>)/<video>/with arun.json, shown, opened and downloaded (one zip per run) from the viewer; moved videos are named and can be located again; Docker getsVIDEOS_DIR/RESULTS_DIRon loopback. Spec:specs/022-videos-read-in-place/. Done 2026-10-06. Its Docker walkthrough is replaced by spec 024 (below). -
A container that feels local: a start-up program for Docker and Podman that asks which folder the videos are in, shares it read-only at its real path, remembers it, and never shows the container's own folders. Spec:
specs/024-container-feels-local/. Built 2026-10-06 (videoannotator-startfor Linux/macOS and Windows, the slim image on GHCR, installed pipelines restored after recreation, Settings' Stop sharing). Quickstart walkthroughs done 2026-10-17: Windows by hand (T084), Linux reliable in daily use (T082). Open: the CI end-to-end runs with Docker and Podman (first run on a push tomaster), and a pilot researcher on macOS (T083, with v1.7.0's pilot). -
The API and CLI from a launcher install (found 2026-10-16): a researcher started with
videoannotator-starthas novideoannotatorcommand on the host, and the API key it made sits instart.conf, which no command prints. Phase 3's R and Python notebooks and Phase 4's MCP server both need the URL and key. Proposal:videoannotator-start apiprints the URL, the key and a three-line R, Python and curl example, andvideoannotator-start cli ARGSruns the CLI inside the container with the key already set. Docs that saydocker compose exec videoannotator va ...(accessing_results.md,demo_commands.md) move to whatever this settles on. -
INSTALLATION.mddown to the researcher's install (2026-10-16: 729 lines): it opens with the launcher and then runs through the developer uv install, a manual OpenFace 3 build that theface-openface3extra replaced, Docker Compose, the dev container, a second troubleshooting section, an architecture overview and performance tips. Keep the launcher steps, sharing, GPU, Ollama, system requirements and where results go. Move the uv install and dev container to a developer page linked fromCONTRIBUTING.md, Docker Compose to its own page, troubleshooting intotroubleshooting.md, and drop the manual OpenFace build. -
Fixes from 2026-10-16/17, found while designing spec 023 and using the server daily: - API keys were written to the log (security): request logging put every request's headers,
Authorization: Bearer va_…included, intologs/api_server.log, and auth failures logged 10–16 key characters. Headers are no longer logged; a key appears only as a sha256 fingerprint (c2edfbd). Anyone with logs from before this should delete them and make a new key. - A terminal that says what matters (c2edfbd): the server prints ready, each job started/finished/failed (with duration, results folder or reason), pipeline installs and stopping; per-request lines and library warnings go tologs/only. A viewer calling without a working key gets one plain hint instead of a stream of 401s. - Hugging Face token from the viewer, Settings or the CLI, no restart (spec 031:0ffd059,ab400a7,fdc5346,9e8b530): an admin saves it to a 0600 file beside the database, checked with Hugging Face first and never returned;videoannotator settings huggingface-tokendoes the same from a terminal. - Timeline colours that don't mislead (41a38d2): every speaker had the same colour (the hue came from the ID's first letter, always "S"); hues now come from each value's rank. Timeline switches with no lane behind them are gone. - The version on every docs page, andvideoannotator --version(79582bd).
Not in this phase: a visual redesign. It is v1.7.0's first phase (design in
specs/023-viewer-overhaul, built by specs 026–029).
Problem: each pipeline writes its own native format (COCO, WebVTT, RTTM, scene JSON). That is right for provenance and wrong for analysis: to get one table in R, a researcher has to write a parser per pipeline.
Solution:
- Tidy export: one row per event (video, pipeline, track or person, start, end, label,
value, confidence, model), as CSV and Parquet, per job and per batch or dataset, from the
API, the CLI and a viewer button. This table also feeds the corpus view (Phase 6) and agents
(Phase 4).
Done 2026-10-16 (spec 025,
specs/025-results-out/): one long table, a row per event and measure, 19 fixed columns (contracts/table.md, format version 1). Built from the result files and their provenance bysrc/videoannotator/exporters/, sovideoannotator export table <folder>works on any results folder with no server, and the API and the viewer's Export menu give the same rows. Parquet is the optionalparquetextra. - ELAN export:
.eafwith one tier per pipeline track. The viewer already parses.eaf(src/lib/parsers/elan.ts), so this closes the loop with the tool many labs already code in. Done 2026-10-16: EAF 3.0, validated against ELAN's schema in tests; built from the table's rows; frame detections become "in view" spans. The viewer contract test opens the export and a copy edited in ELAN. Not yet opened in ELAN itself by a person (spec 025 quickstart). - Methods paragraph:
GET /api/v1/jobs/{id}/methods, a CLI command and a viewer button. Returns the models, versions, weight revisions, parameters and citations as prose plus BibTeX, ready for a methods section. Needs the provenance fields from the reproducibility item inroadmap_v1.8_to_v2.0.md, pulled forward: model revision SHA, prompt SHA-256, quantisation. Done 2026-10-16, for jobs and runs (/batches/{id}/methodstoo). Citations live in each pipeline's metadata (citations:). Found on the way, not fixed: some metadata defaults differ from the pipelines' code defaults (scenemin_scene_length1.0 vs 2.0, Whisper's punctuation strings), so the paragraph lists those settings as changed. - Two worked analysis notebooks, one R and one Python, reading the tidy export of the demo
video. Done 2026-10-16:
examples/notebooks/; the Python one runs in CI, the R one was run on R 4.3 and is checked before each release (tests/manual/results_out_r_notebook.md).
→ Release candidates: rc1 was cut on 2026-10-14 to test the install before Phase 3 landed; rc2 carries Phase 3.
-
specs/026-pipeline-guide: each pipeline's metadata says what it does in behavioural terms, splits its settings into behavioural (shown) and technical (collapsed), and gives its homepage, published benchmarks and citation; the viewer, the API andvideoannotator pipelines describeshow the same guide. Groundwork for v1.7.0's run setup and model cards; useful on today's Pipelines page. Done 2026-10-17: all 8 pipelines have a guide with sourced figures (spec 026 research.md, "Sourced figures"); two settings the code never read are removed. - Remove the DeepWiki links (they point at the archived viewer repo) and "VAV" from Getting started. Done 2026-10-17: the footer's and Getting started's docs links go to the docs site.
Each on the upstream evaluation plus a demo-clip check (principle: no model change without a reason on record). Done before the public release so its outputs are stable.
Spec: specs/030-model-defaults (specified and clarified
2026-10-17; next: plan, tasks). Library upgrades are checked on the demo clip on their own, before
the model changes (FR-002). Each switch needs a real-model run on a GPU and a deliberate
re-baseline; spec 026's benchmark entries change with each default. Backward compatibility is
waived for this change (Principle V exception, recorded in the spec): old diarization and pose
models stay reachable as a setting, the old speech runtime goes.
- Speech:
openai-whisperbase→ faster-whisper withlarge-v3-turbo, in two steps (runtime withbase, then the model). openai-whisper is removed (Q3), so v1.6.0rc1 is the last version with it. Done 2026-10-18: the runtime alone gave the identical transcript; turbo rewords 3 of the demo clip's 4 cues (hears "Peek-a-boo!"). A translate request with turbo now fails clearly. - Diarization: pyannote
speaker-diarization-3.1→speaker-diarization-community-1(Q2): lower DER on 10 of 12 published sets, and one model licence to accept instead of three. Switched 2026-10-18. Demo clip: one speaker where 3.1 found two; listening showed 3.1 had split the mother's sentence, so community-1 stays. - Person:
yolo11n-pose→yolo26n-pose(COCO-pose mAP50-95 50.0 → 57.2). Ultralytics stays for v1.6.0. Done 2026-10-18 (ultralytics 8.4 first: same output; it had moved the analytics switch-off, now followed). - Face: age and gender off by default, and their models neither downloaded nor run when off.
Today
_deepface_actions()(pipelines/face_analysis/face_pipeline.py) runs all three models whenever any is on: a bug, fixed here. DeepFace's own expression model stays (Q1), described in the guide as trained on adult faces; replacingface_analysisis later work. The detector is already RetinaFace (replaced the Haar cascade earlier in v1.6.0); its reason goes into the metadata and guide. Face analysis joins the demo-clip baseline if RetinaFace finds faces in it. Done 2026-10-18: 125 MB first download instead of 1.2 GB; RetinaFace finds 19 faces in the demo clip, so face analysis joined the baseline. - OpenFace 3:
openface-testconfirmed as the distribution CMU names (dormant since 2025-06, non-commercial licence); the==0.1.13pin stays. Spec 030 writes this into its metadata (FR-026). - Caspar: ask LAAC-LSCP about VTC 2's licence (the repository has none). Open; record the date asked. VTC 2 is a v1.7.0+ candidate whatever the answer.
- Moved to v1.7.0 (2026-10-17, spec 030 Q2): the half-day Sortformer spike. It informs lifting the torch cap later, not this release's default. Audit §8.
Benchmark: not in v1.6.0 or v1.7.0 (was Phase 5b).
Dropped 2026-10-14 (Caspar). The pipelines wrap off-the-shelf models that come with their own published evaluations, and a proper benchmark is a project in itself: no human-coded infant video of our own, and the open sets either cover speech only (HomeBank's public VanDam corpora, CC BY-NC-SA 3.0) or can't be redistributed (BabyView, on Databrary). Model cards (Phase 7) cite the upstream evaluations and say plainly that few were trained or tested on infants. Labs can check a pipeline against their own ELAN codes in the viewer's Compare page. Revisit after the pilot.
-
v1.6.0rc2with Phase 3; then Phases 3b and 5c. - Tag
v1.6.0, GitHub release, Zenodo DOI,CITATION.cffupdated. No announcements. - Release notes say plainly that the viewer is being redesigned for v1.7.0, the public release.
The pilot, Phase 4 (Agents), Phase 5a (Connectors) and 5d (New pipelines), Phase 6 (Corpus
Overview, now spec 029's Videos page), Phase 7 (Docs and Tutorials) and the Launch. See
roadmap_v1.7.0.md.
- The viewer overhaul (v1.7.0).
- Publicity, the pilot, the tutorial and screencast (v1.7.0, written against the new viewer).
- Plugin ecosystem: entry-point discovery,
DispatcherABC,videoannotator-utils(roadmap_v1.8.0.md). - Remote and HPC dispatch (
roadmap_v1.8_to_v2.0.md).
- Changing viewer code cannot ship a stale bundle: CI fails on a mismatch (Phase 0,
scripts/build_viewer.sh --check). - Every version constraint in
pyproject.tomland the viewer'spackage.jsonhas a recorded reason in the dependency audit, and the release supports Python 3.13 (3.14 as soon as TensorFlow does). - Every change of default model has its reason in the CHANGELOG (Phase 5c).
- The viewer has zero TypeScript errors (checked 2026-10-16).
- The viewer has no placeholder pages.
- Nothing leaves the user's machine unless they switched on a hosted backend.
Moved to v1.7.0: model cards for every default model; everything the viewer does available from
the CLI with --json and through MCP; the pilot's measures.
Last Updated: 2026-10-18 (Phase 5c done) Target Release: as soon as Phases 3b and 5c are done Status: Release candidates — Consolidation Release