mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-30 03:46:50 +03:00
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Integrate state changes with review items * Cleanup and fixes * Improve report context management
133 lines
4.8 KiB
Python
133 lines
4.8 KiB
Python
"""Tests for GenAI prompt builders."""
|
|
|
|
import unittest
|
|
|
|
from frigate.config.camera.review import ReviewResponseStyleEnum
|
|
from frigate.genai.prompts import (
|
|
REVIEW_DESCRIPTION_FIELD_GUIDELINES,
|
|
REVIEW_RESPONSE_STYLES,
|
|
build_review_description_prompt,
|
|
build_review_summary_prompt,
|
|
get_review_field_guidelines,
|
|
)
|
|
|
|
|
|
class TestReviewResponseStyle(unittest.TestCase):
|
|
def _build_prompt(self, response_style: str = "default") -> str:
|
|
review_data = {
|
|
"camera": "Front Door",
|
|
"start": "Monday, 09:30 AM",
|
|
"duration": 25,
|
|
"zones": [],
|
|
"unified_objects": ["person"],
|
|
}
|
|
return build_review_description_prompt(
|
|
review_data,
|
|
[b"fake-image"],
|
|
[],
|
|
None,
|
|
"activity context",
|
|
response_style,
|
|
)
|
|
|
|
def test_default_style_leaves_prompt_unchanged(self):
|
|
self.assertEqual(
|
|
get_review_field_guidelines("default"),
|
|
REVIEW_DESCRIPTION_FIELD_GUIDELINES,
|
|
)
|
|
self.assertEqual(self._build_prompt("default"), self._build_prompt())
|
|
|
|
def test_unknown_style_leaves_prompt_unchanged(self):
|
|
self.assertEqual(self._build_prompt("unknown"), self._build_prompt())
|
|
|
|
def test_styles_replace_user_facing_field_guidance(self):
|
|
default_prompt = self._build_prompt()
|
|
for style, overrides in REVIEW_RESPONSE_STYLES.items():
|
|
prompt = self._build_prompt(style)
|
|
for field_name, guidance in overrides.items():
|
|
self.assertIn(f"- `{field_name}`: {guidance}", prompt)
|
|
self.assertNotIn(
|
|
REVIEW_DESCRIPTION_FIELD_GUIDELINES[field_name], prompt
|
|
)
|
|
# Everything outside the overridden guidance lines is unchanged
|
|
self.assertEqual(
|
|
[
|
|
line
|
|
for line in prompt.splitlines()
|
|
if not any(line.startswith(f"- `{f}`:") for f in overrides)
|
|
],
|
|
[
|
|
line
|
|
for line in default_prompt.splitlines()
|
|
if not any(line.startswith(f"- `{f}`:") for f in overrides)
|
|
],
|
|
)
|
|
|
|
def test_styles_never_touch_reasoning_or_threat_fields(self):
|
|
# observations is a reasoning scaffold and potential_threat_level is
|
|
# scoring guidance; presets restyle only the user-facing fields.
|
|
for overrides in REVIEW_RESPONSE_STYLES.values():
|
|
self.assertTrue(
|
|
set(overrides) <= {"scene", "title", "shortSummary"},
|
|
f"unexpected override fields: {set(overrides)}",
|
|
)
|
|
|
|
def test_config_enum_matches_style_presets(self):
|
|
enum_styles = {e.value for e in ReviewResponseStyleEnum}
|
|
self.assertEqual(enum_styles, {"default", *REVIEW_RESPONSE_STYLES})
|
|
|
|
def test_enum_value_selects_preset(self):
|
|
prompt = self._build_prompt(ReviewResponseStyleEnum.natural)
|
|
self.assertIn(
|
|
REVIEW_RESPONSE_STYLES["natural"]["shortSummary"],
|
|
prompt,
|
|
)
|
|
|
|
|
|
class TestClassificationStateChanges(unittest.TestCase):
|
|
def _build_prompt(self, **extra) -> str:
|
|
review_data = {
|
|
"camera": "Front Door",
|
|
"start": "Monday, 09:30 AM",
|
|
"duration": 25,
|
|
"zones": [],
|
|
"unified_objects": ["person"],
|
|
**extra,
|
|
}
|
|
return build_review_description_prompt(
|
|
review_data, [b"fake-image"], [], None, "activity context"
|
|
)
|
|
|
|
def test_no_changes_leaves_prompt_unchanged(self):
|
|
self.assertEqual(
|
|
self._build_prompt(classification_state_changes=[]),
|
|
self._build_prompt(),
|
|
)
|
|
self.assertNotIn("## State Changes", self._build_prompt())
|
|
|
|
def test_changes_are_listed_before_objects(self):
|
|
change = "front gate changed from closed to open, 12s into the activity"
|
|
prompt = self._build_prompt(classification_state_changes=[change])
|
|
self.assertIn(f"\n- {change}\n\n## Objects in Scene", prompt)
|
|
self.assertLess(
|
|
prompt.index("## Sequence Details"), prompt.index("## State Changes")
|
|
)
|
|
|
|
def test_summary_describes_state_changes_only_when_present(self):
|
|
event = {"title": "Person at door", "camera": "Front Door", "context": []}
|
|
without = build_review_summary_prompt(0, 3600, [event], None)
|
|
self.assertNotIn('"state_changes"', without)
|
|
|
|
context_event = {
|
|
**event,
|
|
"context": [
|
|
{"camera": "Driveway", "state_changes": ["gate changed from a to b"]}
|
|
],
|
|
}
|
|
with_changes = build_review_summary_prompt(0, 3600, [context_event], None)
|
|
self.assertIn('- "state_changes"', with_changes)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|