Skip to main content
decktalk verify checks the final mp4 in three ways: section starts, cuts, and cues. A section that sets carries_previous adds a fourth check, cut continuity. This page defines each measurement, limit, and result, in the order that verify computes them.
Every field named on this page is a key under [verify] in Configuration, unless the page names another table. The defaults assume 25 fps, so one frame lasts 40 ms.

Which checks run

The verify stage inside decktalk build checks section starts, cuts, and cut continuity, and no cue. decktalk verify checks all four.
  • With no cue named, verify checks every cue in beats.json, in section order and then cue time.
  • With no cue named, verify skips a cue that cues.json marks "verify": false.
  • Named cues run in the order given, positional cues first, then each --cue.
  • A named cue runs even when cues.json marks it "verify": false.
  • --only N keeps only the cues of section N, named cues included. A named cue outside --only gets no row.
  • Section starts, cuts, and cut continuity are always checked, whatever --only names.
A named cue has the form SECTION:CUE, such as 8:7.1checked. SECTION is the section number, not the scene number. Section 8 of the scaffold plays scene 7. A name that does not start with a section number is an error (malformed check), and the command exits 1.

Section starts

The start check confirms that each section opens on a visible frame.
A section’s start is the total length of the build/out/NN-section.mp4 of every earlier section in decktalk.toml. The check reads one frame after each start. verify reads only the sections in decktalk.toml. A file that matches NN-section.mp4 but belongs to no section, such as one left after you renumber sections, is ignored, and verify warns about it.
  • YMAX is the brightest luma in the frame, and YAVG is the average. A mid-gray slide with no pixel above 60 reads BLACK.
  • The probe sits 0.2 s after the start, whatever the dip length. The default [transition] dip_seconds of 0.15 s rounds to 4 frames, 0.16 s. If you set a longer dip, raise after_dip_seconds too.
  • A clip section has a row too. In the sample, sections 05 and 07 are the scaffold’s clip sections, which play slates.

Cuts

The cut check confirms that the narration is quiet just before each cut.
cut at is where the section’s narration ends in the final mp4. The check measures the narration track in the window before it.
  • The check reads narration.mp3 alone. An underscore or a sound effect at a cut does not count.
  • A level of exactly -40 dBFS passes.
  • A clip section has no row, because it has no narration. In the sample, sections 05 and 07 have none.
  • A page section’s hold_seconds comes after its cut at. In the sample, section 09 holds its last slide from 255.22 s to the end of the video.

Cut continuity

The continuity check confirms that the cut into a section that sets carries_previous = true in decktalk.toml does not show. It runs only for those sections.
cut at is the section start in the final mp4. chg % is the changed share of two frames at diff_level: the previous section’s last frame before any dip, and this section’s first frame after any dip.
  • The frames sit outside the dip, so a fade through black is never a pop. A dip still shows on screen, so leave the pair out of [transition] dips for a cut that should not show.
  • The default of 0.1 matches min_changed_percent, so any change that would pass as a reveal is a pop.
  • A section with no NN-section.mp4, or whose previous section has none, gets no row.
  • POP AT CUT is a certain finding, so decktalk verify and decktalk build exit 1 on it.

Cues

The cue check confirms that the picture changes on each cue, at the cue time.
Each column maps to one term. The terms below come in the order that verify computes them. Note: The table prints the a/v column only when at least one row has an a/v value. The JSON silent key says whether the build was silent.

Cue time

The cue time is the start of the cued word plus the offset key. It is rounded to hundredths, in seconds after the section start.
  • $start has a word start of 0.0 s. $end has the end of the section’s last spoken word.
  • build/audio/beats.anchors.json keeps each word start without the offset key, also rounded to hundredths.
  • A $start cue with a negative offset key keeps its negative cue time.
Example: section 8 starts at 228.28 s, and 8:7.1checked has a cue time of 10.03 s. Its at is 238.31 s.

Changed share

The changed share of two frames is the percentage of pixels whose luma differs by more than a level. verify scales both frames to probe_width by probe_height first, which is 480 by 270.
  • Every share is on a 0 to 100 scale. At 480 by 270, a share of 0.1 is about 130 pixels.
  • The share compares luma only. A reveal that changes only color at the same brightness reads NO CHANGE. Mark such a cue "verify": false in cues.json.
  • Probes and control shares use diff_level (40). The onset scan uses onset_diff_level (12). It also reads a block share at block_width by block_height (240 by 135). Onset and offset says why.
  • A pixel counts only when its difference is strictly greater than the level.
Example: the chg % of 0.83 for 8:7.1checked is about 1,070 of 129,600 pixels.

Reference frame

The reference frame is the picture before the cue. Every probe is compared with it.
  • [verify] lead_seconds has an effect only above (max_offset_frames + 1.5) / fps, which is 0.14 s at the defaults.
  • [verify] lead_seconds is not the sidecar’s lead_seconds. The sidecar value is the recorder’s wall-clock wait before narration t=0.
  • The lead keeps every frame inside the offset limit after the reference, so an early reveal is not already in it. A reveal earlier than the reference frame usually reads NO CHANGE.
  • With [transition] page_fades_in = true, the default, the cut adds no fade-in to a page section. The floor of a page section is then its first frame.
  • A clip section gets the dip when a dip comes before it. So does a page section when page_fades_in is false.
  • A row is REFERENCE_CLAMPED when its cue time is under 0.04 s with no fade-in, or under 0.20 s with the default fade-in. A named cue is still skipped.
  • The reference time is at least one frame before the cue. The reference frame can be closer when the cue time is off the frame grid.
Example: for 8:7.1checked, the reference time is 238.31 - 0.14 = 238.17 s. The reference frame is the next frame on the 40 ms grid, at 238.20 s. That frame is 110 ms before the cue, as the onset figure shows.

Probes

A probe is the frame a fixed delay after the cue. verify takes one probe for each delay in probe_delays.
  • The section end is the next section’s start in the final mp4, or the video length for the last section.
  • The later probe catches a slow reveal.
  • The row is TOO_CLOSE_TO_END only when every probe is dropped. At the defaults, that is a cue less than 0.75 s before the section end.

Control share

The control share measures motion that has nothing to do with the cue. Each probe gets its own control share.
A time axis around cue 8:7.1checked. A dashed line marks the reference time, 0.14 s before the cue. Bars run from it to probes 0.7 s and 1.3 s after the cue, which changed 0.83% and 0.83%. The 0.7 s probe is marked reported. Its two back-to-back control spans of 0.84 s run back from the reference, and each changed 0.00%.A time axis around cue 8:7.1checked. A dashed line marks the reference time, 0.14 s before the cue. Bars run from it to probes 0.7 s and 1.3 s after the cue, which changed 0.83% and 0.83%. The 0.7 s probe is marked reported. Its two back-to-back control spans of 0.84 s run back from the reference, and each changed 0.00%.

Probes run forward from the reference, and two back-to-back control spans run back from it.

  • Each span compares only its first and its last frame.
  • A span that starts before the floor is dropped. If both spans are dropped, the control share is 0.
  • Steady motion, such as a camera push, shows in both spans. An earlier reveal that is still finishing shows in only one, so the smaller share leaves it out.

Close cues

A cue close to another cue gets probes that fit the gap between them. Otherwise the other cue’s reveal would count as this cue’s change, or as motion in every control span.
  • A probe that is not spoiled is used as it is. A cue whose probes are all unspoiled is measured exactly as the sections above describe.
  • A fitted probe has shorter control spans, because each span still lasts as long as the probe’s span.
  • Two probes fitted to the same delay count once. The probes are then taken in order of delay.
  • If no probe can be fitted, verify uses probe_delays as they are.
  • A cue within the reference lead of another is part of the same reveal, so it is not a neighbor.
  • Every cue of the section in beats.json can be a neighbor, including a cue that the run does not check.
  • verify logs another cue is close, so the probes are fitted to <delays> s after the cue for each such cue.
Example: a count reveals a box on each of four cues, at 4.42, 5.11, 5.62, and 6.18 s. The last cue’s reference time is 6.04 s. Its 0.7 s probe has control spans from 5.20 to 6.04 s and from 4.36 to 5.20 s. The 5.62 s window overlaps the first span, and the 4.42 s window overlaps the second, so the probe is spoiled. The probe is fitted to 0.14 s. Its first control span, from 5.76 to 6.04 s, clears the 5.62 s window, which ends at 5.76 s.

Change test

The change test decides whether the picture changed because of the cue.
  • If the reported probe fails the change test, the row is NO CHANGE.
  • A row that passes by a thin change reads THIN CHANGE? instead of changed. The change is thin when the changed share is below thin_change_factor times min_changed_percent, or the margin is below thin_change_factor times min_margin_percent. At the default factor of 3, both limits are 0.3.
  • THIN CHANGE? is an uncertain finding. The cue passed, but a slightly smaller reveal or a little more motion would fail it. verify exits 1 on it only with --strict. Set thin_change_factor to 1 to turn it off.
  • The chg % and ctl % columns show the reported probe, which is not always the probe that changed most.
  • min_changed_percent applies to the raw changed share. A camera push raises that share too.
Example: 8:7.1checked has a changed share of 0.83 and a control share of 0.00. Its margin is 0.83, so it passes, and the change is not thin. The next cue, 7.1back, fires 1.44 s later, so verify fits the 1.5 s probe to 1.3 s, as Close cues describes. Both probes have a margin of 0.83, so verify reports the earlier 0.7 s probe.

Onset and offset

After a cue passes the change test, verify finds its onset, the first frame of the reveal.
  1. verify measures the changed share of each frame from the reference frame up to the reported probe. It compares each frame with the reference frame, at onset_diff_level. It also measures a block share of each frame, the same comparison with both frames scaled to block_width by block_height (240 by 135).
  2. The onset is the first frame after the reference whose share rises by at least onset_percent over the frame before it, and whose block share is above 0.
  3. If no frame rises that much, the onset is the first frame after the reference whose share is above a threshold, and whose block share is above 0.
  4. If no frame is above the threshold, the row has no onset.
Seven frames 40 ms apart around cue 8:7.1checked. The first is the reference frame, 110 ms before the cue. The frames at 70 and 30 ms before the cue changed 0.00%. The frame 10 ms after the cue changed 1.24% and is outlined as the onset. A shaded band spans 80 ms on each side of the cue.Seven frames 40 ms apart around cue 8:7.1checked. The first is the reference frame, 110 ms before the cue. The frames at 70 and 30 ms before the cue changed 0.00%. The frame 10 ms after the cue changed 1.24% and is outlined as the onset. A shaded band spans 80 ms on each side of the cue.

The outlined frame is the onset, 10 ms after the cue and inside the 80 ms offset limit.

The figure’s shares use onset_diff_level (12), so the onset frame reads 1.24%. The chg % column reads 0.83%, because the change test uses diff_level (40), at the probe 0.7 s after the cue.
  • A rise is in percentage points of share. At 480 by 270, 0.01 points is about 13 pixels.
  • At 240 by 135, each pixel averages an 8 by 8 block of a 1080p frame, the size of an H.264 transform block. The x264 encoder leaves a little ringing on a still picture in the one or two frames before a change: a few pixels, up to about 15 luma levels apart at 480 by 270. Alone, that ringing read as a reveal 40 to 100 ms before it happened, while the recording showed the reveal on time. Ringing is as much lighter as darker, so it cancels over a block and leaves the block share at 0. A reveal changes whole blocks, so its first frame has a block share above 0.
  • A reveal changes the picture in a step. Steady motion, such as a camera push or a line that was already drawing before the reference, grows a little every frame. That motion does not count as an onset, but it can change frames before the onset.
  • The onset threshold is far below min_changed_percent on purpose. A fade starts on its cue but takes many frames to change 0.1 percent of the pixels.
  • The fallback catches a reveal that grows slowly, such as text that types in. At the defaults, its window ends 100 ms before the cue.
  • The onset can come before the cue time.
  • A cue with no onset reads changed. Its offset column shows -, its a/v is blank, and neither limit applies. No NO_CLICK reason is set.
Example: 8:7.1checked has an offset column of +10 ms, so its onset frame is at 238.32 s.

The a/v value

After a silent build, verify also compares the picture with the sound in the final mp4. The click track of a silent build has a click at every word start.
  • The word start comes from beats.anchors.json, so the offset key does not add to a/v. An a/v of 0 means the picture changes one offset key after the click.
  • The offset column compares the picture with the cue time in beats.json. The a/v value compares the picture with the audio in the final file.
  • An a/v near 0 shows that these steps kept sound and picture together: the recording, the trim at narration t=0, the cut, the join, and the mix.
  • The search window never reaches past the cue’s own section, so the audio of a clip next to it is never the click.
  • A sound effect or an underscore that is louder than the click inside the window becomes the click. A decktalk build --silent --nomix leaves them out of the mix.
  • The clicks stay at -24 dBFS, because a silent build skips the loudness pass.
  • If no sample reaches the level, the row gets the reason NO_CLICK, and its a/v is blank. The row keeps its offset result.
  • NO_CLICK is usual for a $end cue whose last word is longer than click_search_seconds. $end anchors on the end of the last word, and clicks sit on word starts. A shorter last word gives an a/v that is off by the word’s length.
  • A voiced build has no a/v. Its JSON silent is false, and every av_ms is null.
Example: 8:7.1checked has an offset column of +10 ms and an a/v of +3 ms, so its click distance is +7 ms.

Limits

Two limits turn a measured row into OFF CUE. Both apply to early and late values.
  • The a/v limit is one frame wider, because the click carries encoding jitter too.
  • The table prints only the result. Compare the offset column with the offset limit and a/v with the a/v limit to see which one failed.
  • A slower machine can put a reveal two or three frames late. Build in CI and offline shows how to widen the limits on one runner.
This example shows the format of an OFF CUE row.
This row is OFF CUE because its offset column, -90 ms, is past the 80 ms offset limit. Its a/v of -118 ms is inside the a/v limit.

Results

Each cue row gets its result from the first rule that matches.
  1. If the run names no cue and cues.json marks the cue "verify": false, the row is skipped with the reason OPTED_OUT.
  2. If a named cue is not in beats.json, the row is UNRESOLVED.
  3. If the section has no NN-section.mp4, the row is skipped with the reason SECTION_NOT_ASSEMBLED.
  4. If no reference frame fits, the row is skipped with the reason REFERENCE_CLAMPED.
  5. If every probe is dropped, the row is skipped with the reason TOO_CLOSE_TO_END. A cue close to another cue gets fitted probes first.
  6. If the reported probe fails the change test, the row is NO CHANGE.
  7. If the scan finds no onset, the row is changed, with - in the offset column and no a/v. A thin change reads THIN CHANGE? instead.
  8. If the offset column is past the offset limit, the row is OFF CUE.
  9. After a silent build, if no click is found, the row gets the reason NO_CLICK and keeps the result of rule 8.
  10. After a silent build, if the a/v is past the a/v limit, the row is OFF CUE.
  11. Otherwise, the row is changed, or THIN CHANGE? when the change is thin.
OFF CUE, NO CHANGE, UNRESOLVED, BLACK, SPEECH AT CUT, and POP AT CUT are certain findings, so decktalk verify exits 1. THIN CHANGE? is an uncertain finding, so verify exits 1 on it only with --strict. A skipped row never fails. Exit codes lists the codes for every command.

Skipped rows

A skipped row measured nothing. The table prints skipped, the reason, and a short note. Note: NO_CLICK is a reason on a measured row, not a skipped row. The a/v value explains it. A reveal that is too small or too slow for a frame difference, such as a thin arrow, can fail the change test. Mark such a cue "verify": false, and check it by eye with decktalk shots --step ID --cue CUE.

JSON fields

decktalk verify --json prints one JSON object on stdout. The measurements are under the verify key.
Note: Seconds are rounded to three decimals, and shares to two. A skipped row has null measurements.