The recorded tour¶
A narrated walkthrough of every page in the product, recorded against the running system with a live database behind it. Twelve chapters, seventy five scenes, 20 minutes and 28 seconds at 1920 by 1080, 50 MB.
It exists because the two earlier films are short. They show the system working; they do not explain what each surface is for, and they do not visit most of it. This one visits seventy two of the seventy three routes in the web application. The one it skips is the sign-in page.
What is real in it¶
Everything on screen. The browser talks to the API on port 8000 against the labeloxav database, so
every count, interval and empty panel in the recording is what the system actually held at the moment
it was filmed.
Every number spoken in the narration is read from that same database at record time and substituted into the script, so the voice cannot drift from the picture. A figure that cannot be read is spoken as unreadable rather than as zero, and the reason is printed in the run log.
The uncomfortable numbers are spoken too. The tour says out loud that 788 of 598,179 objects carry a human verdict, that 30,863 of 30,865 reviews have no timing, and that the multi-camera benefit is measured over six sessions and is therefore small. A tour that quotes only the flattering figures is a sales reel.
The data it was recorded on¶
Four recordings were ingested for it, and the corpus is measurably better for them.
| before | after | |
|---|---|---|
| measured ego poses | 0 | 154 |
| real LiDAR points | 0 | 18,000,000 |
| point clouds from a laser | 0 | 154 |
The three dashcam clips are the operator's own footage from Bengaluru. The fourth recording is KITTI drive
2011_09_26_drive_0005, 154 frames from Karlsruhe with a sixty four beam Velodyne on the roof, real
GPS and inertial measurements, and the rig's own calibration: focal length 721.54 pixels over a 1242 by
375 image. It was imported by scripts/import_kitti_raw.py.
Before that import every point cloud in the corpus was estimated from single camera depth, 453,983 points across 39 clouds, and no ego pose anywhere was measured. The three GNSS fixes the corpus held across 41,752 frames were the honest reason most three dimensional work in it was estimated.
What was then built on those recordings, all measured from the runs:
| stage | result |
|---|---|
| auto-labelling | 3,977 objects on the KITTI drive, 4,397 across the three dashcam clips |
| tracking | 123 tracks and 48 scenarios on the KITTI drive, 458 tracks across the dashcam clips |
| ego pose | 154 poses, all 154 measured, from GPS and inertial |
| cuboids from 2D | 2,089 across 154 frames, no frame failed |
| tracks lifted to 3D | 40 |
| occupancy | 154 grids, 739,309 occupied voxels |
The occupancy grids carry a caveat the builder prints itself and the tour does not hide: none of the occupied space carries a velocity from a track yet, so the grids are close to static.
Running it¶
.venv/bin/python scripts/demo_tour/postprocess.py --routes demo-tour-2026 # prepare the data
.venv/bin/python scripts/demo_tour/driver.py --check-pages # load every page, film nothing
.venv/bin/python scripts/demo_tour/driver.py --out site/demo/labeloxav-tour.mp4
--check-pages first, always. A broken route found there costs one line of output. Found during a
recording it costs the whole take, because the segments after it are already timed against audio that
has been synthesised.
--only scene_key,other_key re-records named scenes, and --assemble-only rebuilds the film from
segments already on disk.
The pieces:
narration.pyis the script. OneSceneper page, with the words, the route, and how long to hold. Page interactions live in a separateACTIONStable so that a selector which moves is a one line change rather than an edit inside a paragraph of narration.record.pysynthesises speech with piper, writes both subtitle formats, and assembles.driver.pydrives the browser, resolves the numbers, and records.postprocess.pyruns tracking, embedding, ego pose, the three dimensional lift and occupancy over the tour's sessions, so the later chapters narrate features over pages that have something on them.
The edited cut¶
scripts/demo_tour/produce.py turns the raw recording into a finished film: an animated intro, a card
for each of the twelve chapters, cross-dissolves between scenes, a progress bar, chapter marks, a music
bed that ducks under the narration, and an outro. It edits the footage already recorded and never touches
the running system, so the numbers spoken are still the ones read from the database on the day of
recording.
.venv/bin/python scripts/demo_tour/produce.py plan # scene list, captions matched, zoom per scene
.venv/bin/python scripts/demo_tour/produce.py all # builds .scratch/demo/pro/labeloxav-tour-pro.mp4
The interface sits in a rounded frame on a designed canvas rather than filling the screen. The first cut burned captions over the bottom of every page, covering the part being described; here captions live in the margin below the frame and the chapter and scene name in the margin above.
About a fifth of the recorded pages put a small panel in one corner of a large empty screen. Each scene is sampled at three moments, the region holding content is measured, and where that region is small the frame eases in on it after a beat. 24 of the 75 scenes are zoomed. The move interpolates the edges of the visible rectangle rather than a centre and a zoom level: the first version did the latter, and the view swung sideways part way through, cutting off the panel it was zooming towards.
Titles and cards are HTML rendered frame by frame by headless Chromium, with every animation paused and its clock set explicitly per frame, so the timing is exact on every run rather than depending on what a screen recorder happened to capture. Type is Space Grotesk, the web app's own typeface; the palette is sampled from the logo.
Audio stays lossless until the final encode. The film joins 89 clips, and AAC pads every stream to a whole frame, which left compressed would walk the voice out of sync by up to two seconds by the end. Measured on the finished film by cross-correlating six narration clips against the soundtrack at their scheduled times, from the first scene to the last, the worst offset is 0 ms.
The music is three tracks by Kevin MacLeod from incompetech.com: "Beauty Flow" under the intro and the closing chapters, "Space Jazz" from Review, and "Sincerely" from Measurement, each change a three second crossfade centred on a chapter card. They are licensed under Creative Commons Attribution 4.0, so the outro carries the credit, and any upload must repeat it in its description. The pipeline downloads them into the work directory on first run and levels each to the same loudness before mixing.
The mix was levelled by measurement because the film was produced without listening to it. Measured as ungated RMS, the music sits 14.2 to 17.8 dB under the voice across three different passages, and comes up about 4 dB for the intro and outro. The ducking is a gentle 4:1: at 14:1 the bed fell 30 dB under dense speech and swelled back in every pause, which is audible pumping. Integrated LUFS was the wrong instrument for this: its relative gate discards quiet passages, so the harder the music ducked, the louder that reading claimed it was.
| length | 21 minutes 10 seconds |
| loudness | -15 LUFS, peak -1.2 dBFS |
| chapter marks | 14 |
| size | 108 MB, over GitHub's 100 MB file limit, so the film is not committed |
Three decisions worth knowing about¶
The browser records itself. This machine runs a Wayland session, where an X11 grab of :0 sees
only the XWayland root and returns black frames, which is exactly what the first take produced.
Chromium writing its own video captures the page rather than the desktop, so it is pixel exact at
1920 by 1080, runs headless, and nothing else on the desktop can appear in the film.
Every scene length is decided, not observed. The narration is synthesised first and measured, the length is rounded up to a whole frame, and then the picture is cut to that many frames and the audio padded to the same value. Picture, voice and captions are all built from one number that each can represent exactly. Letting the capture decide its own length drifts them apart by a frame per scene.
The cut is by frame count rather than by -t, and that distinction is load bearing. A duration lands
exactly on a frame boundary by construction here, which is the one place -t is ambiguous: whether the
frame starting at that instant falls inside the cut is a floating point coin toss. Five segments of
seventy five came out a single frame short, and the film ended with the audio 339 milliseconds past the
picture. Cut by frame count it is 5.7 milliseconds, and assembly now refuses to run at all if any
segment is not the length the manifest claims.
Subtitles are burned from ASS, and the film carries no subtitle track. libass assumes a 384 by 288 play resolution for a format that carries none and scales everything it draws by the ratio to the real frame, so a font size chosen in pixels comes out nearly four times too big. The ASS file states the real resolution.
The first cut also embedded the SRT as a selectable mov_text track, on the reasoning that a burned
caption always shows while a soft one stays searchable. Played back, both appeared at once: the embedded
track is flagged default, so the player switched it on over the caption already in the picture and the
two texts overlapped at different sizes. There is no arrangement that keeps both without that risk,
because a player choosing its own subtitle source is exactly what a burned caption exists to prevent.
So the film carries one caption, and the SRT is written beside it as a separate file, in its own
directory. The directory is not tidiness: players auto-load a .srt that shares the video's name, which
reproduces the doubling with no track embedded at all.
Three defects the tour found¶
Preparing the data for it surfaced two bugs, both of the shape that survives a test suite: each took a branch only reachable when the interesting data existed, and every test exercised the other branch.
_gnss_rows read Frame.gnss, a PostGIS geography point, as though it were JSON. The query filters on
the column being non-null, so every row it ever returned raised, and the only sessions that appeared to
work were those with no satellite fixes at all. Measured ego pose had therefore never been written by
that module.
object_on_real_frame was a bare exists().where(...), whose FROM clause SQLAlchemy fills in by
auto-correlation. Against a query that also joins Frame it correlates the table outwards and leaves
the subquery with no FROM, which raises at compile time. embed_objects joins Frame to read the
image URI, so the one caller in production raised on every run while all six tests passed.
Both fixes shipped with tests checked against the old code.
Filming found a third, in the product rather than the pipeline. The console's Background panel
interpolated each agent run count straight into a template string, and several agents write a list
there, so every drift and lift row read findings [object Object],[object Object]. It had been that way
for as long as the panel existed and nothing threw or logged. The scene was re-recorded after the fix,
which is what --only and the manifest merge are for.
A fourth was in the narration itself. The ingest scene said "three real recordings, two dashcam clips",
which was true the afternoon the script was written and stopped being true when a third clip was
labelled. Both counts now come from the database like every other figure, and check() refuses a
sentence that begins with a placeholder, because a substituted count at the start of a sentence comes
out in lower case.