Fix tail truncation, decouple call length from buffer, trim silence
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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user