[Recording] Anchor per-speaker capture to a wall clock — tracks have never shared a session timeline #320
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: CRITICAL. Found in the August 2026 session lifecycle review (#319). This is the root cause of the scrambled chronology and misattributed actions reported after the 2026-08-11 session, and it affects every multi-speaker recording Quest Board has ever produced.
The defect
Per-speaker WAV tracks are not on a shared timeline.
PerUserPCMSink.write()appends decoded PCM only when a packet for that user arrives, and Discord clients stop transmitting entirely during silence (roughly five DTX hangover frames, ~100 ms, then nothing). Every pause is therefore deleted from that speaker's track, cumulatively, for hours. A track's duration ends up equal to that speaker's total talk time, not the session's wall-clock length.Whisper's timestamps are relative to the file it is given, so each track carries its own private, compressed clock.
merge_attributed_transcriptthen sorts segments from six mutually incomparable clocks onto one axis.The one piece of machinery that looks like a session clock is not one.
_session_bytesis documented atrecording.py:118as "Bytes on the first speaker's timeline" and advances only for packets whereuid == next(iter(self._writers))— one arbitrary user, whose own track also only advances while they transmit. Late speakers are padded once, on their first packet, to that value.Evidence
bot/questboard_bot/cogs/recording.py:156-215—write()is the only place audio is produced. It never readsdata.packet.timestampor.sequenceand never consults a wall clock.bot/questboard_bot/cogs/recording.py:118-119— the_session_bytescomment.bot/questboard_bot/cogs/recording.py:172-174— the one-time first-packet pad.bot/questboard_bot/cogs/recording.py:183-184,:204-205—_session_bytesadvances only for the reference uid.7ddff16("Fix per-speaker timestamp alignment", Apr 2026) introduced this and its message claims_session_bytes"tracks the wall-clock position of the first speaker". It has never read a clock. That commit is the original misdiagnosis.Production confirmation
Session
56cc4dee-99c0-470a-bd90-18e7621b4194, ~3 hours scheduled. Stored transcript: 1904 lines, 0 unparsed.Max timestamp 00:54:47 for a multi-hour session, per-speaker durations summing to ~2.08 h, and impossible sustained speech density (Idani: one utterance every 2.6 seconds for their entire track).
Proposed fix — wall-clock anchoring
Give
PerUserPCMSinkat0(the sametime.monotonic()sample asrec.started_at, with an injectableclockparameter for tests). Per packet, for useruid:At
close(), tail-pad every track toduration_sso all tracks are identical length. Delete_session_bytes, the reference-uid checks, and the first-packet pad entirely — the per-packet gap logic subsumes all three (a first packet at minute 40 sees a 40-minute gap and pads exactly).Placement error is bounded by jitter-buffer delivery delay plus the threshold, per talk spurt, and is non-cumulative — every placement is absolute against our own clock. Mutes, leaves, rejoins, reconnects, region changes and packet loss all become silence automatically, because the clock does not care why packets stopped.
The 250 ms threshold is load-bearing: without it, arrival jitter triggers one- and two-frame pads inside continuous speech, which stretches it audibly.
Rejected alternatives
RTP-timestamp anchoring would be frame-accurate and the data is available (
.timestamp,.ssrc,.sequenceall reach the sink). Deferred, not dismissed: it assumes every Discord client advances its RTP timestamp across silence, and any client that does not would silently recreate this exact bug, invisible until the next bad summary. Ship wall-clock anchoring, logts_delta_vs_wall_deltain the existingAUDIO_DIAGstream for one release, and revisit only if that data justifies it.Accepting the jitter-buffer PLC FakePackets (currently skipped at
recording.py:163-166) cannot fix this and must not be attempted. PLC fires only on sequence-number gaps, and sequence numbers pause during silence — inter-spurt gaps generate zero FakePackets. It also re-opens a known production incident: commit02898b3documents deferred DAVE packets causing a 148x PLC explosion, 340 MB for 12 seconds of audio.Dependencies
Must ship together with the headerless container change and the SSRC decoder reset in this milestone, and with the VAD change — correcting capture makes every track run the full wall-clock length, which re-arms Whisper's hallucination-on-silence failure and multiplies ASR input roughly ninefold.
Acceptance criteria
PerUserPCMSinktakesstarted_atand an injectableclockclose()tail-pads every track toduration_s_session_bytes, thenext(iter(self._writers))checks and the first-packet pad are deleted