From 6b24219e48baa68e8d842e6f7944b3da54450d71 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:15:22 -0500 Subject: [PATCH] basic e2e frontend test framework --- web/e2e/fixtures/frigate-test.ts | 80 +++++++ web/e2e/fixtures/mock-data/camera-activity.ts | 77 ++++++ .../fixtures/mock-data/config-snapshot.json | 1 + web/e2e/fixtures/mock-data/config.ts | 76 ++++++ .../mock-data/generate-config-snapshot.py | 94 ++++++++ web/e2e/fixtures/mock-data/profile.ts | 39 +++ web/e2e/fixtures/mock-data/stats.ts | 76 ++++++ web/e2e/global-setup.ts | 7 + web/e2e/helpers/api-mocker.ts | 225 ++++++++++++++++++ web/e2e/helpers/ws-mocker.ts | 125 ++++++++++ web/e2e/pages/base.page.ts | 82 +++++++ web/e2e/playwright.config.ts | 56 +++++ web/e2e/specs/auth.spec.ts | 70 ++++++ web/e2e/specs/chat.spec.ts | 22 ++ web/e2e/specs/classification.spec.ts | 18 ++ web/e2e/specs/config-editor.spec.ts | 23 ++ web/e2e/specs/explore.spec.ts | 82 +++++++ web/e2e/specs/export.spec.ts | 31 +++ web/e2e/specs/face-library.spec.ts | 19 ++ web/e2e/specs/live.spec.ts | 194 +++++++++++++++ web/e2e/specs/logs.spec.ts | 29 +++ web/e2e/specs/navigation.spec.ts | 198 +++++++++++++++ web/e2e/specs/replay.spec.ts | 13 + web/e2e/specs/review.spec.ts | 58 +++++ web/e2e/specs/settings/ui-settings.spec.ts | 32 +++ web/e2e/specs/system.spec.ts | 28 +++ web/package-lock.json | 64 +++++ web/package.json | 5 + 28 files changed, 1824 insertions(+) create mode 100644 web/e2e/fixtures/frigate-test.ts create mode 100644 web/e2e/fixtures/mock-data/camera-activity.ts create mode 100644 web/e2e/fixtures/mock-data/config-snapshot.json create mode 100644 web/e2e/fixtures/mock-data/config.ts create mode 100644 web/e2e/fixtures/mock-data/generate-config-snapshot.py create mode 100644 web/e2e/fixtures/mock-data/profile.ts create mode 100644 web/e2e/fixtures/mock-data/stats.ts create mode 100644 web/e2e/global-setup.ts create mode 100644 web/e2e/helpers/api-mocker.ts create mode 100644 web/e2e/helpers/ws-mocker.ts create mode 100644 web/e2e/pages/base.page.ts create mode 100644 web/e2e/playwright.config.ts create mode 100644 web/e2e/specs/auth.spec.ts create mode 100644 web/e2e/specs/chat.spec.ts create mode 100644 web/e2e/specs/classification.spec.ts create mode 100644 web/e2e/specs/config-editor.spec.ts create mode 100644 web/e2e/specs/explore.spec.ts create mode 100644 web/e2e/specs/export.spec.ts create mode 100644 web/e2e/specs/face-library.spec.ts create mode 100644 web/e2e/specs/live.spec.ts create mode 100644 web/e2e/specs/logs.spec.ts create mode 100644 web/e2e/specs/navigation.spec.ts create mode 100644 web/e2e/specs/replay.spec.ts create mode 100644 web/e2e/specs/review.spec.ts create mode 100644 web/e2e/specs/settings/ui-settings.spec.ts create mode 100644 web/e2e/specs/system.spec.ts diff --git a/web/e2e/fixtures/frigate-test.ts b/web/e2e/fixtures/frigate-test.ts new file mode 100644 index 000000000..88a2945d7 --- /dev/null +++ b/web/e2e/fixtures/frigate-test.ts @@ -0,0 +1,80 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +/** + * Extended Playwright test fixture with FrigateApp. + * + * Every test imports `test` and `expect` from this file instead of + * @playwright/test directly. The `frigateApp` fixture provides a + * fully mocked Frigate frontend ready for interaction. + * + * CRITICAL: All route/WS handlers are registered before page.goto() + * to prevent AuthProvider from redirecting to login.html. + */ + +import { test as base, expect, type Page } from "@playwright/test"; +import { + ApiMocker, + MediaMocker, + type ApiMockOverrides, +} from "../helpers/api-mocker"; +import { WsMocker } from "../helpers/ws-mocker"; + +export class FrigateApp { + public api: ApiMocker; + public media: MediaMocker; + public ws: WsMocker; + public page: Page; + + private isDesktop: boolean; + + constructor(page: Page, projectName: string) { + this.page = page; + this.api = new ApiMocker(page); + this.media = new MediaMocker(page); + this.ws = new WsMocker(); + this.isDesktop = projectName === "desktop"; + } + + get isMobile() { + return !this.isDesktop; + } + + /** Install all mocks with default data. Call before goto(). */ + async installDefaults(overrides?: ApiMockOverrides) { + // Mock i18n locale files to prevent 404s + await this.page.route("**/locales/**", async (route) => { + // Let the request through to the built files + return route.fallback(); + }); + + await this.ws.install(this.page); + await this.media.install(); + await this.api.install(overrides); + } + + /** Navigate to a page. Always call installDefaults() first. */ + async goto(path: string) { + await this.page.goto(path); + // Wait for the app to render past the loading indicator + await this.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + } + + /** Navigate to a page that may show a loading indicator */ + async gotoAndWait(path: string, selector: string) { + await this.page.goto(path); + await this.page.waitForSelector(selector, { timeout: 10_000 }); + } +} + +type FrigateFixtures = { + frigateApp: FrigateApp; +}; + +export const test = base.extend({ + frigateApp: async ({ page }, use, testInfo) => { + const app = new FrigateApp(page, testInfo.project.name); + await app.installDefaults(); + await use(app); + }, +}); + +export { expect }; diff --git a/web/e2e/fixtures/mock-data/camera-activity.ts b/web/e2e/fixtures/mock-data/camera-activity.ts new file mode 100644 index 000000000..425e931a8 --- /dev/null +++ b/web/e2e/fixtures/mock-data/camera-activity.ts @@ -0,0 +1,77 @@ +/** + * Camera activity WebSocket payload factory. + * + * The camera_activity topic payload is double-serialized: + * the WS message contains { topic: "camera_activity", payload: JSON.stringify(activityMap) } + */ + +export interface CameraActivityState { + config: { + enabled: boolean; + detect: boolean; + record: boolean; + snapshots: boolean; + audio: boolean; + audio_transcription: boolean; + notifications: boolean; + notifications_suspended: number; + autotracking: boolean; + alerts: boolean; + detections: boolean; + object_descriptions: boolean; + review_descriptions: boolean; + }; + motion: boolean; + objects: Array<{ + label: string; + score: number; + box: [number, number, number, number]; + area: number; + ratio: number; + region: [number, number, number, number]; + current_zones: string[]; + id: string; + }>; + audio_detections: Array<{ + label: string; + score: number; + }>; +} + +function defaultCameraActivity(): CameraActivityState { + return { + config: { + enabled: true, + detect: true, + record: true, + snapshots: true, + audio: false, + audio_transcription: false, + notifications: false, + notifications_suspended: 0, + autotracking: false, + alerts: true, + detections: true, + object_descriptions: false, + review_descriptions: false, + }, + motion: false, + objects: [], + audio_detections: [], + }; +} + +export function cameraActivityPayload( + cameras: string[], + overrides?: Partial>>, +): string { + const activity: Record = {}; + for (const name of cameras) { + activity[name] = { + ...defaultCameraActivity(), + ...overrides?.[name], + } as CameraActivityState; + } + // Double-serialize: the WS payload is a JSON string + return JSON.stringify(activity); +} diff --git a/web/e2e/fixtures/mock-data/config-snapshot.json b/web/e2e/fixtures/mock-data/config-snapshot.json new file mode 100644 index 000000000..b898c5ba3 --- /dev/null +++ b/web/e2e/fixtures/mock-data/config-snapshot.json @@ -0,0 +1 @@ +{"version": null, "safe_mode": false, "environment_vars": {}, "logger": {"default": "info", "logs": {}}, "auth": {"enabled": true, "reset_admin_password": false, "cookie_name": "frigate_token", "cookie_secure": false, "session_length": 86400, "refresh_time": 1800, "failed_login_rate_limit": null, "trusted_proxies": [], "hash_iterations": 600000, "roles": {"admin": [], "viewer": []}, "admin_first_time_login": false}, "database": {"path": "/config/frigate.db"}, "go2rtc": {}, "mqtt": {"enabled": true, "host": "mqtt", "port": 1883, "topic_prefix": "frigate", "client_id": "frigate", "stats_interval": 60, "user": null, "password": null, "tls_ca_certs": null, "tls_client_cert": null, "tls_client_key": null, "tls_insecure": null, "qos": 0}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "networking": {"ipv6": {"enabled": false}, "listen": {"internal": 5000, "external": 8971}}, "proxy": {"header_map": {"user": null, "role": null, "role_map": {}}, "logout_url": null, "auth_secret": null, "default_role": "viewer", "separator": ","}, "telemetry": {"network_interfaces": [], "stats": {"amd_gpu_stats": true, "intel_gpu_stats": true, "network_bandwidth": false, "intel_gpu_device": null}, "version_check": true}, "tls": {"enabled": true}, "ui": {"timezone": null, "time_format": "browser", "date_style": "short", "time_style": "medium", "unit_system": "metric"}, "detectors": {"cpu": {"type": "cpu", "model": {"path": "/cpu_model.tflite", "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd"}, "model_path": null}}, "model": {"path": null, "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd", "all_attributes": ["amazon", "an_post", "canada_post", "dhl", "dpd", "face", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "colormap": {}}, "genai": {}, "cameras": {"front_door": {"name": "front_door", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"front_door": "front_door"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "backyard": {"name": "backyard", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"backyard": "backyard"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "garage": {"name": "garage", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"garage": "garage"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}}, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": null, "num_threads": 2}, "birdseye": {"enabled": true, "mode": "objects", "restream": false, "width": 1280, "height": 720, "quality": 8, "inactivity_threshold": 30, "layout": {"scaling_factor": 2.0, "max_cameras": null}, "idle_heartbeat_fps": 0.0}, "detect": {"enabled": false, "height": null, "width": null, "fps": 5, "min_initialized": null, "max_disappeared": null, "stationary": {"interval": null, "threshold": null, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0}, "live": {"streams": [], "height": 720, "quality": 8}, "motion": null, "objects": {"track": ["person"], "filters": {"nzpost": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "gls": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "amazon": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnord": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "ups": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "usps": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "license_plate": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dhl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dpd": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "canada_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "purolator": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "an_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "fedex": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "face": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "royal_mail": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": null}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "auto"}, "preview": {"quality": "medium"}, "enabled_in_config": null}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": null, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": null}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": null, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "audio_transcription": {"enabled": false, "language": "en", "device": "CPU", "model_size": "small", "live_enabled": false}, "classification": {"bird": {"enabled": false, "threshold": 0.9}, "custom": {}}, "semantic_search": {"enabled": false, "reindex": false, "model": "jinav1", "model_size": "small", "device": null}, "face_recognition": {"enabled": false, "model_size": "small", "unknown_score": 0.8, "detection_threshold": 0.7, "recognition_threshold": 0.9, "min_area": 750, "min_faces": 1, "save_attempts": 200, "blur_confidence_filter": true, "device": null}, "lpr": {"enabled": false, "model_size": "small", "detection_threshold": 0.7, "min_area": 1000, "recognition_threshold": 0.9, "min_plate_length": 4, "format": null, "match_distance": 1, "known_plates": {}, "enhancement": 0, "debug_save_plates": false, "device": null, "replace_rules": []}, "camera_groups": {"default": {"cameras": ["front_door", "backyard", "garage"], "icon": "generic", "order": 0}, "outdoor": {"cameras": ["front_door", "backyard"], "icon": "generic", "order": 1}}, "profiles": {}} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/config.ts b/web/e2e/fixtures/mock-data/config.ts new file mode 100644 index 000000000..ba86425a9 --- /dev/null +++ b/web/e2e/fixtures/mock-data/config.ts @@ -0,0 +1,76 @@ +/** + * FrigateConfig factory for E2E tests. + * + * Uses a real config snapshot generated from the Python backend's FrigateConfig + * model. This guarantees all fields are present and match what the app expects. + * Tests override specific fields via DeepPartial. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const configSnapshot = JSON.parse( + readFileSync(resolve(__dirname, "config-snapshot.json"), "utf-8"), +); + +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +function deepMerge>( + base: T, + overrides?: DeepPartial, +): T { + if (!overrides) return base; + const result = { ...base }; + for (const key of Object.keys(overrides) as (keyof T)[]) { + const val = overrides[key]; + if ( + val !== undefined && + typeof val === "object" && + val !== null && + !Array.isArray(val) && + typeof base[key] === "object" && + base[key] !== null && + !Array.isArray(base[key]) + ) { + result[key] = deepMerge( + base[key] as Record, + val as DeepPartial>, + ) as T[keyof T]; + } else if (val !== undefined) { + result[key] = val as T[keyof T]; + } + } + return result; +} + +// The base config is a real snapshot from the Python backend. +// Apply test-specific overrides: friendly names, camera groups, version. +export const BASE_CONFIG = { + ...configSnapshot, + version: "0.15.0-test", + cameras: { + ...configSnapshot.cameras, + front_door: { + ...configSnapshot.cameras.front_door, + friendly_name: "Front Door", + }, + backyard: { + ...configSnapshot.cameras.backyard, + friendly_name: "Backyard", + }, + garage: { + ...configSnapshot.cameras.garage, + friendly_name: "Garage", + }, + }, +}; + +export function configFactory( + overrides?: DeepPartial, +): typeof BASE_CONFIG { + return deepMerge(BASE_CONFIG, overrides); +} diff --git a/web/e2e/fixtures/mock-data/generate-config-snapshot.py b/web/e2e/fixtures/mock-data/generate-config-snapshot.py new file mode 100644 index 000000000..bcb07d430 --- /dev/null +++ b/web/e2e/fixtures/mock-data/generate-config-snapshot.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Generate a complete FrigateConfig snapshot for E2E tests. + +Run from the repo root: + python3 web/e2e/fixtures/mock-data/generate-config-snapshot.py + +This generates config-snapshot.json with all fields from the Python backend, +plus runtime-computed fields that the API adds but aren't in the Pydantic model. +""" + +import json +import sys +import warnings +from pathlib import Path + +warnings.filterwarnings("ignore") + +from frigate.config import FrigateConfig # noqa: E402 + +# Minimal config with 3 test cameras and camera groups +MINIMAL_CONFIG = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 720, "width": 1280, "fps": 5}, + }, + "backyard": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 720, "width": 1280, "fps": 5}, + }, + "garage": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 720, "width": 1280, "fps": 5}, + }, + }, + "camera_groups": { + "default": { + "cameras": ["front_door", "backyard", "garage"], + "icon": "generic", + "order": 0, + }, + "outdoor": { + "cameras": ["front_door", "backyard"], + "icon": "generic", + "order": 1, + }, + }, +} + + +def generate(): + config = FrigateConfig.model_validate_json(json.dumps(MINIMAL_CONFIG)) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + snapshot = config.model_dump() + + # Add runtime-computed fields that the API serves but aren't in the + # Pydantic model dump. These are computed by the backend when handling + # GET /api/config requests. + + # model.all_attributes: flattened list of all attribute labels from attributes_map + all_attrs = set() + for attrs in snapshot.get("model", {}).get("attributes_map", {}).values(): + all_attrs.update(attrs) + snapshot["model"]["all_attributes"] = sorted(all_attrs) + + # model.colormap: empty by default (populated at runtime from model output) + snapshot["model"]["colormap"] = {} + + # Convert to JSON-serializable format (handles datetime, Path, etc.) + output = json.dumps(snapshot, default=str) + + # Write to config-snapshot.json in the same directory as this script + output_path = Path(__file__).parent / "config-snapshot.json" + output_path.write_text(output) + print(f"Generated {output_path} ({len(output)} bytes)") + + +if __name__ == "__main__": + generate() diff --git a/web/e2e/fixtures/mock-data/profile.ts b/web/e2e/fixtures/mock-data/profile.ts new file mode 100644 index 000000000..62d70e3a0 --- /dev/null +++ b/web/e2e/fixtures/mock-data/profile.ts @@ -0,0 +1,39 @@ +/** + * User profile factories for E2E tests. + */ + +export interface UserProfile { + username: string; + role: string; + allowed_cameras: string[] | null; +} + +export function adminProfile(overrides?: Partial): UserProfile { + return { + username: "admin", + role: "admin", + allowed_cameras: null, + ...overrides, + }; +} + +export function viewerProfile(overrides?: Partial): UserProfile { + return { + username: "viewer", + role: "viewer", + allowed_cameras: null, + ...overrides, + }; +} + +export function restrictedProfile( + cameras: string[], + overrides?: Partial, +): UserProfile { + return { + username: "restricted", + role: "viewer", + allowed_cameras: cameras, + ...overrides, + }; +} diff --git a/web/e2e/fixtures/mock-data/stats.ts b/web/e2e/fixtures/mock-data/stats.ts new file mode 100644 index 000000000..d34ea25fc --- /dev/null +++ b/web/e2e/fixtures/mock-data/stats.ts @@ -0,0 +1,76 @@ +/** + * FrigateStats factory for E2E tests. + */ + +import type { DeepPartial } from "./config"; + +function cameraStats(_name: string) { + return { + audio_dBFPS: 0, + audio_rms: 0, + camera_fps: 5.0, + capture_pid: 100, + detection_enabled: 1, + detection_fps: 5.0, + ffmpeg_pid: 101, + pid: 102, + process_fps: 5.0, + skipped_fps: 0, + connection_quality: "excellent" as const, + expected_fps: 5, + reconnects_last_hour: 0, + stalls_last_hour: 0, + }; +} + +export const BASE_STATS = { + cameras: { + front_door: cameraStats("front_door"), + backyard: cameraStats("backyard"), + garage: cameraStats("garage"), + }, + cpu_usages: { + "1": { cmdline: "frigate.app", cpu: "5.0", cpu_average: "4.5", mem: "2.1" }, + }, + detectors: { + cpu: { + detection_start: 0, + inference_speed: 75.5, + pid: 200, + }, + }, + gpu_usages: {}, + npu_usages: {}, + processes: {}, + service: { + last_updated: Date.now() / 1000, + storage: { + "/media/frigate/recordings": { + free: 50000000000, + total: 100000000000, + used: 50000000000, + mount_type: "ext4", + }, + "/tmp/cache": { + free: 500000000, + total: 1000000000, + used: 500000000, + mount_type: "tmpfs", + }, + }, + uptime: 86400, + latest_version: "0.15.0", + version: "0.15.0-test", + }, + camera_fps: 15.0, + process_fps: 15.0, + skipped_fps: 0, + detection_fps: 15.0, +}; + +export function statsFactory( + overrides?: DeepPartial, +): typeof BASE_STATS { + if (!overrides) return BASE_STATS; + return { ...BASE_STATS, ...overrides } as typeof BASE_STATS; +} diff --git a/web/e2e/global-setup.ts b/web/e2e/global-setup.ts new file mode 100644 index 000000000..ef8f546b5 --- /dev/null +++ b/web/e2e/global-setup.ts @@ -0,0 +1,7 @@ +import { execSync } from "child_process"; +import path from "path"; + +export default function globalSetup() { + const webDir = path.resolve(__dirname, ".."); + execSync("npm run e2e:build", { cwd: webDir, stdio: "inherit" }); +} diff --git a/web/e2e/helpers/api-mocker.ts b/web/e2e/helpers/api-mocker.ts new file mode 100644 index 000000000..804dfc1ea --- /dev/null +++ b/web/e2e/helpers/api-mocker.ts @@ -0,0 +1,225 @@ +/** + * REST API mock using Playwright's page.route(). + * + * Intercepts all /api/* requests and returns factory-generated responses. + * Must be installed BEFORE page.goto() to prevent auth redirects. + */ + +import type { Page } from "@playwright/test"; +import { + BASE_CONFIG, + type DeepPartial, + configFactory, +} from "../fixtures/mock-data/config"; +import { adminProfile, type UserProfile } from "../fixtures/mock-data/profile"; +import { BASE_STATS, statsFactory } from "../fixtures/mock-data/stats"; + +// 1x1 transparent PNG +const PLACEHOLDER_PNG = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "base64", +); + +export interface ApiMockOverrides { + config?: DeepPartial; + profile?: UserProfile; + stats?: DeepPartial; + reviews?: unknown[]; + events?: unknown[]; + exports?: unknown[]; + cases?: unknown[]; + faces?: Record; + configRaw?: string; + configSchema?: Record; +} + +export class ApiMocker { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async install(overrides?: ApiMockOverrides) { + const config = configFactory(overrides?.config); + const profile = overrides?.profile ?? adminProfile(); + const stats = statsFactory(overrides?.stats); + + // Config endpoint + await this.page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + + // Profile endpoint (AuthProvider fetches /profile directly via axios, + // which resolves to /api/profile due to axios.defaults.baseURL) + await this.page.route("**/profile", (route) => + route.fulfill({ json: profile }), + ); + + // Stats endpoint + await this.page.route("**/api/stats", (route) => + route.fulfill({ json: stats }), + ); + + // Reviews + await this.page.route("**/api/reviews**", (route) => + route.fulfill({ json: overrides?.reviews ?? [] }), + ); + + // Events / search + await this.page.route("**/api/events**", (route) => + route.fulfill({ json: overrides?.events ?? [] }), + ); + + // Exports + await this.page.route("**/api/export**", (route) => + route.fulfill({ json: overrides?.exports ?? [] }), + ); + + // Cases + await this.page.route("**/api/cases", (route) => + route.fulfill({ json: overrides?.cases ?? [] }), + ); + + // Faces + await this.page.route("**/api/faces", (route) => + route.fulfill({ json: overrides?.faces ?? {} }), + ); + + // Logs + await this.page.route("**/api/logs/**", (route) => + route.fulfill({ + contentType: "text/plain", + body: "[2026-04-06 10:00:00] INFO: Frigate started\n[2026-04-06 10:00:01] INFO: Cameras loaded\n", + }), + ); + + // Config raw + await this.page.route("**/api/config/raw", (route) => + route.fulfill({ + contentType: "text/plain", + body: + overrides?.configRaw ?? + "mqtt:\n host: mqtt\ncameras:\n front_door:\n enabled: true\n", + }), + ); + + // Config schema + await this.page.route("**/api/config/schema.json", (route) => + route.fulfill({ + json: overrides?.configSchema ?? { type: "object", properties: {} }, + }), + ); + + // Config set (mutation) + await this.page.route("**/api/config/set", (route) => + route.fulfill({ json: { success: true, require_restart: false } }), + ); + + // Go2RTC streams + await this.page.route("**/api/go2rtc/streams**", (route) => + route.fulfill({ json: {} }), + ); + + // Profiles + await this.page.route("**/api/profiles**", (route) => + route.fulfill({ + json: { profiles: [], active_profile: null, last_activated: {} }, + }), + ); + + // Motion search + await this.page.route("**/api/motion_search**", (route) => + route.fulfill({ json: { job_id: "test-job" } }), + ); + + // Region grid + await this.page.route("**/api/*/region_grid", (route) => + route.fulfill({ json: {} }), + ); + + // Debug replay + await this.page.route("**/api/debug_replay/**", (route) => + route.fulfill({ json: {} }), + ); + + // Generic mutation catch-all for remaining endpoints. + // Uses route.fallback() to defer to more specific routes registered above. + // Playwright matches routes in reverse registration order (last wins), + // so this catch-all must use fallback() to let specific routes take precedence. + await this.page.route("**/api/**", (route) => { + const method = route.request().method(); + if ( + method === "POST" || + method === "PUT" || + method === "PATCH" || + method === "DELETE" + ) { + return route.fulfill({ json: { success: true } }); + } + // Fall through to more specific routes for GET requests + return route.fallback(); + }); + } +} + +export class MediaMocker { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + async install() { + // Camera snapshots + await this.page.route("**/api/*/latest.jpg**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // Clips and thumbnails + await this.page.route("**/clips/**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // Event thumbnails + await this.page.route("**/api/events/*/thumbnail.jpg**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // Event snapshots + await this.page.route("**/api/events/*/snapshot.jpg**", (route) => + route.fulfill({ + contentType: "image/png", + body: PLACEHOLDER_PNG, + }), + ); + + // VOD / recordings + await this.page.route("**/vod/**", (route) => + route.fulfill({ + contentType: "application/vnd.apple.mpegurl", + body: "#EXTM3U\n#EXT-X-ENDLIST\n", + }), + ); + + // Live streams + await this.page.route("**/live/**", (route) => + route.fulfill({ + contentType: "application/vnd.apple.mpegurl", + body: "#EXTM3U\n#EXT-X-ENDLIST\n", + }), + ); + } +} diff --git a/web/e2e/helpers/ws-mocker.ts b/web/e2e/helpers/ws-mocker.ts new file mode 100644 index 000000000..6b29b7639 --- /dev/null +++ b/web/e2e/helpers/ws-mocker.ts @@ -0,0 +1,125 @@ +/** + * WebSocket mock using Playwright's native page.routeWebSocket(). + * + * Intercepts the app's WebSocket connection and simulates the Frigate + * WS protocol: onConnect handshake, camera_activity expansion, and + * topic-based state updates. + */ + +import type { Page, WebSocketRoute } from "@playwright/test"; +import { cameraActivityPayload } from "../fixtures/mock-data/camera-activity"; + +export class WsMocker { + private mockWs: WebSocketRoute | null = null; + private cameras: string[]; + + constructor(cameras: string[] = ["front_door", "backyard", "garage"]) { + this.cameras = cameras; + } + + async install(page: Page) { + await page.routeWebSocket("**/ws", (ws) => { + this.mockWs = ws; + + ws.onMessage((msg) => { + this.handleClientMessage(msg.toString()); + }); + }); + } + + private handleClientMessage(raw: string) { + let data: { topic: string; payload?: unknown; message?: string }; + try { + data = JSON.parse(raw); + } catch { + return; + } + + if (data.topic === "onConnect") { + // Send initial camera_activity state + this.sendCameraActivity(); + + // Send initial stats + this.send( + "stats", + JSON.stringify({ + cameras: Object.fromEntries( + this.cameras.map((c) => [ + c, + { + camera_fps: 5, + detection_fps: 5, + process_fps: 5, + skipped_fps: 0, + detection_enabled: 1, + connection_quality: "excellent", + }, + ]), + ), + service: { + last_updated: Date.now() / 1000, + uptime: 86400, + version: "0.15.0-test", + latest_version: "0.15.0", + storage: {}, + }, + detectors: {}, + cpu_usages: {}, + gpu_usages: {}, + camera_fps: 15, + process_fps: 15, + skipped_fps: 0, + detection_fps: 15, + }), + ); + } + + // Echo back state commands (e.g., modelState, jobState, etc.) + if (data.topic === "modelState") { + this.send("model_state", JSON.stringify({})); + } + if (data.topic === "embeddingsReindexProgress") { + this.send("embeddings_reindex_progress", JSON.stringify(null)); + } + if (data.topic === "birdseyeLayout") { + this.send("birdseye_layout", JSON.stringify(null)); + } + if (data.topic === "jobState") { + this.send("job_state", JSON.stringify({})); + } + if (data.topic === "audioTranscriptionState") { + this.send("audio_transcription_state", JSON.stringify("idle")); + } + + // Camera toggle commands: echo back the new state + const toggleMatch = data.topic?.match( + /^(.+)\/(detect|recordings|snapshots|audio|enabled|notifications|ptz_autotracker|review_alerts|review_detections|object_descriptions|review_descriptions|audio_transcription)\/set$/, + ); + if (toggleMatch) { + const [, camera, feature] = toggleMatch; + this.send(`${camera}/${feature}/state`, data.payload); + } + } + + /** Send a raw WS message to the app */ + send(topic: string, payload: unknown) { + if (!this.mockWs) return; + this.mockWs.send(JSON.stringify({ topic, payload })); + } + + /** Send camera_activity with default or custom state */ + sendCameraActivity(overrides?: Parameters[1]) { + const payload = cameraActivityPayload(this.cameras, overrides); + this.send("camera_activity", payload); + } + + /** Send a review update */ + sendReview(review: unknown) { + this.send("reviews", JSON.stringify(review)); + } + + /** Send an event update */ + sendEvent(event: unknown) { + this.send("events", JSON.stringify(event)); + } +} diff --git a/web/e2e/pages/base.page.ts b/web/e2e/pages/base.page.ts new file mode 100644 index 000000000..4362f786f --- /dev/null +++ b/web/e2e/pages/base.page.ts @@ -0,0 +1,82 @@ +/** + * Base page object with viewport-aware navigation helpers. + * + * Desktop: clicks sidebar NavLink elements. + * Mobile: clicks bottombar NavLink elements. + */ + +import type { Page, Locator } from "@playwright/test"; + +export class BasePage { + constructor( + protected page: Page, + public isDesktop: boolean, + ) {} + + get isMobile() { + return !this.isDesktop; + } + + /** The sidebar (desktop only) */ + get sidebar(): Locator { + return this.page.locator("aside"); + } + + /** The bottombar (mobile only) */ + get bottombar(): Locator { + return this.page + .locator('[data-bottombar="true"]') + .or(this.page.locator(".absolute.inset-x-4.bottom-0").first()); + } + + /** The main page content area */ + get pageRoot(): Locator { + return this.page.locator("#pageRoot"); + } + + /** Navigate using a NavLink by its href */ + async navigateTo(path: string) { + // Wait for any in-progress React renders to settle before clicking + await this.page.waitForLoadState("domcontentloaded"); + // Use page.click with a CSS selector to avoid stale element issues + // when React re-renders the nav during route transitions. + // force: true bypasses actionability checks that fail when React + // detaches and reattaches nav elements during re-renders. + const selector = this.isDesktop + ? `aside a[href="${path}"]` + : `a[href="${path}"]`; + // Use dispatchEvent to bypass actionability checks that fail when + // React tooltip wrappers detach/reattach nav elements during re-renders + await this.page.locator(selector).first().dispatchEvent("click"); + // React Router navigates client-side, wait for URL update + if (path !== "/") { + const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + await this.page.waitForURL(new RegExp(escaped), { timeout: 10_000 }); + } + } + + /** Navigate to Live page */ + async goToLive() { + await this.navigateTo("/"); + } + + /** Navigate to Review page */ + async goToReview() { + await this.navigateTo("/review"); + } + + /** Navigate to Explore page */ + async goToExplore() { + await this.navigateTo("/explore"); + } + + /** Navigate to Export page */ + async goToExport() { + await this.navigateTo("/export"); + } + + /** Check if the page has loaded */ + async waitForPageLoad() { + await this.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + } +} diff --git a/web/e2e/playwright.config.ts b/web/e2e/playwright.config.ts new file mode 100644 index 000000000..525d42818 --- /dev/null +++ b/web/e2e/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const webRoot = resolve(__dirname, ".."); + +const DESKTOP_UA = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; +const MOBILE_UA = + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; + +export default defineConfig({ + testDir: "./specs", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 4 : undefined, + reporter: process.env.CI ? [["json"], ["html"]] : [["html"]], + timeout: 30_000, + expect: { timeout: 5_000 }, + + use: { + baseURL: "http://localhost:4173", + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + + webServer: { + command: "npx vite preview --port 4173", + port: 4173, + cwd: webRoot, + reuseExistingServer: !process.env.CI, + }, + + projects: [ + { + name: "desktop", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, + userAgent: DESKTOP_UA, + }, + }, + { + name: "mobile", + use: { + ...devices["Desktop Chrome"], + viewport: { width: 390, height: 844 }, + userAgent: MOBILE_UA, + isMobile: true, + hasTouch: true, + }, + }, + ], +}); diff --git a/web/e2e/specs/auth.spec.ts b/web/e2e/specs/auth.spec.ts new file mode 100644 index 000000000..a56a5bd40 --- /dev/null +++ b/web/e2e/specs/auth.spec.ts @@ -0,0 +1,70 @@ +/** + * Auth and cross-cutting tests -- HIGH tier. + * + * Tests protected routes, unauthorized redirect, + * and app-wide behaviors. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { viewerProfile } from "../fixtures/mock-data/profile"; + +test.describe("Auth & Protected Routes @high", () => { + test("admin can access /system", async ({ frigateApp }) => { + await frigateApp.goto("/system"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("admin can access /config", async ({ frigateApp }) => { + await frigateApp.goto("/config"); + // Config editor may take time to load Monaco + await frigateApp.page.waitForTimeout(3000); + await expect(frigateApp.page.locator("body")).toBeVisible(); + }); + + test("admin can access /logs", async ({ frigateApp }) => { + await frigateApp.goto("/logs"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("viewer is redirected from admin routes", async ({ + frigateApp, + page, + }) => { + // Re-install mocks with viewer profile + await frigateApp.installDefaults({ + profile: viewerProfile(), + }); + await page.goto("/system"); + await page.waitForTimeout(2000); + // Should be redirected to unauthorized page + const url = page.url(); + const hasAccessDenied = url.includes("unauthorized"); + const bodyText = await page.textContent("body"); + const showsAccessDenied = + bodyText?.includes("Access Denied") || + bodyText?.includes("permission") || + hasAccessDenied; + expect(showsAccessDenied).toBeTruthy(); + }); + + test("all main pages render without crash", async ({ frigateApp }) => { + // Smoke test all user-accessible routes + const routes = ["/", "/review", "/explore", "/export", "/settings"]; + for (const route of routes) { + await frigateApp.goto(route); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + } + }); + + test("all admin pages render without crash", async ({ frigateApp }) => { + const routes = ["/system", "/logs"]; + for (const route of routes) { + await frigateApp.goto(route); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible({ + timeout: 10_000, + }); + } + }); +}); diff --git a/web/e2e/specs/chat.spec.ts b/web/e2e/specs/chat.spec.ts new file mode 100644 index 000000000..f60910c09 --- /dev/null +++ b/web/e2e/specs/chat.spec.ts @@ -0,0 +1,22 @@ +/** + * Chat page tests -- MEDIUM tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Chat Page @medium", () => { + test("chat page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/chat"); + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("body")).toBeVisible(); + }); + + test("chat page has interactive elements", async ({ frigateApp }) => { + await frigateApp.goto("/chat"); + await frigateApp.page.waitForTimeout(2000); + // Should have interactive elements (input, textarea, or buttons) + const interactive = frigateApp.page.locator("input, textarea, button"); + const count = await interactive.count(); + expect(count).toBeGreaterThan(0); + }); +}); diff --git a/web/e2e/specs/classification.spec.ts b/web/e2e/specs/classification.spec.ts new file mode 100644 index 000000000..4b2fc845c --- /dev/null +++ b/web/e2e/specs/classification.spec.ts @@ -0,0 +1,18 @@ +/** + * Classification page tests -- MEDIUM tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Classification @medium", () => { + test("classification page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/classification"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("classification page shows content", async ({ frigateApp }) => { + await frigateApp.goto("/classification"); + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/config-editor.spec.ts b/web/e2e/specs/config-editor.spec.ts new file mode 100644 index 000000000..549e346f7 --- /dev/null +++ b/web/e2e/specs/config-editor.spec.ts @@ -0,0 +1,23 @@ +/** + * Config Editor page tests -- MEDIUM tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Config Editor @medium", () => { + test("config editor page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/config"); + // Monaco editor may take time to load + await frigateApp.page.waitForTimeout(3000); + await expect(frigateApp.page.locator("body")).toBeVisible(); + }); + + test("config editor has save button", async ({ frigateApp }) => { + await frigateApp.goto("/config"); + await frigateApp.page.waitForTimeout(3000); + // Should have at least a save or action button + const buttons = frigateApp.page.locator("button"); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + }); +}); diff --git a/web/e2e/specs/explore.spec.ts b/web/e2e/specs/explore.spec.ts new file mode 100644 index 000000000..04b2f1769 --- /dev/null +++ b/web/e2e/specs/explore.spec.ts @@ -0,0 +1,82 @@ +/** + * Explore page tests -- HIGH tier. + * + * Tests search input, filter dialogs, camera filter, calendar filter, + * and search result interactions. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Explore Page @high", () => { + test("explore page renders with search and filter controls", async ({ + frigateApp, + }) => { + await frigateApp.goto("/explore"); + const pageRoot = frigateApp.page.locator("#pageRoot"); + await expect(pageRoot).toBeVisible(); + // Should have filter buttons (camera filter, calendar, etc.) + const buttons = frigateApp.page.locator("#pageRoot button"); + await expect(buttons.first()).toBeVisible({ timeout: 10_000 }); + }); + + test("camera filter button opens camera selector", async ({ frigateApp }) => { + await frigateApp.goto("/explore"); + await frigateApp.page.waitForTimeout(1000); + // Find and click the camera filter button (has camera/video icon) + const filterButtons = frigateApp.page.locator("#pageRoot button"); + // Click the first filter button + await filterButtons.first().click(); + await frigateApp.page.waitForTimeout(500); + // A popover, dropdown, or dialog should appear + const overlay = frigateApp.page.locator( + '[role="dialog"], [role="menu"], [data-radix-popper-content-wrapper], [data-radix-menu-content]', + ); + const overlayVisible = await overlay + .first() + .isVisible() + .catch(() => false); + // The button click should not crash the page + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + // If an overlay appeared, it should be dismissible + if (overlayVisible) { + await frigateApp.page.keyboard.press("Escape"); + await frigateApp.page.waitForTimeout(300); + } + }); + + test("search input accepts text", async ({ frigateApp }) => { + await frigateApp.goto("/explore"); + await frigateApp.page.waitForTimeout(1000); + // Find the search input (InputWithTags component) + const searchInput = frigateApp.page.locator("input").first(); + if (await searchInput.isVisible()) { + await searchInput.fill("person"); + await expect(searchInput).toHaveValue("person"); + } + }); + + test("filter button click opens overlay and escape closes it", async ({ + frigateApp, + }) => { + await frigateApp.goto("/explore"); + await frigateApp.page.waitForTimeout(1000); + // Click the first filter button in the page + const firstButton = frigateApp.page.locator("#pageRoot button").first(); + await expect(firstButton).toBeVisible({ timeout: 5_000 }); + await firstButton.click(); + await frigateApp.page.waitForTimeout(500); + // An overlay may have appeared -- dismiss it + await frigateApp.page.keyboard.press("Escape"); + await frigateApp.page.waitForTimeout(300); + // Page should still be functional + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("explore page shows summary or empty state", async ({ frigateApp }) => { + await frigateApp.goto("/explore"); + await frigateApp.page.waitForTimeout(2000); + // With no search results, should show either summary view or empty state + const pageText = await frigateApp.page.textContent("#pageRoot"); + expect(pageText?.length).toBeGreaterThan(0); + }); +}); diff --git a/web/e2e/specs/export.spec.ts b/web/e2e/specs/export.spec.ts new file mode 100644 index 000000000..a33e28955 --- /dev/null +++ b/web/e2e/specs/export.spec.ts @@ -0,0 +1,31 @@ +/** + * Export page tests -- HIGH tier. + * + * Tests export list, export cards, download/rename/delete actions, + * and the export dialog. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Export Page @high", () => { + test("export page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/export"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("empty state shows when no exports", async ({ frigateApp }) => { + await frigateApp.goto("/export"); + await frigateApp.page.waitForTimeout(2000); + // With empty exports mock, should show empty state + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("export page has filter controls", async ({ frigateApp }) => { + await frigateApp.goto("/export"); + // Should render buttons/controls + const buttons = frigateApp.page.locator("button"); + const count = await buttons.count(); + expect(count).toBeGreaterThanOrEqual(0); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/face-library.spec.ts b/web/e2e/specs/face-library.spec.ts new file mode 100644 index 000000000..44313036e --- /dev/null +++ b/web/e2e/specs/face-library.spec.ts @@ -0,0 +1,19 @@ +/** + * Face Library page tests -- MEDIUM tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Face Library @medium", () => { + test("face library page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/faces"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("face library shows empty state or content", async ({ frigateApp }) => { + await frigateApp.goto("/faces"); + await frigateApp.page.waitForTimeout(2000); + // With empty faces mock, should show empty state + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/live.spec.ts b/web/e2e/specs/live.spec.ts new file mode 100644 index 000000000..6b449ad67 --- /dev/null +++ b/web/e2e/specs/live.spec.ts @@ -0,0 +1,194 @@ +/** + * Live page tests -- CRITICAL tier. + * + * Tests camera dashboard, single camera view, camera groups, + * feature toggles, and context menus on both desktop and mobile. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Live Dashboard @critical", () => { + test("dashboard renders with camera grid", async ({ frigateApp }) => { + await frigateApp.goto("/"); + // Should see camera containers for each mock camera + const pageRoot = frigateApp.page.locator("#pageRoot"); + await expect(pageRoot).toBeVisible(); + // Check that camera names from config are referenced in the page + await expect( + frigateApp.page.locator("[data-camera='front_door']"), + ).toBeVisible({ timeout: 10_000 }); + await expect( + frigateApp.page.locator("[data-camera='backyard']"), + ).toBeVisible({ timeout: 10_000 }); + await expect(frigateApp.page.locator("[data-camera='garage']")).toBeVisible( + { timeout: 10_000 }, + ); + }); + + test("click camera enters single camera view", async ({ frigateApp }) => { + await frigateApp.goto("/"); + // Click the front_door camera card + const cameraCard = frigateApp.page + .locator("[data-camera='front_door']") + .first(); + await cameraCard.click({ timeout: 10_000 }); + // URL hash should change to include the camera name + await expect(frigateApp.page).toHaveURL(/#front_door/); + }); + + test("back button returns to dashboard from single camera", async ({ + frigateApp, + }) => { + // Navigate directly to single camera view via hash + await frigateApp.goto("/#front_door"); + // Wait for single camera view to render + await frigateApp.page.waitForTimeout(1000); + // Click back button + const backButton = frigateApp.page + .locator("button") + .filter({ + has: frigateApp.page.locator("svg"), + }) + .first(); + await backButton.click(); + // Should return to dashboard (hash cleared) + await frigateApp.page.waitForTimeout(1000); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("fullscreen toggle works", async ({ frigateApp }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + await frigateApp.goto("/"); + // The fullscreen button should be present (fixed position at bottom-right) + const fullscreenBtn = frigateApp.page.locator("button:has(svg)").last(); + await expect(fullscreenBtn).toBeVisible({ timeout: 10_000 }); + }); + + test("camera group selector is visible on live page", async ({ + frigateApp, + }) => { + if (frigateApp.isMobile) { + // On mobile, the camera group selector is in the header + await frigateApp.goto("/"); + // Just verify the page renders without crash + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + return; + } + await frigateApp.goto("/"); + // On desktop, camera group selector is in the sidebar below the Live nav item + await expect(frigateApp.page.locator("aside")).toBeVisible(); + }); + + test("page renders without crash when no cameras match group", async ({ + frigateApp, + }) => { + // Navigate to a non-existent camera group + await frigateApp.page.goto("/?group=nonexistent"); + await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 }); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("birdseye view accessible when enabled", async ({ frigateApp }) => { + // Birdseye is enabled in our default config + await frigateApp.goto("/#birdseye"); + await frigateApp.page.waitForTimeout(2000); + // Should not crash - either shows birdseye or falls back + const body = frigateApp.page.locator("body"); + await expect(body).toBeVisible(); + }); +}); + +test.describe("Live Camera Features @critical", () => { + test("single camera view renders with controls", async ({ frigateApp }) => { + await frigateApp.goto("/#front_door"); + await frigateApp.page.waitForTimeout(2000); + // The page should render without crash + await expect(frigateApp.page.locator("body")).toBeVisible(); + // Should have some buttons (back, fullscreen, settings, etc.) + const buttons = frigateApp.page.locator("button"); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + }); + + test("camera feature toggles are clickable", async ({ frigateApp }) => { + await frigateApp.goto("/#front_door"); + await frigateApp.page.waitForTimeout(2000); + // Find toggle/switch elements - FilterSwitch components + const switches = frigateApp.page.locator('button[role="switch"]'); + const count = await switches.count(); + if (count > 0) { + // Click the first switch to toggle it + await switches.first().click(); + // Should not crash + await expect(frigateApp.page.locator("body")).toBeVisible(); + } + }); + + test("keyboard shortcut f does not crash", async ({ frigateApp }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + await frigateApp.goto("/"); + // Press 'f' for fullscreen + await frigateApp.page.keyboard.press("f"); + await frigateApp.page.waitForTimeout(500); + // Should not crash + await expect(frigateApp.page.locator("body")).toBeVisible(); + }); +}); + +test.describe("Live Context Menu @critical", () => { + test("right-click on camera opens context menu (desktop)", async ({ + frigateApp, + }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + await frigateApp.goto("/"); + const cameraCard = frigateApp.page + .locator("[data-camera='front_door']") + .first(); + await cameraCard.waitFor({ state: "visible", timeout: 10_000 }); + // Right-click to open context menu + await cameraCard.click({ button: "right" }); + // Context menu should appear (Radix ContextMenu renders a portal) + const contextMenu = frigateApp.page.locator( + '[role="menu"], [data-radix-menu-content]', + ); + await expect(contextMenu.first()).toBeVisible({ timeout: 5_000 }); + }); +}); + +test.describe("Live Mobile @critical", () => { + test("mobile shows list layout by default", async ({ frigateApp }) => { + if (!frigateApp.isMobile) { + test.skip(); + return; + } + await frigateApp.goto("/"); + // On mobile, cameras render in a list (single column) + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + // Should have camera elements + await expect( + frigateApp.page.locator("[data-camera='front_door']"), + ).toBeVisible({ timeout: 10_000 }); + }); + + test("mobile camera click enters single view", async ({ frigateApp }) => { + if (!frigateApp.isMobile) { + test.skip(); + return; + } + await frigateApp.goto("/"); + const cameraCard = frigateApp.page + .locator("[data-camera='front_door']") + .first(); + await cameraCard.click({ timeout: 10_000 }); + await expect(frigateApp.page).toHaveURL(/#front_door/); + }); +}); diff --git a/web/e2e/specs/logs.spec.ts b/web/e2e/specs/logs.spec.ts new file mode 100644 index 000000000..32cf0bba9 --- /dev/null +++ b/web/e2e/specs/logs.spec.ts @@ -0,0 +1,29 @@ +/** + * Logs page tests -- MEDIUM tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Logs Page @medium", () => { + test("logs page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/logs"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("logs page has service toggle", async ({ frigateApp }) => { + await frigateApp.goto("/logs"); + await frigateApp.page.waitForTimeout(2000); + // Should have toggle buttons for frigate/go2rtc/nginx services + const toggleGroup = frigateApp.page.locator('[role="group"]'); + const count = await toggleGroup.count(); + expect(count).toBeGreaterThan(0); + }); + + test("logs page shows log content", async ({ frigateApp }) => { + await frigateApp.goto("/logs"); + await frigateApp.page.waitForTimeout(2000); + // Should display some text content (our mock returns log lines) + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text?.length).toBeGreaterThan(0); + }); +}); diff --git a/web/e2e/specs/navigation.spec.ts b/web/e2e/specs/navigation.spec.ts new file mode 100644 index 000000000..4fececf86 --- /dev/null +++ b/web/e2e/specs/navigation.spec.ts @@ -0,0 +1,198 @@ +/** + * Navigation tests -- CRITICAL tier. + * + * Tests sidebar (desktop) and bottombar (mobile) navigation, + * conditional nav items, settings menus, and route transitions. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { BasePage } from "../pages/base.page"; + +test.describe("Navigation @critical", () => { + test("app loads and renders page root", async ({ frigateApp }) => { + await frigateApp.goto("/"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("logo is visible and links to home", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + + if (!frigateApp.isMobile) { + // Desktop: logo in sidebar + const logo = base.sidebar.locator('a[href="/"]').first(); + await expect(logo).toBeVisible(); + } + }); + + test("Live nav item is active on root path", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const liveLink = frigateApp.page.locator('a[href="/"]').first(); + await expect(liveLink).toBeVisible(); + }); + + test("navigate to Review page", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + + await base.navigateTo("/review"); + await expect(frigateApp.page).toHaveURL(/\/review/); + }); + + test("navigate to Explore page", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + + await base.navigateTo("/explore"); + await expect(frigateApp.page).toHaveURL(/\/explore/); + }); + + test("navigate to Export page", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + + await base.navigateTo("/export"); + await expect(frigateApp.page).toHaveURL(/\/export/); + }); + + test("all primary nav links are present", async ({ frigateApp }) => { + await frigateApp.goto("/"); + + // Live, Review, Explore, Export are always present + await expect(frigateApp.page.locator('a[href="/"]').first()).toBeVisible(); + await expect( + frigateApp.page.locator('a[href="/review"]').first(), + ).toBeVisible(); + await expect( + frigateApp.page.locator('a[href="/explore"]').first(), + ).toBeVisible(); + await expect( + frigateApp.page.locator('a[href="/export"]').first(), + ).toBeVisible(); + }); + + test("desktop sidebar is visible on desktop, hidden on mobile", async ({ + frigateApp, + }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + + if (!frigateApp.isMobile) { + await expect(base.sidebar).toBeVisible(); + } else { + await expect(base.sidebar).not.toBeVisible(); + } + }); + + test("navigate between pages without crash", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + const pageRoot = frigateApp.page.locator("#pageRoot"); + + // Navigate through all main pages in sequence + await base.navigateTo("/review"); + await expect(pageRoot).toBeVisible({ timeout: 10_000 }); + + await base.navigateTo("/explore"); + await expect(pageRoot).toBeVisible({ timeout: 10_000 }); + + await base.navigateTo("/export"); + await expect(pageRoot).toBeVisible({ timeout: 10_000 }); + + // Navigate back to review (not root, to avoid same-route re-render issues) + await base.navigateTo("/review"); + await expect(pageRoot).toBeVisible({ timeout: 10_000 }); + }); + + test("unknown route redirects to home", async ({ frigateApp }) => { + // Navigate to an unknown route - React Router's catch-all should redirect + await frigateApp.page.goto("/nonexistent-route"); + // Wait for React to render and redirect + await frigateApp.page.waitForTimeout(2000); + // Should either be at root or show the page root (app didn't crash) + const url = frigateApp.page.url(); + const hasPageRoot = await frigateApp.page + .locator("#pageRoot") + .isVisible() + .catch(() => false); + expect(url.endsWith("/") || hasPageRoot).toBeTruthy(); + }); + + test("Faces nav hidden when face_recognition disabled", async ({ + frigateApp, + }) => { + // Default config has face_recognition.enabled = false + await frigateApp.goto("/"); + await expect(frigateApp.page.locator('a[href="/faces"]')).not.toBeVisible(); + }); + + test("Chat nav hidden when genai model is none", async ({ frigateApp }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + // Override config with genai.model = "none" to hide chat + await frigateApp.installDefaults({ + config: { + genai: { + enabled: false, + provider: "ollama", + model: "none", + base_url: "", + }, + }, + }); + await frigateApp.goto("/"); + await expect(frigateApp.page.locator('a[href="/chat"]')).not.toBeVisible(); + }); + + test("Faces nav visible when face_recognition enabled and admin on desktop", async ({ + frigateApp, + page, + }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + + // Re-install with face_recognition enabled + await frigateApp.installDefaults({ + config: { + face_recognition: { enabled: true }, + }, + }); + await frigateApp.goto("/"); + await expect(page.locator('a[href="/faces"]')).toBeVisible(); + }); + + test("Chat nav visible when genai model set and admin on desktop", async ({ + frigateApp, + page, + }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + + await frigateApp.installDefaults({ + config: { + genai: { enabled: true, model: "llava" }, + }, + }); + await frigateApp.goto("/"); + await expect(page.locator('a[href="/chat"]')).toBeVisible(); + }); + + test("Classification nav visible for admin on desktop", async ({ + frigateApp, + page, + }) => { + if (frigateApp.isMobile) { + test.skip(); + return; + } + + await frigateApp.goto("/"); + await expect(page.locator('a[href="/classification"]')).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/replay.spec.ts b/web/e2e/specs/replay.spec.ts new file mode 100644 index 000000000..8a46e7cdd --- /dev/null +++ b/web/e2e/specs/replay.spec.ts @@ -0,0 +1,13 @@ +/** + * Replay page tests -- LOW tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("Replay Page @low", () => { + test("replay page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/replay"); + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("body")).toBeVisible(); + }); +}); diff --git a/web/e2e/specs/review.spec.ts b/web/e2e/specs/review.spec.ts new file mode 100644 index 000000000..316b7bb43 --- /dev/null +++ b/web/e2e/specs/review.spec.ts @@ -0,0 +1,58 @@ +/** + * Review/Events page tests -- CRITICAL tier. + * + * Tests timeline, filters, event cards, video controls, + * and mobile-specific drawer interactions. + */ + +import { test, expect } from "../fixtures/frigate-test"; +import { BasePage } from "../pages/base.page"; + +test.describe("Review Page @critical", () => { + test("review page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("severity toggle group is visible", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + // The review page has a toggle group for alert/detection severity + const toggleGroup = frigateApp.page.locator('[role="group"]').first(); + await expect(toggleGroup).toBeVisible({ timeout: 10_000 }); + }); + + test("camera filter button is clickable", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + // Find a button that opens the camera filter + const filterButtons = frigateApp.page.locator("button"); + const count = await filterButtons.count(); + expect(count).toBeGreaterThan(0); + // Page should not crash after interaction + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("empty state shows when no events", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + // With empty reviews mock, should show some kind of content (not crash) + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("navigate to review from live page", async ({ frigateApp }) => { + await frigateApp.goto("/"); + const base = new BasePage(frigateApp.page, !frigateApp.isMobile); + await base.navigateTo("/review"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("review page has interactive controls", async ({ frigateApp }) => { + await frigateApp.goto("/review"); + await frigateApp.page.waitForTimeout(2000); + // Should have buttons/controls for filtering + const interactive = frigateApp.page.locator( + "button, input, [role='group']", + ); + const count = await interactive.count(); + expect(count).toBeGreaterThan(0); + }); +}); diff --git a/web/e2e/specs/settings/ui-settings.spec.ts b/web/e2e/specs/settings/ui-settings.spec.ts new file mode 100644 index 000000000..8eca700bb --- /dev/null +++ b/web/e2e/specs/settings/ui-settings.spec.ts @@ -0,0 +1,32 @@ +/** + * Settings page tests -- HIGH tier. + * + * Tests the Settings page renders without crash and + * basic navigation between settings sections. + */ + +import { test, expect } from "../../fixtures/frigate-test"; + +test.describe("Settings Page @high", () => { + test("settings page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/settings"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("settings page has navigation sections", async ({ frigateApp }) => { + await frigateApp.goto("/settings"); + await frigateApp.page.waitForTimeout(2000); + // Should have sidebar navigation or section links + const buttons = frigateApp.page.locator("button, a"); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + }); + + test("settings page shows content", async ({ frigateApp }) => { + await frigateApp.goto("/settings"); + await frigateApp.page.waitForTimeout(2000); + // The page should have meaningful content + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text?.length).toBeGreaterThan(0); + }); +}); diff --git a/web/e2e/specs/system.spec.ts b/web/e2e/specs/system.spec.ts new file mode 100644 index 000000000..217fce0fb --- /dev/null +++ b/web/e2e/specs/system.spec.ts @@ -0,0 +1,28 @@ +/** + * System page tests -- MEDIUM tier. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +test.describe("System Page @medium", () => { + test("system page renders without crash", async ({ frigateApp }) => { + await frigateApp.goto("/system"); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + }); + + test("system page has interactive controls", async ({ frigateApp }) => { + await frigateApp.goto("/system"); + await frigateApp.page.waitForTimeout(2000); + // Should have buttons for tab switching or other controls + const buttons = frigateApp.page.locator("button"); + const count = await buttons.count(); + expect(count).toBeGreaterThan(0); + }); + + test("system page shows metrics content", async ({ frigateApp }) => { + await frigateApp.goto("/system"); + await frigateApp.page.waitForTimeout(2000); + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text?.length).toBeGreaterThan(0); + }); +}); diff --git a/web/package-lock.json b/web/package-lock.json index 494498f30..baa7cc743 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -89,6 +89,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@tailwindcss/forms": "^0.5.9", "@testing-library/jest-dom": "^6.6.2", "@types/js-yaml": "^4.0.9", @@ -1485,6 +1486,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -11336,6 +11353,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", diff --git a/web/package.json b/web/package.json index 960b556ff..5f3f69a3e 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,10 @@ "prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"", "test": "vitest", "coverage": "vitest run --coverage", + "e2e:build": "tsc && vite build --base=/", + "e2e": "playwright test --config e2e/playwright.config.ts", + "e2e:ui": "playwright test --config e2e/playwright.config.ts --ui", + "e2e:headed": "playwright test --config e2e/playwright.config.ts --headed", "i18n:extract": "i18next-cli extract", "i18n:extract:ci": "i18next-cli extract --ci", "i18n:status": "i18next-cli status" @@ -98,6 +102,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@tailwindcss/forms": "^0.5.9", "@testing-library/jest-dom": "^6.6.2", "@types/js-yaml": "^4.0.9",