Skip to main content
decktalk verify is the last stage and the only one that makes no artifact. It opens the finished mp4 and measures it against the clock the earlier stages promised it would keep. It changes nothing a later run reads: the only files it touches are its own events file and the build lock it holds while it measures. This is the page to read when a number it printed is not the number you expected. Five things are measured. Every section must open on a real picture past its dip to black. The narration must be quiet in the window before each cut, so no cut lands on a word. A section that declares itself seamless must open on the picture the section before it ended on. Every cue must change the picture, by enough to be seen, within the offset limit of the word it was promised to, with every declared motion around it masked out. And every declared motion must depart and arrive on its own lane within that same limit of its words. Beside those five it repeats what every recording log already judged, rather than measuring it again, so a page that threw or an equation that was never typeset is still a finding after the build that recorded it, and after a build somebody else recorded.

Running it

--section N narrows every measurement to those sections, taking 3, 3,5 or 7-9 and repeating. --fail-on and --allow decide what fails the run, as Which findings fail a run says. decktalk verify --help prints every flag. The stage inside decktalk build is the same call, so a build measures everything a bare verify would. Three refusals come before any measurement. A film that is not there is NOT_BUILT, and so is a film that carries no placements file and has no section files to read a shape from. Both say to run decktalk assemble first. A film whose recorded page sections have no recording log under build/recordings/ is NOT_BUILT too, because without the logs verify would repeat none of the recorder’s findings and measure none of the motion, so a film moved to another machine takes build/recordings/ with it. A section the film filled with black was never recorded and needs none.

What it prints

The table holds one row per cue that was measured all the way to an onset. A cue that was skipped has no row, and neither has one that changed nothing or whose reveal never stepped. Each of those raises a finding of its own, except a cue that cues.json opted out of, which is silent by design. The starts, the cuts and the seams print no table at all: their numbers are in decktalk verify --json, and what is wrong about any of them is a finding. A deck that declares motions prints a second table under the first, with one row per landing that was measured. Motions gives its columns. change_percent is in the JSON and not in the table: it is how much of the picture the reported probe found changed.

The frame of reference

Every second on this page is a second in the finished film. verify reads the film’s own placements, build/final/placements.json, for where each section starts and how long the whole film runs, because that file is the film’s own record of its shape. With no placements file it adds up the section files instead and says on its stream that it did. Shares are measured on a quarter-size copy of the frame, which is verify.probe_width by verify.probe_height, both derived from the frame size. A share is the percentage of pixels whose luma differs by more than a level, so the comparison reads brightness and never colour: a reveal that changes only colour at the same brightness reads as no change, and the way to keep such a cue is "verify": false in cues.json.

Section starts

For each section that the film plays, verify reads the frame verify.after_dip_seconds past the section’s start and takes its brightest luma. A frame whose brightest pixel is at or under verify.black_max_luma is black, and the section opens on nothing. The probe sits a fixed distance past the start whatever the dip is, so a dip longer than the default needs after_dip_seconds raised past it. A clip section gets a row like any other.

Cuts

For each section that has a take, verify measures the level of the narration track alone over the window before the section’s narration ends. The window is verify.cut_window_seconds or the take’s own span, whichever is shorter, and the level is an RMS. Above verify.cut_max_dbfs the cut is falling on a word. The narration track is read alone on purpose, so music or a sound effect at a boundary is never taken for speech. A clip section carries its own audio and has no row. Each row also carries a step, which is how far the level of the finished film jumps across the cut, measured over a twentieth of a second on each side. It is reported for a reader and no limit is held against it.

Seams

A section that sets seamless = true in decktalk.toml claims it opens on the picture the section before it ended on. verify compares the outgoing section’s last whole frame with the incoming section’s first, both taken outside any dip so that a fade through black is never read as a jump. Over verify.cut_change_max_percent of the frame changing is a visible seam, and verify raises CUT_POP against it. A seam is only ever a seamless cut, so a section that does not set seamless has no row here and its cut is never judged for the picture. The recorder steps the section clock, so a seamless section opens on the very frame the section before it ended on, and the first frame alone decides CUT_POP. A picture that arrives even one frame late is a frame a viewer sees jump. When the two do not match, the next three frames are read as well, so the row can say how late the outgoing picture arrived: drift_seconds is how far past the cut it was found, in seconds, and it is zero when the seam matched straight away.

Cues

A cue check asks one question: did the picture change because of this cue, and did it change when the word was said. It is answered in stages, and each stage has its own way of failing.

Reference frame

The reference frame is the picture before the cue, and every probe is compared against it. It sits the whole reference lead before the cue.
GUARD_FRAMES is 1.5, which is half a frame of rounding guard on each side of the window the offset limit allows, and video.fps is the rate the recorder steps the section clock at, so the guard is always one and a half of the film’s own frames. The lead has to be at least that wide: a reveal that lands at the early edge of the offset limit would otherwise already be showing in the reference frame, and the check would measure the change only once the reveal had settled. verify.reference_lead_extra_ms is the escape hatch for a deck whose reference frame is still inside its own reveal. It defaults to 0 because the lead is already as wide as the limit implies. The reference may not sit inside the section’s fade-in, where the picture is still coming up from black, and it must sit at least one frame before the cue. When no frame satisfies both, the cue is skipped with at_section_start as its reason.

Probes

A probe is the frame a fixed delay after the cue. verify.probe_delays_seconds names the delays, 0.7 and 1.5 seconds by default, and the later one is what catches a slow reveal. A probe that would fall within 0.05 seconds of the section’s end is dropped, and a cue whose probes are all dropped is skipped with too_close_to_end as its reason. Each probe gets a control share, which measures motion that has nothing to do with the cue. The control is the quieter of two back-to-back spans of the probe’s own length that end at the reference frame. Steady motion, such as a camera push, shows in both spans. An earlier reveal that is still settling shows in only one, so taking the smaller of the two leaves it out. A span that would start before the section’s floor is dropped, and with both dropped the control is zero.
A time axis around one cue. A dashed line marks the reference frame a short way before the cue. Bars run from it forward to two probes after the cue, each labelled with the share of the frame that changed, and the earlier probe is marked as the reported one. Two equal control spans run back from the reference, each labelled with its own share.A time axis around one cue. A dashed line marks the reference frame a short way before the cue. Bars run from it forward to two probes after the cue, each labelled with the share of the frame that changed, and the earlier probe is marked as the reported one. Two equal control spans run back from the reference, each labelled with its own share.

Probes run forward from the reference frame to each delay after the cue, and two back-to-back control spans of the same length run back from the reference.

The figure shows the arrangement rather than this release’s numbers, which were measured on an earlier one. Run decktalk verify --json for what your own film measures.

The neighbour allowance

Another cue of the same section can spoil a probe. Its own reveal would count as this cue’s change if it can fall inside the probe’s span, or as motion that was always there if it can fall inside every control span that is measured. The allowance that decides whether a neighbour reaches into a span is deliberately not symmetric.
  • Backward it is the reference lead, which is verify.cue_offset_max_ms / 1000 plus the guard frames over the capture rate plus verify.reference_lead_extra_ms / 1000. A reveal may legally land that early and still pass, so the check has to assume a neighbour might.
  • Forward it is the neighbouring effect’s own declared span, taken from the attribute registry through the catalog the page published and scaled by motion.scale. An effect stops moving when its own animation ends, and not a fixed distance later.
One symmetric constant cannot serve both directions. It is too short for a draw, which keeps moving for most of half a second, and too long for a cut, which is over the instant it fires. Reading the span the effect itself declares is what makes a tight passage of cues measurable at all. A neighbour closer to this cue than the backward allowance is not a neighbour. It cannot be told apart from the cue being measured, so it is part of the same reveal. When a probe is spoiled, it is fitted to the longest shorter delay that nothing spoils, down to one frame past the offset limit, so a reveal at the late edge of that limit still shows. Two probes fitted to the same delay count once. verify says on its stream which cue was fitted and to what, as a detail line that -v prints, because a fitted probe is how a close pair is measured and not a problem.

Change test

Both shares are measured at verify.probe_diff_luma. A cue that fails the test raises CUE_NO_CHANGE and nothing further is measured for it. A cue that passes by less than the factor raises CUE_THIN_CHANGE, which is a warning: the cue did pass, and a slightly smaller reveal or a little more motion around it would not have been measured at all. Setting verify.thin_change_factor to 1 turns that second opinion off. The change_percent in the JSON is the reported probe’s share, which is not always the largest share measured, because the reported probe is chosen on margin.

PAGE_THIN_DRAW

decktalk check runs the same arithmetic over two frozen frames rather than over the film, and it has one code of its own for the case a stroke makes. PAGE_THIN_DRAW is a warning, and it says that an entrance drawn as a stroke sweeps less of the frame than a change has to cross to be seen. A stroke that thin is a stroke nobody will see rather than a reveal that never happened, which is why it is not CUE_NO_CHANGE. It is measured in Python from the frozen frames and never raised by the page itself, because the floor it is read against is verify.changed_share_min_percent, a settings key no browser can read. video.crf moves it too, since the encoder’s own ringing competes with a thin stroke.

The onset

Once a cue has passed the change test, verify finds the frame the reveal began on.
  1. It measures the changed share of every frame from the reference frame up to the reported probe, each compared with the reference frame at verify.onset_diff_luma.
  2. It measures the same series again with both frames scaled to verify.block_width by verify.block_height, which is one pixel per H.264 transform block.
  3. The onset is the first frame after the reference whose share rises by at least verify.onset_rise_points over the frame before it, and whose block share is above zero.
  4. When no frame rises that much, the onset is the first frame after the reference whose share passes a floor, and whose block share is above zero. That floor is the larger of verify.onset_rise_points and the largest share measured up to the point that sits the offset limit plus half a frame before the cue, so a picture that was already moving before the cue cannot pass for the reveal.
  5. When no frame does either, the cue raises CUE_NO_ONSET and has no row in the table.
A reveal is a step: between two consecutive frames the changed share jumps. Motion that was always there, such as a curve still drawing, is a slope that grows a little every frame and never jumps, which is why the first test looks for a rise rather than for a level. The fallback catches a reveal that grows slowly, such as a number counting up. The block series is what keeps the encoder out of the answer. The x264 encoder leaves a little ringing on a still picture in the frame or two before a change. Ringing is as much lighter as darker, so it cancels over a block and leaves the block share at zero, while a real reveal changes whole blocks and its first frame has a block share above zero. The onset may sit before the cue.
A row of frames one frame apart, starting at the reference frame before the cue. Each frame is labelled with the share of the picture that changed against the reference. The first frames read zero, and the frame just after the cue steps up to a larger share and is outlined as the onset. A shaded band marks the offset limit on each side of the cue.A row of frames one frame apart, starting at the reference frame before the cue. Each frame is labelled with the share of the picture that changed against the reference. The first frames read zero, and the frame just after the cue steps up to a larger share and is outlined as the onset. A shaded band marks the offset limit on each side of the cue.

Consecutive frames from the reference frame onward, each with its share against the reference, and the first frame that steps up is the onset.

The figure shows the shape of the scan rather than this release’s numbers. Its shares are measured at verify.onset_diff_luma, which is a much lower luma difference than the change test’s verify.probe_diff_luma, so the same cue reads a different share here than in change_percent.

The word the onset is measured against

After a build on placeholders, the narration is click audio with a click at every word start and one where the last word ends, so the film’s own audio can be measured against its own picture. verify searches verify.click_search_seconds on each side of the cue’s second, inside that cue’s section alone, and takes the loudest sample. A sample under verify.click_floor_dbfs is not a click, and DeckTalk generates its own click at -24 dBFS, so a floor at or above that finds nothing at all.
  • When a click is found, it becomes the row’s Spoken, and Offset is the onset less the click. That measures the picture against the film’s own audio, so it covers the recording, the trim at the start of the section clock, the cut, the join and the mix in one number.
  • When no click is found, and after a voiced build, Spoken is the cue’s own second in the film and Offset is the onset less that.

Limits

The half frame is there so a limit written in frames is not failed by its own rounding. The sound has its own wider limit because an mp3 frame smears a transient across about 26 milliseconds and the encode adds its own, which is why verify.av_offset_max_ms may not be set under 80. Both limits raise CUE_OFF, and each message says which of the two it is. A whole run that lands a frame or two late is usually a slow machine rather than a bad deck. Build in CI and offline shows how to widen a limit on one runner.

Motions

A page declares everything that moves for longer than a moment, as a motion, a loop or the region a frame hook draws in, and a render section’s program may write the same declarations as its landings. The recorder keeps one lane for each in the recording log, with the box it swept and the seconds it moved, and verify reads the lanes from there. The lane report gives their fields.

The mask

Every cue is measured with every lane alive anywhere from the start of its control spans to its last probe masked out. The mask is the union of those lanes’ boxes, scaled to the probe size, and it is taken out of the changed share, the control share and the onset scan alike, so a ring turning in a corner never reads as a reveal starting early. masked_percent is the share of the frame the mask covered. Over verify.motion_mask_max_percent, which is 40 by default, verify raises MOTION_MASK_LARGE. It is a warning, because the cue was still measured, on the part of the picture that was left.

Landings

A cue that is only a motion’s departure or arrival, where the page reveals nothing else, has no step in the frame to read. It is skipped in the cue rows as motion_landing and measured on the motion’s own lane instead. A cue where the page also reveals something is measured both ways: as a reveal with the motion masked, and as a landing on the lane. Inside the lane, frame by frame, the motion counts as moving while more than verify.motion_floor_percent of the moving element’s own area changes from one frame to the next, which is 0.5 percent by default. The comparison is made at verify.onset_diff_luma.
The landing table holds one row per landing, in film order.
The limit is verify.cue_offset_max_ms plus half a frame, as it is for a cue. An arrival that came to rest early is MOTION_NO_EDGE rather than MOTION_OFF, because the fix is the ease rather than the cue: a motion that eases to rest has no edge on its word, and firm gives it one. A landing on a cue that cues.json marks "verify": false has no row and no finding, which is how a settle or spring arrival is written.

A render section

A render section’s lanes are the landings its program wrote. When it wrote none, nothing says what moves in its picture, so every cue of that section is skipped as rendered and verify says once on its stream that it measured none of them. Such a cue is never passed.

Every finding verify can raise

verify also raises again every finding the recording logs hold, such as RECORD_TRUNCATED, PAGE_KATEX_ERROR and every warning the runtime reported while it recorded. Those are read from the log rather than measured, which is why recording that section again is the only thing that clears one. Each finding has a page of its own under /reference/findings/, and Configuration is the same index in reverse. decktalk schema prints every code with the keys that decide it as JSON, in its decides list.

Skipped cues

A skipped cue has no shown, no offset_seconds and no change_percent, and its skipped field says why it has none. Two of the eight reasons carry a finding, and the table says which.

The JSON

decktalk verify --json prints one flat object with the four reserved keys, run, and eight fields of its own.
A skipped cue has null in shown, offset_seconds and change_percent. A measured cue has null in skipped. What is wrong is in findings, never in a row, so a reader dispatches on a finding’s code and reads a row for the number behind it. decktalk schema verify prints the whole result model.