* improve keyframes messages

* don't pad the labelmap with unknown

`load_labels()` prefilled 91 `unknown` entries before reading the label file, so any model with fewer than 91 classes kept that padding in `merged_labelmap` and `unknown` showed up as a selectable object type in the objects settings UI. The padding only existed so `RemoteObjectDetector.detect` could index the labelmap without a KeyError, and it didn't even cover the empty-file case or Frigate+, which never had a prefill. Both lookups now skip class ids the labelmap doesn't name and warn once per id.
This commit is contained in:
Josh Hawkins
2026-09-12 07:30:04 -06:00
committed by Nicolas Mowen
parent 853840dfd4
commit 396a2156b2
8 changed files with 160 additions and 21 deletions
+8 -1
View File
@@ -152,12 +152,19 @@ def get_record_segment_time(config: "CameraConfig") -> int:
def load_labels(
path: str | None, encoding="utf-8", prefill=91, indexed: bool | None = None
path: str | None, encoding="utf-8", prefill=0, indexed: bool | None = None
):
"""Loads labels from file (with or without index numbers).
Only the indices the file defines are returned, so the result describes
exactly the classes a model can name. Callers must treat a missing index
as an unnamed class rather than assuming a contiguous range.
Args:
path: path to label file.
encoding: label file encoding.
prefill: pad indices below this with "unknown" before reading the file.
indexed: whether lines start with an index; auto-detected when None.
Returns:
Dictionary mapping indices to labels.
"""
+13 -1
View File
@@ -1061,6 +1061,7 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro
KEYFRAME_PROBE_WINDOW_SECONDS = 20
KEYFRAME_GAP_WARNING_SECONDS = 4.0
KEYFRAME_GAP_JITTER_SECONDS = 0.5
def parse_keyframe_packets(output: str) -> tuple[list[float], float | None]:
@@ -1100,6 +1101,10 @@ def classify_keyframe_gaps(
- "error" when the longest gap exceeds the record segment length
- "warning" when the longest gap exceeds the warning threshold
- "ok" otherwise
The "pattern" key separates the two causes so callers can give accurate
advice: "fixed" is a regular GOP that is simply too long, "variable" is
the irregular spacing a smart/+ codec produces.
"""
thresholds = {
"warning": KEYFRAME_GAP_WARNING_SECONDS,
@@ -1112,6 +1117,7 @@ def classify_keyframe_gaps(
"max_gap": None,
"mean_gap": None,
"min_gap": None,
"pattern": None,
"segment_time": segment_time,
"severity": "unknown",
"thresholds": thresholds,
@@ -1119,6 +1125,7 @@ def classify_keyframe_gaps(
gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])]
max_gap = max(gaps)
min_gap = min(gaps)
if max_gap > segment_time:
severity = "error"
@@ -1127,11 +1134,16 @@ def classify_keyframe_gaps(
else:
severity = "ok"
# allow for encoder jitter and probe rounding before calling a GOP variable
tolerance = max(KEYFRAME_GAP_JITTER_SECONDS, min_gap * 0.25)
pattern = "variable" if (max_gap - min_gap) > tolerance else "fixed"
return {
"keyframe_count": len(keyframe_pts),
"max_gap": round(max_gap, 2),
"mean_gap": round(sum(gaps) / len(gaps), 2),
"min_gap": round(min(gaps), 2),
"min_gap": round(min_gap, 2),
"pattern": pattern,
"segment_time": segment_time,
"severity": severity,
"thresholds": thresholds,