Fix tail truncation, decouple call length from buffer, trim silence
CI / lint (push) Failing after 4s
CI / test (push) Successful in 21s

Measured six recordings off a live P25 node and found two independent
defects causing lost audio at the end of calls:

- stop_recording() sliced the ring buffer immediately, so if the MP3
  muxer had not yet delivered the tail the file was silently short.
  Now waits (bounded, 2s) until buffered audio covers the end epoch.
- TGID-change closes used the new grant's epoch as the end with no pad
  at all, guaranteeing truncation on every split. Tail pad is now a
  setting, default raised 0.5s -> 1.0s.

The ring buffer also capped maximum call length: a call longer than the
buffer had its front silently clamped. The ring now serves the pre-roll
only, with a per-call accumulator for the rest, bounded at 4.8MB.
Clamping is loudly warned rather than silent.

Uploads averaged 63% silence, which inflates STT cost and is a known
Whisper hallucination trigger. Leading/trailing silence is now trimmed
conservatively (-40dB, 0.25s guard, internal pauses untouched).
started_at/ended_at still describe the call; new audio_* fields carry
the trimmed audio bounds so playback can map back to wall clock.
All-silence recordings are skipped and logged instead of uploaded.

Also: log measured control-channel idle on idle-timeout closes so
CALL_IDLE_TIMEOUT can be tuned from data, and quiet the httpx logger
which emitted ~170k lines/day of poll noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-08-04 21:39:36 -04:00
parent efdbe7d803
commit b0a8ed2a5a
10 changed files with 1118 additions and 120 deletions
+31 -5
View File
@@ -47,15 +47,23 @@ POLL_INTERVAL = 0.5
# Seconds of unreachable OP25 before an open segment is force-closed.
OP25_OFFLINE_GRACE = 3.0
# Audio kept after the observed end of the last transmission, so the srcaddr edge
# (up to one poll late) never clips the tail.
TAIL_PAD_SECONDS = 0.5
# Hard ceiling on a single segment; mirrors MAX_RECORDING_SECONDS in call_recorder
# so a talkgroup that never goes quiet cannot produce an unbounded recording.
MAX_SEGMENT_SECONDS = 600
def _tail_pad() -> float:
"""
Audio kept after the observed end of the last transmission, so the srcaddr
edge (up to one poll late) plus encoder latency never clips the tail.
Read live from settings (env CALL_TAIL_PAD_SECONDS) rather than frozen into a
module constant, so it is tunable per node. See the setting for why the
default moved 0.5 → 1.0.
"""
return settings.call_tail_pad_seconds
def _as_int(value: Any) -> Optional[int]:
"""Coerce an OP25 field to a positive int, or None. Rejects 0/""/"None"."""
if value is None:
@@ -235,7 +243,25 @@ class MetadataWatcher:
# STOP: quiet for long enough. End the audio at the last transmission
# plus a short pad, not at "now" — otherwise every recording carries
# call_idle_timeout seconds of silence.
end = (self._last_tx_end + TAIL_PAD_SECONDS) if self._last_tx_end is not None else now
#
# The measured idle below is the CONTROL-CHANNEL idle (srcaddr 1→0
# edge → now). It is NOT comparable to silence measured in the audio,
# which additionally contains the ~1.9 s P25 grant→speech delay.
# Tune settings.call_idle_timeout from THIS number and nothing else.
if self._last_tx_end is not None:
measured_idle = now - self._last_tx_end
end = self._last_tx_end + _tail_pad()
logger.info(
f"Idle timeout for tgid {self._current_tgid}: measured control-channel idle "
f"{measured_idle:.2f}s (threshold {settings.call_idle_timeout:.2f}s, "
f"tail pad {_tail_pad():.2f}s)."
)
else:
end = now
logger.info(
f"Idle timeout for tgid {self._current_tgid}: no srcaddr end edge observed, "
f"idle {now - self._last_activity:.2f}s measured from last activity."
)
await self._close_segment(min(end, now), reason="idle_timeout")
return