Compare commits

...

6 Commits

Author SHA1 Message Date
Nicolas Mowen
27625e75ac Make reviewed optional and add null to API call 2026-01-11 09:50:43 -07:00
Nicolas Mowen
b1d7187ef8 Handle options file for go2rtc option 2026-01-11 09:18:41 -07:00
Nicolas Mowen
3533578aef Strip model name before training 2026-01-11 09:15:50 -07:00
Blake Blackshear
7b5a1b7284 ensure cloudflare pages are indexed by google (#21606)
* ensure cloudflare pages are indexed by google

* avoid indexing dev-docs as well
2026-01-11 08:48:03 -07:00
Dermot Duffy
7e5d98dbab fix: Correctly apply API filter for "reviewed" (#21600) 2026-01-11 06:42:33 -07:00
Josh Hawkins
d952a97bda reduce gif size for docs assets changes (#21594) 2026-01-10 12:59:15 -07:00
10 changed files with 103 additions and 11 deletions

View File

@@ -23,8 +23,28 @@ sys.path.remove("/opt/frigate")
yaml = YAML()
# Check if arbitrary exec sources are allowed (defaults to False for security)
ALLOW_ARBITRARY_EXEC = os.environ.get(
"GO2RTC_ALLOW_ARBITRARY_EXEC", "false"
allow_arbitrary_exec = None
if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ:
allow_arbitrary_exec = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC")
elif (
os.path.isdir("/run/secrets")
and os.access("/run/secrets", os.R_OK)
and "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.listdir("/run/secrets")
):
allow_arbitrary_exec = (
Path(os.path.join("/run/secrets", "GO2RTC_ALLOW_ARBITRARY_EXEC"))
.read_text()
.strip()
)
# check for the add-on options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:
raw_options = f.read()
options = json.loads(raw_options)
allow_arbitrary_exec = options.get("go2rtc_allow_arbitrary_exec")
ALLOW_ARBITRARY_EXEC = allow_arbitrary_exec is not None and str(
allow_arbitrary_exec
).lower() in ("true", "1", "yes")
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}

8
docs/static/_headers vendored Normal file
View File

@@ -0,0 +1,8 @@
https://:project.pages.dev/*
X-Robots-Tag: noindex
https://:version.:project.pages.dev/*
X-Robots-Tag: noindex
https://docs-dev.frigate.video/*
X-Robots-Tag: noindex

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 MiB

After

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -10,7 +10,7 @@ class ReviewQueryParams(BaseModel):
cameras: str = "all"
labels: str = "all"
zones: str = "all"
reviewed: int = 0
reviewed: Union[int, SkipJsonSchema[None]] = None
limit: Union[int, SkipJsonSchema[None]] = None
severity: Union[SeverityEnum, SkipJsonSchema[None]] = None
before: Union[float, SkipJsonSchema[None]] = None

View File

@@ -144,6 +144,8 @@ async def review(
(UserReviewStatus.has_been_reviewed == False)
| (UserReviewStatus.has_been_reviewed.is_null())
)
elif reviewed == 1:
review_query = review_query.where(UserReviewStatus.has_been_reviewed == True)
# Apply ordering and limit
review_query = (

View File

@@ -171,8 +171,8 @@ class BaseTestHttp(unittest.TestCase):
def insert_mock_event(
self,
id: str,
start_time: float = datetime.datetime.now().timestamp(),
end_time: float = datetime.datetime.now().timestamp() + 20,
start_time: float | None = None,
end_time: float | None = None,
has_clip: bool = True,
top_score: int = 100,
score: int = 0,
@@ -180,6 +180,11 @@ class BaseTestHttp(unittest.TestCase):
camera: str = "front_door",
) -> Event:
"""Inserts a basic event model with a given id."""
if start_time is None:
start_time = datetime.datetime.now().timestamp()
if end_time is None:
end_time = start_time + 20
return Event.insert(
id=id,
label="Mock",
@@ -229,11 +234,16 @@ class BaseTestHttp(unittest.TestCase):
def insert_mock_recording(
self,
id: str,
start_time: float = datetime.datetime.now().timestamp(),
end_time: float = datetime.datetime.now().timestamp() + 20,
start_time: float | None = None,
end_time: float | None = None,
motion: int = 0,
) -> Event:
"""Inserts a recording model with a given id."""
if start_time is None:
start_time = datetime.datetime.now().timestamp()
if end_time is None:
end_time = start_time + 20
return Recordings.insert(
id=id,
path=id,

View File

@@ -96,16 +96,17 @@ class TestHttpApp(BaseTestHttp):
assert len(events) == 0
def test_get_event_list_limit(self):
now = datetime.now().timestamp()
id = "123456.random"
id2 = "54321.random"
with AuthTestClient(self.app) as client:
super().insert_mock_event(id)
super().insert_mock_event(id, start_time=now + 1)
events = client.get("/events").json()
assert len(events) == 1
assert events[0]["id"] == id
super().insert_mock_event(id2)
super().insert_mock_event(id2, start_time=now)
events = client.get("/events").json()
assert len(events) == 2
@@ -144,7 +145,7 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id2
assert events[1]["id"] == id
events = client.get("/events", params={"sort": "score_des"}).json()
events = client.get("/events", params={"sort": "score_desc"}).json()
assert len(events) == 2
assert events[0]["id"] == id
assert events[1]["id"] == id2

View File

@@ -196,6 +196,50 @@ class TestHttpReview(BaseTestHttp):
assert len(response_json) == 1
assert response_json[0]["id"] == id
def test_get_review_with_reviewed_filter_unreviewed(self):
"""Test that reviewed=0 returns only unreviewed items."""
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
id_unreviewed = "123456.unreviewed"
id_reviewed = "123456.reviewed"
super().insert_mock_review_segment(id_unreviewed, now, now + 2)
super().insert_mock_review_segment(id_reviewed, now, now + 2)
self._insert_user_review_status(id_reviewed, reviewed=True)
params = {
"reviewed": 0,
"after": now - 1,
"before": now + 3,
}
response = client.get("/review", params=params)
assert response.status_code == 200
response_json = response.json()
assert len(response_json) == 1
assert response_json[0]["id"] == id_unreviewed
def test_get_review_with_reviewed_filter_reviewed(self):
"""Test that reviewed=1 returns only reviewed items."""
now = datetime.now().timestamp()
with AuthTestClient(self.app) as client:
id_unreviewed = "123456.unreviewed"
id_reviewed = "123456.reviewed"
super().insert_mock_review_segment(id_unreviewed, now, now + 2)
super().insert_mock_review_segment(id_reviewed, now, now + 2)
self._insert_user_review_status(id_reviewed, reviewed=True)
params = {
"reviewed": 1,
"after": now - 1,
"before": now + 3,
}
response = client.get("/review", params=params)
assert response.status_code == 200
response_json = response.json()
assert len(response_json) == 1
assert response_json[0]["id"] == id_reviewed
####################################################################################################################
################################### GET /review/summary Endpoint #################################################
####################################################################################################################

View File

@@ -43,6 +43,7 @@ def write_training_metadata(model_name: str, image_count: int) -> None:
model_name: Name of the classification model
image_count: Number of images used in training
"""
model_name = model_name.strip()
clips_model_dir = os.path.join(CLIPS_DIR, model_name)
os.makedirs(clips_model_dir, exist_ok=True)
@@ -70,6 +71,7 @@ def read_training_metadata(model_name: str) -> dict[str, any] | None:
Returns:
Dictionary with last_training_date and last_training_image_count, or None if not found
"""
model_name = model_name.strip()
clips_model_dir = os.path.join(CLIPS_DIR, model_name)
metadata_path = os.path.join(clips_model_dir, TRAINING_METADATA_FILE)
@@ -95,6 +97,7 @@ def get_dataset_image_count(model_name: str) -> int:
Returns:
Total count of images across all categories
"""
model_name = model_name.strip()
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
if not os.path.exists(dataset_dir):
@@ -126,6 +129,7 @@ class ClassificationTrainingProcess(FrigateProcess):
"TF_KERAS_MOBILENET_V2_WEIGHTS_URL",
"",
)
model_name = model_name.strip()
super().__init__(
stop_event=None,
priority=PROCESS_PRIORITY_LOW,
@@ -292,6 +296,7 @@ class ClassificationTrainingProcess(FrigateProcess):
def kickoff_model_training(
embeddingRequestor: EmbeddingsRequestor, model_name: str
) -> None:
model_name = model_name.strip()
requestor = InterProcessRequestor()
requestor.send_data(
UPDATE_MODEL_STATE,
@@ -359,6 +364,7 @@ def collect_state_classification_examples(
model_name: Name of the classification model
cameras: Dict mapping camera names to normalized crop coordinates [x1, y1, x2, y2] (0-1)
"""
model_name = model_name.strip()
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
# Step 1: Get review items for the cameras
@@ -714,6 +720,7 @@ def collect_object_classification_examples(
model_name: Name of the classification model
label: Object label to collect (e.g., "person", "car")
"""
model_name = model_name.strip()
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
temp_dir = os.path.join(dataset_dir, "temp")
os.makedirs(temp_dir, exist_ok=True)

View File

@@ -205,7 +205,7 @@ export default function Events() {
cameras: reviewSearchParams["cameras"],
labels: reviewSearchParams["labels"],
zones: reviewSearchParams["zones"],
reviewed: 1,
reviewed: null, // We want both reviewed and unreviewed items as we filter in the UI
before: reviewSearchParams["before"] || last24Hours.before,
after: reviewSearchParams["after"] || last24Hours.after,
};