mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Compare commits
19 Commits
4cbbb5d9dd
...
84105be5e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84105be5e9 | ||
|
|
27ed1ad4e1 | ||
|
|
d7200fec4c | ||
|
|
ace71505ca | ||
|
|
52717b62de | ||
|
|
3faefa5db2 | ||
|
|
09b3f3cf66 | ||
|
|
06e72454bd | ||
|
|
bde3b5d6db | ||
|
|
eb9758583a | ||
|
|
364e1620b6 | ||
|
|
c98d3a1aaf | ||
|
|
0bcf171299 | ||
|
|
db886f85e7 | ||
|
|
76d2cc853f | ||
|
|
5e38da8fb1 | ||
|
|
672e91d24f | ||
|
|
ccefc79499 | ||
|
|
00720f6b31 |
@ -111,9 +111,3 @@ review:
|
|||||||
## Review Reports
|
## Review Reports
|
||||||
|
|
||||||
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
|
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.
|
||||||
|
|
||||||
### Requesting Reports Programmatically
|
|
||||||
|
|
||||||
Review reports can be requested via the [API](/integrations/api#review-summarization) by sending a POST request to `/api/review/summarize/start/{start_ts}/end/{end_ts}` with Unix timestamps.
|
|
||||||
|
|
||||||
For Home Assistant users, there is a built-in service (`frigate.generate_review_summary`) that makes it easy to request review reports as part of automations or scripts. This allows you to automatically generate daily summaries, vacation reports, or custom time period reports based on your specific needs.
|
|
||||||
|
|||||||
@ -107,7 +107,7 @@ Fine-tune the LPR feature using these optional parameters at the global level of
|
|||||||
|
|
||||||
### Normalization Rules
|
### Normalization Rules
|
||||||
|
|
||||||
- **`replace_rules`**: List of regex replacement rules to normalize detected plates. These rules are applied sequentially and are applied _before_ the `format` regex, if specified. Each rule must have a `pattern` (which can be a string or a regex, prepended by `r`) and `replacement` (a string, which also supports [backrefs](https://docs.python.org/3/library/re.html#re.sub) like `\1`). These rules are useful for dealing with common OCR issues like noise characters, separators, or confusions (e.g., 'O'→'0').
|
- **`replace_rules`**: List of regex replacement rules to normalize detected plates. These rules are applied sequentially. Each rule must have a `pattern` (which can be a string or a regex, prepended by `r`) and `replacement` (a string, which also supports [backrefs](https://docs.python.org/3/library/re.html#re.sub) like `\1`). These rules are useful for dealing with common OCR issues like noise characters, separators, or confusions (e.g., 'O'→'0').
|
||||||
|
|
||||||
These rules must be defined at the global level of your `lpr` config.
|
These rules must be defined at the global level of your `lpr` config.
|
||||||
|
|
||||||
|
|||||||
@ -164,6 +164,12 @@ A Tensorflow Lite is provided in the container at `/openvino-model/ssdlite_mobil
|
|||||||
<details>
|
<details>
|
||||||
<summary>YOLOv9 Setup & Config</summary>
|
<summary>YOLOv9 Setup & Config</summary>
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
If you are using a Frigate+ YOLOv9 model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
After placing the downloaded files for the tflite model and labels in your config folder, you can use the following configuration:
|
After placing the downloaded files for the tflite model and labels in your config folder, you can use the following configuration:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -402,7 +408,7 @@ The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv
|
|||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
|
If you are using a Frigate+ YOLOv9 model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@ -742,7 +748,7 @@ The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv
|
|||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
|
If you are using a Frigate+ YOLOv9 model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
1135
docs/static/frigate-api.yaml
vendored
1135
docs/static/frigate-api.yaml
vendored
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,7 @@ class EventsDescriptionBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class EventsCreateBody(BaseModel):
|
class EventsCreateBody(BaseModel):
|
||||||
|
source_type: Optional[str] = "api"
|
||||||
sub_label: Optional[str] = None
|
sub_label: Optional[str] = None
|
||||||
score: Optional[float] = 0
|
score: Optional[float] = 0
|
||||||
duration: Optional[int] = 30
|
duration: Optional[int] = 30
|
||||||
|
|||||||
@ -346,7 +346,7 @@ def events(
|
|||||||
"/events/explore",
|
"/events/explore",
|
||||||
response_model=list[EventResponse],
|
response_model=list[EventResponse],
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Get summary of objects",
|
summary="Get summary of objects.",
|
||||||
description="""Gets a summary of objects from the database.
|
description="""Gets a summary of objects from the database.
|
||||||
Returns a list of objects with a max of `limit` objects for each label.
|
Returns a list of objects with a max of `limit` objects for each label.
|
||||||
""",
|
""",
|
||||||
@ -439,7 +439,7 @@ def events_explore(
|
|||||||
"/event_ids",
|
"/event_ids",
|
||||||
response_model=list[EventResponse],
|
response_model=list[EventResponse],
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Get events by ids",
|
summary="Get events by ids.",
|
||||||
description="""Gets events by a list of ids.
|
description="""Gets events by a list of ids.
|
||||||
Returns a list of events.
|
Returns a list of events.
|
||||||
""",
|
""",
|
||||||
@ -473,7 +473,7 @@ async def event_ids(ids: str, request: Request):
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/events/search",
|
"/events/search",
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Search events",
|
summary="Search events.",
|
||||||
description="""Searches for events in the database.
|
description="""Searches for events in the database.
|
||||||
Returns a list of events.
|
Returns a list of events.
|
||||||
""",
|
""",
|
||||||
@ -924,7 +924,7 @@ def events_summary(
|
|||||||
"/events/{event_id}",
|
"/events/{event_id}",
|
||||||
response_model=EventResponse,
|
response_model=EventResponse,
|
||||||
dependencies=[Depends(allow_any_authenticated())],
|
dependencies=[Depends(allow_any_authenticated())],
|
||||||
summary="Get event by id",
|
summary="Get event by id.",
|
||||||
description="Gets an event by its id.",
|
description="Gets an event by its id.",
|
||||||
)
|
)
|
||||||
async def event(event_id: str, request: Request):
|
async def event(event_id: str, request: Request):
|
||||||
@ -968,7 +968,7 @@ def set_retain(event_id: str):
|
|||||||
"/events/{event_id}/plus",
|
"/events/{event_id}/plus",
|
||||||
response_model=EventUploadPlusResponse,
|
response_model=EventUploadPlusResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Send event to Frigate+",
|
summary="Send event to Frigate+.",
|
||||||
description="""Sends an event to Frigate+.
|
description="""Sends an event to Frigate+.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1207,7 +1207,7 @@ async def false_positive(request: Request, event_id: str):
|
|||||||
"/events/{event_id}/retain",
|
"/events/{event_id}/retain",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Stop event from being retained indefinitely",
|
summary="Stop event from being retained indefinitely.",
|
||||||
description="""Stops an event from being retained indefinitely.
|
description="""Stops an event from being retained indefinitely.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
NOTE: This is a legacy endpoint and is not supported in the frontend.
|
NOTE: This is a legacy endpoint and is not supported in the frontend.
|
||||||
@ -1236,7 +1236,7 @@ async def delete_retain(event_id: str, request: Request):
|
|||||||
"/events/{event_id}/sub_label",
|
"/events/{event_id}/sub_label",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Set event sub label",
|
summary="Set event sub label.",
|
||||||
description="""Sets an event's sub label.
|
description="""Sets an event's sub label.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1295,7 +1295,7 @@ async def set_sub_label(
|
|||||||
"/events/{event_id}/recognized_license_plate",
|
"/events/{event_id}/recognized_license_plate",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Set event license plate",
|
summary="Set event license plate.",
|
||||||
description="""Sets an event's license plate.
|
description="""Sets an event's license plate.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1355,7 +1355,7 @@ async def set_plate(
|
|||||||
"/events/{event_id}/description",
|
"/events/{event_id}/description",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Set event description",
|
summary="Set event description.",
|
||||||
description="""Sets an event's description.
|
description="""Sets an event's description.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1411,7 +1411,7 @@ async def set_description(
|
|||||||
"/events/{event_id}/description/regenerate",
|
"/events/{event_id}/description/regenerate",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Regenerate event description",
|
summary="Regenerate event description.",
|
||||||
description="""Regenerates an event's description.
|
description="""Regenerates an event's description.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1463,8 +1463,8 @@ async def regenerate_description(
|
|||||||
@router.post(
|
@router.post(
|
||||||
"/description/generate",
|
"/description/generate",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
# dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Generate description embedding",
|
summary="Generate description embedding.",
|
||||||
description="""Generates an embedding for an event's description.
|
description="""Generates an embedding for an event's description.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1529,7 +1529,7 @@ async def delete_single_event(event_id: str, request: Request) -> dict:
|
|||||||
"/events/{event_id}",
|
"/events/{event_id}",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Delete event",
|
summary="Delete event.",
|
||||||
description="""Deletes an event from the database.
|
description="""Deletes an event from the database.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
""",
|
""",
|
||||||
@ -1544,7 +1544,7 @@ async def delete_event(request: Request, event_id: str):
|
|||||||
"/events/",
|
"/events/",
|
||||||
response_model=EventMultiDeleteResponse,
|
response_model=EventMultiDeleteResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Delete events",
|
summary="Delete events.",
|
||||||
description="""Deletes a list of events from the database.
|
description="""Deletes a list of events from the database.
|
||||||
Returns a success message or an error if the events are not found.
|
Returns a success message or an error if the events are not found.
|
||||||
""",
|
""",
|
||||||
@ -1578,7 +1578,7 @@ async def delete_events(request: Request, body: EventsDeleteBody):
|
|||||||
"/events/{camera_name}/{label}/create",
|
"/events/{camera_name}/{label}/create",
|
||||||
response_model=EventCreateResponse,
|
response_model=EventCreateResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Create manual event",
|
summary="Create manual event.",
|
||||||
description="""Creates a manual event in the database.
|
description="""Creates a manual event in the database.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
NOTES:
|
NOTES:
|
||||||
@ -1620,7 +1620,7 @@ def create_event(
|
|||||||
body.score,
|
body.score,
|
||||||
body.sub_label,
|
body.sub_label,
|
||||||
body.duration,
|
body.duration,
|
||||||
"api",
|
body.source_type,
|
||||||
body.draw,
|
body.draw,
|
||||||
),
|
),
|
||||||
EventMetadataTypeEnum.manual_event_create.value,
|
EventMetadataTypeEnum.manual_event_create.value,
|
||||||
@ -1642,7 +1642,7 @@ def create_event(
|
|||||||
"/events/{event_id}/end",
|
"/events/{event_id}/end",
|
||||||
response_model=GenericResponse,
|
response_model=GenericResponse,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="End manual event",
|
summary="End manual event.",
|
||||||
description="""Ends a manual event.
|
description="""Ends a manual event.
|
||||||
Returns a success message or an error if the event is not found.
|
Returns a success message or an error if the event is not found.
|
||||||
NOTE: This should only be used for manual events.
|
NOTE: This should only be used for manual events.
|
||||||
@ -1652,27 +1652,10 @@ async def end_event(request: Request, event_id: str, body: EventsEndBody):
|
|||||||
try:
|
try:
|
||||||
event: Event = Event.get(Event.id == event_id)
|
event: Event = Event.get(Event.id == event_id)
|
||||||
await require_camera_access(event.camera, request=request)
|
await require_camera_access(event.camera, request=request)
|
||||||
|
|
||||||
if body.end_time is not None and body.end_time < event.start_time:
|
|
||||||
return JSONResponse(
|
|
||||||
content=(
|
|
||||||
{
|
|
||||||
"success": False,
|
|
||||||
"message": f"end_time ({body.end_time}) cannot be before start_time ({event.start_time}).",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_time = body.end_time or datetime.datetime.now().timestamp()
|
end_time = body.end_time or datetime.datetime.now().timestamp()
|
||||||
request.app.event_metadata_updater.publish(
|
request.app.event_metadata_updater.publish(
|
||||||
(event_id, end_time), EventMetadataTypeEnum.manual_event_end.value
|
(event_id, end_time), EventMetadataTypeEnum.manual_event_end.value
|
||||||
)
|
)
|
||||||
except DoesNotExist:
|
|
||||||
return JSONResponse(
|
|
||||||
content=({"success": False, "message": f"Event {event_id} not found."}),
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=(
|
content=(
|
||||||
@ -1691,7 +1674,7 @@ async def end_event(request: Request, event_id: str, body: EventsEndBody):
|
|||||||
"/trigger/embedding",
|
"/trigger/embedding",
|
||||||
response_model=dict,
|
response_model=dict,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Create trigger embedding",
|
summary="Create trigger embedding.",
|
||||||
description="""Creates a trigger embedding for a specific trigger.
|
description="""Creates a trigger embedding for a specific trigger.
|
||||||
Returns a success message or an error if the trigger is not found.
|
Returns a success message or an error if the trigger is not found.
|
||||||
""",
|
""",
|
||||||
@ -1849,7 +1832,7 @@ def create_trigger_embedding(
|
|||||||
"/trigger/embedding/{camera_name}/{name}",
|
"/trigger/embedding/{camera_name}/{name}",
|
||||||
response_model=dict,
|
response_model=dict,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Update trigger embedding",
|
summary="Update trigger embedding.",
|
||||||
description="""Updates a trigger embedding for a specific trigger.
|
description="""Updates a trigger embedding for a specific trigger.
|
||||||
Returns a success message or an error if the trigger is not found.
|
Returns a success message or an error if the trigger is not found.
|
||||||
""",
|
""",
|
||||||
@ -2014,7 +1997,7 @@ def update_trigger_embedding(
|
|||||||
"/trigger/embedding/{camera_name}/{name}",
|
"/trigger/embedding/{camera_name}/{name}",
|
||||||
response_model=dict,
|
response_model=dict,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Delete trigger embedding",
|
summary="Delete trigger embedding.",
|
||||||
description="""Deletes a trigger embedding for a specific trigger.
|
description="""Deletes a trigger embedding for a specific trigger.
|
||||||
Returns a success message or an error if the trigger is not found.
|
Returns a success message or an error if the trigger is not found.
|
||||||
""",
|
""",
|
||||||
@ -2088,7 +2071,7 @@ def delete_trigger_embedding(
|
|||||||
"/triggers/status/{camera_name}",
|
"/triggers/status/{camera_name}",
|
||||||
response_model=dict,
|
response_model=dict,
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
summary="Get triggers status",
|
summary="Get triggers status.",
|
||||||
description="""Gets the status of all triggers for a specific camera.
|
description="""Gets the status of all triggers for a specific camera.
|
||||||
Returns a success message or an error if the camera is not found.
|
Returns a success message or an error if the camera is not found.
|
||||||
""",
|
""",
|
||||||
|
|||||||
@ -209,22 +209,10 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
f"Found GenAI Review Summary request for {start_ts} to {end_ts}"
|
f"Found GenAI Review Summary request for {start_ts} to {end_ts}"
|
||||||
)
|
)
|
||||||
|
items: list[dict[str, Any]] = [
|
||||||
# Query all review segments with camera and time information
|
r["data"]["metadata"]
|
||||||
segments: list[dict[str, Any]] = [
|
|
||||||
{
|
|
||||||
"camera": r["camera"].replace("_", " ").title(),
|
|
||||||
"start_time": r["start_time"],
|
|
||||||
"end_time": r["end_time"],
|
|
||||||
"metadata": r["data"]["metadata"],
|
|
||||||
}
|
|
||||||
for r in (
|
for r in (
|
||||||
ReviewSegment.select(
|
ReviewSegment.select(ReviewSegment.data)
|
||||||
ReviewSegment.camera,
|
|
||||||
ReviewSegment.start_time,
|
|
||||||
ReviewSegment.end_time,
|
|
||||||
ReviewSegment.data,
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(ReviewSegment.data["metadata"].is_null(False))
|
(ReviewSegment.data["metadata"].is_null(False))
|
||||||
& (ReviewSegment.start_time < end_ts)
|
& (ReviewSegment.start_time < end_ts)
|
||||||
@ -236,65 +224,20 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
if len(segments) == 0:
|
if len(items) == 0:
|
||||||
logger.debug("No review items with metadata found during time period")
|
logger.debug("No review items with metadata found during time period")
|
||||||
return "No activity was found during this time period."
|
return "No activity was found during this time."
|
||||||
|
|
||||||
# Identify primary items (important items that need review)
|
important_items = list(
|
||||||
primary_segments = [
|
filter(
|
||||||
seg
|
lambda item: item.get("potential_threat_level", 0) > 0
|
||||||
for seg in segments
|
or item.get("other_concerns"),
|
||||||
if seg["metadata"].get("potential_threat_level", 0) > 0
|
items,
|
||||||
or seg["metadata"].get("other_concerns")
|
|
||||||
]
|
|
||||||
|
|
||||||
if not primary_segments:
|
|
||||||
return "No concerns were found during this time period."
|
|
||||||
|
|
||||||
# For each primary segment, find overlapping contextual items from other cameras
|
|
||||||
all_items_for_summary = []
|
|
||||||
|
|
||||||
for primary_seg in primary_segments:
|
|
||||||
# Add the primary item with marker
|
|
||||||
primary_item = copy.deepcopy(primary_seg["metadata"])
|
|
||||||
primary_item["_is_primary"] = True
|
|
||||||
primary_item["_camera"] = primary_seg["camera"]
|
|
||||||
all_items_for_summary.append(primary_item)
|
|
||||||
|
|
||||||
# Find overlapping contextual items from other cameras
|
|
||||||
primary_start = primary_seg["start_time"]
|
|
||||||
primary_end = primary_seg["end_time"]
|
|
||||||
primary_camera = primary_seg["camera"]
|
|
||||||
|
|
||||||
for seg in segments:
|
|
||||||
seg_camera = seg["camera"]
|
|
||||||
|
|
||||||
if seg_camera == primary_camera:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if seg in primary_segments:
|
|
||||||
continue
|
|
||||||
|
|
||||||
seg_start = seg["start_time"]
|
|
||||||
seg_end = seg["end_time"]
|
|
||||||
|
|
||||||
if seg_start < primary_end and primary_start < seg_end:
|
|
||||||
contextual_item = copy.deepcopy(seg["metadata"])
|
|
||||||
contextual_item["_is_primary"] = False
|
|
||||||
contextual_item["_camera"] = seg_camera
|
|
||||||
contextual_item["_related_to_camera"] = primary_camera
|
|
||||||
|
|
||||||
if not any(
|
|
||||||
item.get("_camera") == seg_camera
|
|
||||||
and item.get("time") == contextual_item.get("time")
|
|
||||||
for item in all_items_for_summary
|
|
||||||
):
|
|
||||||
all_items_for_summary.append(contextual_item)
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Summary includes {len(primary_segments)} primary items and "
|
|
||||||
f"{len(all_items_for_summary) - len(primary_segments)} contextual items"
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not important_items:
|
||||||
|
return "No concerns were found during this time period."
|
||||||
|
|
||||||
if self.config.review.genai.debug_save_thumbnails:
|
if self.config.review.genai.debug_save_thumbnails:
|
||||||
Path(
|
Path(
|
||||||
@ -304,7 +247,7 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
|||||||
return self.genai_client.generate_review_summary(
|
return self.genai_client.generate_review_summary(
|
||||||
start_ts,
|
start_ts,
|
||||||
end_ts,
|
end_ts,
|
||||||
all_items_for_summary,
|
important_items,
|
||||||
self.config.review.genai.debug_save_thumbnails,
|
self.config.review.genai.debug_save_thumbnails,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -185,66 +185,44 @@ Each line represents a detection state, not necessarily unique individuals. Pare
|
|||||||
timeline_summary_prompt = f"""
|
timeline_summary_prompt = f"""
|
||||||
You are a security officer.
|
You are a security officer.
|
||||||
Time range: {time_range}.
|
Time range: {time_range}.
|
||||||
Input: JSON list with "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "_is_primary", "_camera".
|
Input: JSON list with "title", "scene", "confidence", "potential_threat_level" (1-2), "other_concerns".
|
||||||
|
|
||||||
Task: Write a concise, human-presentable security report in markdown format.
|
Task: Write a concise, human-presentable security report in markdown format.
|
||||||
|
|
||||||
CRITICAL - Understanding Primary vs Contextual Items:
|
|
||||||
- Items with "_is_primary": true are events that REQUIRE REVIEW and MUST be included in the report
|
|
||||||
- Items with "_is_primary": false are additional context from other camera perspectives that overlap in time
|
|
||||||
- **DO NOT create separate bullet points or sections for contextual items**
|
|
||||||
- **ONLY use contextual items to enrich and inform the description of primary items**
|
|
||||||
- The "_camera" field indicates which camera captured each event
|
|
||||||
- **When a contextual item provides relevant background, you MUST incorporate it directly into the primary event's bullet point**
|
|
||||||
- Contextual information often explains or de-escalates seemingly suspicious primary events
|
|
||||||
|
|
||||||
Rules for the report:
|
Rules for the report:
|
||||||
|
|
||||||
- Title & overview
|
- Title & overview
|
||||||
- Start with:
|
- Start with:
|
||||||
# Security Summary - {time_range}
|
# Security Summary - {time_range}
|
||||||
- Write a 1-2 sentence situational overview capturing the general pattern of the period.
|
- Write a 1-2 sentence situational overview capturing the general pattern of the period.
|
||||||
- Keep the overview high-level; specific details will be in the event bullets below.
|
|
||||||
|
|
||||||
- Event details
|
- Event details
|
||||||
- **ONLY create bullet points for PRIMARY items (_is_primary: true)**
|
- Present events in chronological order as a bullet list.
|
||||||
- **Do NOT create sections or bullets for events that don't exist**
|
- **If multiple events occur within the same minute or overlapping time range, COMBINE them into a single bullet.**
|
||||||
- Do NOT create separate bullets for contextual items
|
- Summarize the distinct activities as sub-points under the shared timestamp.
|
||||||
- Present primary events in chronological order as a bullet list.
|
- If no timestamp is given, preserve order but label as “Time not specified.”
|
||||||
- **CRITICAL: When contextual items overlap with a primary event, you MUST weave that information directly into the same bullet point**
|
|
||||||
- Format: **[Timestamp]** - [Description incorporating any contextual information]. [Camera info]. (threat level: X)
|
|
||||||
- If contextual information provides an explanation (e.g., delivery truck → person is likely delivery driver), reflect this understanding in your description and potentially adjust the perceived threat level
|
|
||||||
- If multiple PRIMARY events occur within the same minute, combine them into a single bullet with sub-points.
|
|
||||||
- Use bold timestamps for clarity.
|
- Use bold timestamps for clarity.
|
||||||
- Camera format: "Camera: [camera name]" or mention contextual cameras inline when relevant
|
- Group bullets under subheadings when multiple events fall into the same category (e.g., Vehicle Activity, Porch Activity, Unusual Behavior).
|
||||||
- Group bullets under subheadings ONLY when you have actual PRIMARY events to list (e.g., Porch Activity, Unusual Behavior).
|
|
||||||
|
|
||||||
- Threat levels
|
- Threat levels
|
||||||
- Show the threat level for PRIMARY events using these labels:
|
- Always show the threat level for each event using these labels:
|
||||||
- Threat level 0: "Normal"
|
- Threat level 0: "Normal"
|
||||||
- Threat level 1: "Needs review"
|
- Threat level 1: "Needs review"
|
||||||
- Threat level 2: "Security concern"
|
- Threat level 2: "Security concern"
|
||||||
- Format as (threat level: Normal), (threat level: Needs review), or (threat level: Security concern).
|
- Format as (threat level: Normal), (threat level: Needs review), or (threat level: Security concern).
|
||||||
- **When contextual items clearly explain a primary event (e.g., delivery truck explains person at door), you should describe it as normal activity and note the explanation**
|
- If multiple events at the same time share the same threat level, only state it once.
|
||||||
- **Your description and tone should reflect the fuller understanding provided by contextual information**
|
|
||||||
- Example: Primary event says "unidentified person with face covering" but context shows delivery truck → describe as "delivery person (truck visible on Front Driveway Cam)" rather than emphasizing suspicious elements
|
|
||||||
- The stored threat level remains as originally classified, but your narrative should reflect the contextual understanding
|
|
||||||
- If multiple PRIMARY events at the same time share the same threat level, only state it once.
|
|
||||||
|
|
||||||
- Final assessment
|
- Final assessment
|
||||||
- End with a Final Assessment section.
|
- End with a Final Assessment section.
|
||||||
- If all primary events are threat level 0 or explained by contextual items:
|
- If all events are threat level 0:
|
||||||
Final assessment: Only normal residential activity observed during this period.
|
Final assessment: Only normal residential activity observed during this period.
|
||||||
- If threat level 1 events are present:
|
- If threat level 1 events are present:
|
||||||
Final assessment: Some activity requires review but no security concerns identified.
|
Final assessment: Some activity requires review but no security concerns identified.
|
||||||
- If threat level 2 events are present, clearly summarize them as Security concerns requiring immediate attention.
|
- If threat level 2 events are present, clearly summarize them as Security concerns requiring immediate attention.
|
||||||
- Keep this section brief - do not repeat details from the event descriptions above.
|
|
||||||
|
|
||||||
- Conciseness
|
- Conciseness
|
||||||
- Do not repeat benign clothing/appearance details unless they distinguish individuals.
|
- Do not repeat benign clothing/appearance details unless they distinguish individuals.
|
||||||
- Summarize similar routine events instead of restating full scene descriptions.
|
- Summarize similar routine events instead of restating full scene descriptions.
|
||||||
- When incorporating contextual information, do so briefly and naturally within the primary event description.
|
|
||||||
- Avoid lengthy explanatory notes - integrate context seamlessly into the narrative.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for item in segments:
|
for item in segments:
|
||||||
|
|||||||
@ -156,7 +156,7 @@
|
|||||||
"modelCreated": "El model s'ha creat correctament. Utilitzeu la vista Classificacions recents per a afegir imatges per als estats que falten i, a continuació, entrenar el model.",
|
"modelCreated": "El model s'ha creat correctament. Utilitzeu la vista Classificacions recents per a afegir imatges per als estats que falten i, a continuació, entrenar el model.",
|
||||||
"missingStatesWarning": {
|
"missingStatesWarning": {
|
||||||
"title": "Falten exemples d'estat",
|
"title": "Falten exemples d'estat",
|
||||||
"description": "Es recomana seleccionar exemples per a tots els estats per obtenir els millors resultats. Podeu continuar sense seleccionar tots els estats, però el model no serà entrenat fins que tots els estats tinguin imatges. Després de continuar, utilitzeu la vista Classificacions recents per classificar imatges per als estats que falten, i després entrenar el model."
|
"description": "No heu seleccionat exemples per a tots els estats. El model no serà entrenat fins que tots els estats tinguin imatges. Després de continuar, utilitzeu la vista Classificacions recents per classificar imatges per als estats que falten, i després entrenar el model."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -172,7 +172,7 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"inputPlaceHolder": "Introduïu un nom…",
|
"inputPlaceHolder": "Introduïu un nom…",
|
||||||
"title": "Nom",
|
"title": "Nom",
|
||||||
"tips": "El nom ha de tenir almenys 2 caràcters, ha de tenir almenys una lletra, i no ha de ser el nom d'una càmera o una altra zona en aquesta càmera."
|
"tips": "El nom ha de tenir almenys 2 caràcters, ha de tenir almenys una lletra, i no ha de ser el nom d'una càmera o una altra zona."
|
||||||
},
|
},
|
||||||
"label": "Zones",
|
"label": "Zones",
|
||||||
"desc": {
|
"desc": {
|
||||||
|
|||||||
@ -25,10 +25,7 @@
|
|||||||
"deletedModel_one": "Úspešne odstranený {{count}} model",
|
"deletedModel_one": "Úspešne odstranený {{count}} model",
|
||||||
"deletedModel_few": "Úspešne odstranené {{count}} modely",
|
"deletedModel_few": "Úspešne odstranené {{count}} modely",
|
||||||
"deletedModel_other": "Úspěšne ostranených {{count}} modelov",
|
"deletedModel_other": "Úspěšne ostranených {{count}} modelov",
|
||||||
"deletedCategory": "Zmazať triedu",
|
"deletedCategory": "Zmazať triedu"
|
||||||
"categorizedImage": "Obrázek úspěšně klasifikován",
|
|
||||||
"trainedModel": "Úspěšně vytrénovaný model.",
|
|
||||||
"trainingModel": "Trénování modelu bylo úspěšně zahájeno."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,8 +41,6 @@
|
|||||||
"zoomOut": "Oddálit",
|
"zoomOut": "Oddálit",
|
||||||
"detail": {
|
"detail": {
|
||||||
"label": "Detail",
|
"label": "Detail",
|
||||||
"noDataFound": "Žádná detailní data k prohlédnutí",
|
"noDataFound": "Žádná detailní data k prohlédnutí"
|
||||||
"aria": "Přepnout detailní zobrazení",
|
|
||||||
"trackedObject_other": "{{count}} objektů"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
"desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje."
|
"desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje."
|
||||||
},
|
},
|
||||||
"train": {
|
"train": {
|
||||||
"title": "Nedávná rozpoznání",
|
"title": "Trénovat",
|
||||||
"empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje",
|
"empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje",
|
||||||
"aria": "Vybrat trénink"
|
"aria": "Vybrat trénink"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -307,9 +307,7 @@
|
|||||||
"frigateplus": "Frigate+",
|
"frigateplus": "Frigate+",
|
||||||
"enrichments": "Obohacení",
|
"enrichments": "Obohacení",
|
||||||
"triggers": "Spouštěče",
|
"triggers": "Spouštěče",
|
||||||
"cameraManagement": "Správa",
|
"cameraManagement": "Správa"
|
||||||
"cameraReview": "Kontrola",
|
|
||||||
"roles": "Role"
|
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"unsavedChanges": {
|
"unsavedChanges": {
|
||||||
|
|||||||
@ -177,7 +177,7 @@
|
|||||||
"generateSuccess": "Successfully generated sample images",
|
"generateSuccess": "Successfully generated sample images",
|
||||||
"missingStatesWarning": {
|
"missingStatesWarning": {
|
||||||
"title": "Missing State Examples",
|
"title": "Missing State Examples",
|
||||||
"description": "It's recommended to select examples for all states for best results. You can continue without selecting all states, but the model will not be trained until all states have images. After continuing, use the Recent Classifications view to classify images for the missing states, then train the model."
|
"description": "You haven't selected examples for all states. The model will not be trained until all states have images. After continuing, use the Recent Classifications view to classify images for the missing states, then train the model."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -500,7 +500,7 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"title": "Name",
|
"title": "Name",
|
||||||
"inputPlaceHolder": "Enter a name…",
|
"inputPlaceHolder": "Enter a name…",
|
||||||
"tips": "Name must be at least 2 characters, must have at least one letter, and must not be the name of a camera or another zone on this camera."
|
"tips": "Name must be at least 2 characters, must have at least one letter, and must not be the name of a camera or another zone."
|
||||||
},
|
},
|
||||||
"inertia": {
|
"inertia": {
|
||||||
"title": "Inertia",
|
"title": "Inertia",
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -212,10 +212,6 @@
|
|||||||
},
|
},
|
||||||
"hideObjectDetails": {
|
"hideObjectDetails": {
|
||||||
"label": "Ascunde traseul obiectului"
|
"label": "Ascunde traseul obiectului"
|
||||||
},
|
|
||||||
"downloadCleanSnapshot": {
|
|
||||||
"label": "Descarcă un snapshot curat",
|
|
||||||
"aria": "Descarcă snapshot curat"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
|||||||
@ -121,8 +121,7 @@
|
|||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
"twoWayTalk": "Çift Yönlü Ses",
|
"twoWayTalk": "Çift Yönlü Ses",
|
||||||
"close": "Kapat",
|
"close": "Kapat",
|
||||||
"delete": "Sil",
|
"delete": "Sil"
|
||||||
"continue": "Devam Et"
|
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"systemLogs": "Sistem günlükleri",
|
"systemLogs": "Sistem günlükleri",
|
||||||
@ -223,8 +222,7 @@
|
|||||||
"help": "Yardım",
|
"help": "Yardım",
|
||||||
"faceLibrary": "Yüz Veritabanı",
|
"faceLibrary": "Yüz Veritabanı",
|
||||||
"systemMetrics": "Sistem metrikleri",
|
"systemMetrics": "Sistem metrikleri",
|
||||||
"uiPlayground": "UI Deneme Alanı",
|
"uiPlayground": "UI Deneme Alanı"
|
||||||
"classification": "Sınıflandırma"
|
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"back": "Geri",
|
"back": "Geri",
|
||||||
@ -300,8 +298,5 @@
|
|||||||
"field": {
|
"field": {
|
||||||
"optional": "İsteğe bağlı",
|
"optional": "İsteğe bağlı",
|
||||||
"internalID": "Frigate’ın yapılandırma ve veritabanında kullandığı Dahili Kimlik"
|
"internalID": "Frigate’ın yapılandırma ve veritabanında kullandığı Dahili Kimlik"
|
||||||
},
|
|
||||||
"information": {
|
|
||||||
"pixels": "{{area}}px"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,64 +121,7 @@
|
|||||||
"objectLabel": "Nesne Etiketi",
|
"objectLabel": "Nesne Etiketi",
|
||||||
"objectLabelPlaceholder": "Nesne türünü seçin...",
|
"objectLabelPlaceholder": "Nesne türünü seçin...",
|
||||||
"classificationType": "Sınıflandırma Türü",
|
"classificationType": "Sınıflandırma Türü",
|
||||||
"classificationTypeTip": "Sınıflandırma türleri hakkında bilgi edinin",
|
"classificationTypeTip": "Sınıflandırma türleri hakkında bilgi edinin"
|
||||||
"classificationTypeDesc": "Alt etiketleri, nesne etiketine ek metin ekler (örneğin: “Person: UPS”). Öznitelikler (attributes) ise nesne meta verilerinde ayrı olarak saklanan ve aranabilir metadata bilgileridir.",
|
|
||||||
"classificationSubLabel": "Alt Etiket",
|
|
||||||
"classificationAttribute": "Özellik",
|
|
||||||
"classes": "Sınıflar",
|
|
||||||
"states": "Durumlar",
|
|
||||||
"classesTip": "Sınıflar hakkında bilgi edinin",
|
|
||||||
"classesStateDesc": "Kamera alanınızın içinde bulunabileceği farklı durumları tanımlayın. Örneğin: bir garaj kapısı için ‘açık’ ve ‘kapalı’.",
|
|
||||||
"classesObjectDesc": "Algılanan nesneleri sınıflandırmak için farklı kategorileri tanımlayın. Örneğin: Bir kişi sınıflandırması için ‘teslimat_görevlisi’, ‘sakin’, ‘yabancı’.",
|
|
||||||
"classPlaceholder": "Sınıf adını girin...",
|
|
||||||
"errors": {
|
|
||||||
"nameRequired": "Model adı gerekli",
|
|
||||||
"nameLength": "Model adı 64 karakter veya daha az olmalıdır",
|
|
||||||
"nameOnlyNumbers": "Model adı yalnızca rakamlardan oluşamaz",
|
|
||||||
"classRequired": "En az 1 sınıf gereklidir",
|
|
||||||
"classesUnique": "Sınıf adları benzersiz olmalıdır",
|
|
||||||
"stateRequiresTwoClasses": "Durum modelleri en az 2 sınıf gerektirir",
|
|
||||||
"objectLabelRequired": "Lütfen bir nesne etiketi seçin",
|
|
||||||
"objectTypeRequired": "Lütfen bir sınıflandırma türü seçin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"step2": {
|
|
||||||
"description": "İzlenecek alanı her kamera için seçin ve tanımlayın. Model bu alanların durumunu sınıflandıracaktır.",
|
|
||||||
"cameras": "Kameralar",
|
|
||||||
"selectCamera": "Kamera Seç",
|
|
||||||
"noCameras": "Kameraları eklemek için + simgesine tıklayın",
|
|
||||||
"selectCameraPrompt": "Listedeki bir kamerayı seçerek izlenecek alanı tanımlayın"
|
|
||||||
},
|
|
||||||
"step3": {
|
|
||||||
"selectImagesPrompt": "{{className}} etiketli tüm görselleri seç",
|
|
||||||
"selectImagesDescription": "Görselleri seçmek için üzerlerine tıklayın. Bu sınıfla işiniz bittiğinde Devam Et’e tıklayın.",
|
|
||||||
"allImagesRequired_one": "Lütfen tüm görselleri sınıflandırın. {{count}} görsel kaldı.",
|
|
||||||
"allImagesRequired_other": "Lütfen tüm görselleri sınıflandırın. {{count}} görsel kaldı.",
|
|
||||||
"generating": {
|
|
||||||
"title": "Örnek Görseller Oluşturuluyor",
|
|
||||||
"description": "Frigate kayıtlarınızdan temsilî görüntüler çekiliyor. Bu işlem biraz zaman alabilir…"
|
|
||||||
},
|
|
||||||
"training": {
|
|
||||||
"title": "Model Eğitiliyor",
|
|
||||||
"description": "Modeliniz arka planda eğitiliyor. Bu pencereyi kapatabilirsiniz; eğitim tamamlandığında model otomatik olarak çalışmaya başlayacaktır."
|
|
||||||
},
|
|
||||||
"retryGenerate": "Oluşturmayı Yeniden Dene",
|
|
||||||
"noImages": "Örnek görsel oluşturulamadı",
|
|
||||||
"classifying": "Sınıflandırılıyor ve Eğitiliyor...",
|
|
||||||
"trainingStarted": "Eğitim başarıyla başlatıldı",
|
|
||||||
"modelCreated": "Model başarıyla oluşturuldu. Eksik durumlar için görseller eklemek üzere Son Sınıflandırmalar görünümünü kullanın ve ardından modeli eğitin.",
|
|
||||||
"errors": {
|
|
||||||
"noCameras": "Hiç kamera yapılandırılmadı",
|
|
||||||
"noObjectLabel": "Nesne etiketi seçilmedi",
|
|
||||||
"generateFailed": "Örnekler oluşturulamadı: {{error}}",
|
|
||||||
"generationFailed": "Oluşturma başarısız oldu. Lütfen tekrar deneyin.",
|
|
||||||
"classifyFailed": "Görseller sınıflandırılamadı: {{error}}"
|
|
||||||
},
|
|
||||||
"generateSuccess": "Örnek görseller başarıyla oluşturuldu",
|
|
||||||
"missingStatesWarning": {
|
|
||||||
"title": "Eksik Durum Örnekleri",
|
|
||||||
"description": "En iyi sonuçlar için tavsiye edilir: Tüm durumlar (state) için örnekler seçin. Tüm durumlar için örnek seçmeden devam edebilirsiniz, ancak model, tüm durumlara ait görüntüler eklenene kadar eğitilmeyecektir. Devam ettikten sonra, eksik durumlar için görüntüleri sınıflandırmak ve ardından modeli eğitmek için Son Sınıflandırmalar (Recent Classifications) görünümünü kullanın."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,8 +55,5 @@
|
|||||||
"objectTrack": {
|
"objectTrack": {
|
||||||
"trackedPoint": "Takip edilen nokta",
|
"trackedPoint": "Takip edilen nokta",
|
||||||
"clickToSeek": "Bu zamana gitmek için tıklayın"
|
"clickToSeek": "Bu zamana gitmek için tıklayın"
|
||||||
},
|
}
|
||||||
"normalActivity": "Normal",
|
|
||||||
"needsReview": "İnceleme Gerekiyor",
|
|
||||||
"securityConcern": "Güvenlik endişesi"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,20 +197,6 @@
|
|||||||
"audioTranscription": {
|
"audioTranscription": {
|
||||||
"label": "Çözümle",
|
"label": "Çözümle",
|
||||||
"aria": "Ses çözümlemesi iste"
|
"aria": "Ses çözümlemesi iste"
|
||||||
},
|
|
||||||
"downloadCleanSnapshot": {
|
|
||||||
"label": "Temiz anlık görüntüyü indir",
|
|
||||||
"aria": "Temiz anlık görüntüyü indir"
|
|
||||||
},
|
|
||||||
"viewTrackingDetails": {
|
|
||||||
"label": "İzleme ayrıntılarını görüntüle",
|
|
||||||
"aria": "Takip ayrıntılarını göster"
|
|
||||||
},
|
|
||||||
"showObjectDetails": {
|
|
||||||
"label": "Nesne yolunu göster"
|
|
||||||
},
|
|
||||||
"hideObjectDetails": {
|
|
||||||
"label": "Nesne yolunu gizle"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"noTrackedObjects": "Takip Edilen Nesne Bulunamadı",
|
"noTrackedObjects": "Takip Edilen Nesne Bulunamadı",
|
||||||
@ -222,13 +208,11 @@
|
|||||||
"success": "Takip edilen nesne başarıyla silindi."
|
"success": "Takip edilen nesne başarıyla silindi."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltip": "Eşleşme: {{type}} (%{{confidence}})",
|
"tooltip": "Eşleşme: {{type}} (%{{confidence}})"
|
||||||
"previousTrackedObject": "Önceki izlenen nesne",
|
|
||||||
"nextTrackedObject": "Sonraki izlenen nesne"
|
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"confirmDelete": {
|
"confirmDelete": {
|
||||||
"desc": "Bu takip edilen nesneyi silmek anlık görüntüyü, kaydedilmiş gömü verilerini ve ilişkili yaşam döngüsü kayıtlarını siler. Geçmiş görünümündeki bu izlenen nesneye ait kayıtlı video görüntüleri <em>SİLİNMEYECEKTİR.</em> <br /><br />Devam etmek istediğinizden emin misiniz?",
|
"desc": "Bu takip edilen nesneyi silmek nesne fotoğrafını, ilişkili gömüyü ve ilişkili yaşam döngüsü kayıtlarını siler. Video kayıt görüntüleri geçmiş görünümünden <em>SİLİNMEYECEKTİR.</em> <br /><br />Devam etmek istediğinize emin misiniz?",
|
||||||
"title": "Silmeyi onayla"
|
"title": "Silmeyi onayla"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -286,8 +270,5 @@
|
|||||||
"previous": "Önceki slayt",
|
"previous": "Önceki slayt",
|
||||||
"next": "Sonraki slayt"
|
"next": "Sonraki slayt"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"concerns": {
|
|
||||||
"label": "Endişeler"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"selectImage": "Lütfen bir resim dosyası seçin."
|
"selectImage": "Lütfen bir resim dosyası seçin."
|
||||||
},
|
},
|
||||||
"dropInstructions": "Bir görseli buraya sürükleyip bırakın, yapıştırın ya da seçmek için tıklayın"
|
"dropInstructions": "Bir görseli buraya sürükleyip bırakın, yapıştırın ya da seçmek için tıklayın."
|
||||||
},
|
},
|
||||||
"trainFaceAs": "Yüzü şu olarak eğit:",
|
"trainFaceAs": "Yüzü şu olarak eğit:",
|
||||||
"toast": {
|
"toast": {
|
||||||
|
|||||||
@ -176,14 +176,5 @@
|
|||||||
"noVideoSource": "Anlık görüntü için kullanılabilir bir video kaynağı bulunamadı.",
|
"noVideoSource": "Anlık görüntü için kullanılabilir bir video kaynağı bulunamadı.",
|
||||||
"captureFailed": "Anlık görüntü yakalanamadı.",
|
"captureFailed": "Anlık görüntü yakalanamadı.",
|
||||||
"downloadStarted": "Anlık görüntü indirme işlemi başlatıldı."
|
"downloadStarted": "Anlık görüntü indirme işlemi başlatıldı."
|
||||||
},
|
|
||||||
"noCameras": {
|
|
||||||
"title": "Hiç Kamera Yapılandırılmamış",
|
|
||||||
"description": "Frigate’e bir kamera bağlayarak başlayın.",
|
|
||||||
"buttonText": "Kamera Ekle",
|
|
||||||
"restricted": {
|
|
||||||
"title": "Kullanılabilir Kamera Yok",
|
|
||||||
"description": "Bu gruptaki kameraları görüntüleme izniniz yok."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -839,53 +839,5 @@
|
|||||||
"deleteTriggerFailed": "Tetik silinemedi: {{errorMessage}}"
|
"deleteTriggerFailed": "Tetik silinemedi: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"cameraWizard": {
|
|
||||||
"title": "Kamera Ekle",
|
|
||||||
"description": "Aşağıdaki adımları izleyerek Frigate kurulumunuza yeni bir kamera ekleyin.",
|
|
||||||
"steps": {
|
|
||||||
"nameAndConnection": "Ad & Bağlantı",
|
|
||||||
"probeOrSnapshot": "Probe veya Anlık Görüntü",
|
|
||||||
"streamConfiguration": "Akış Yapılandırması",
|
|
||||||
"validationAndTesting": "Doğrulama ve Test"
|
|
||||||
},
|
|
||||||
"save": {
|
|
||||||
"success": "Yeni kamera {{cameraName}} başarıyla kaydedildi.",
|
|
||||||
"failure": "{{cameraName}} kaydedilirken hata oluştu."
|
|
||||||
},
|
|
||||||
"testResultLabels": {
|
|
||||||
"resolution": "Çözünürlük",
|
|
||||||
"video": "Video",
|
|
||||||
"audio": "Ses",
|
|
||||||
"fps": "FPS"
|
|
||||||
},
|
|
||||||
"commonErrors": {
|
|
||||||
"noUrl": "Lütfen geçerli bir akış URL'si sağlayın",
|
|
||||||
"testFailed": "Akış testi başarısız oldu: {{error}}"
|
|
||||||
},
|
|
||||||
"step1": {
|
|
||||||
"description": "Kamera bilgilerinizi girin ve kamerayı taramayı (probe) ya da markayı manuel olarak seçmeyi tercih edin.",
|
|
||||||
"cameraName": "Kamera Adı",
|
|
||||||
"cameraNamePlaceholder": "ör. front_door veya Arka Bahçe Genel Görünümü",
|
|
||||||
"host": "Ana Makine / IP Adresi",
|
|
||||||
"port": "Port",
|
|
||||||
"username": "Kullanıcı adı",
|
|
||||||
"usernamePlaceholder": "İsteğe bağlı",
|
|
||||||
"password": "Şifre",
|
|
||||||
"passwordPlaceholder": "İsteğe bağlı",
|
|
||||||
"selectTransport": "İletişim protokolünü seçin",
|
|
||||||
"cameraBrand": "Kamera Markası",
|
|
||||||
"selectBrand": "URL şablonu için kamera markasını seçin",
|
|
||||||
"customUrl": "Özel Akış URL’si",
|
|
||||||
"brandInformation": "Marka Bilgileri",
|
|
||||||
"brandUrlFormat": "RTSP URL formatı şu şekilde olan kameralar için: {{exampleUrl}}",
|
|
||||||
"customUrlPlaceholder": "rtsp://kullanıcıadı:şifre@host:port/path",
|
|
||||||
"connectionSettings": "Bağlantı Ayarları",
|
|
||||||
"detectionMethod": "Akış Algılama Yöntemi",
|
|
||||||
"onvifPort": "ONVIF Portu",
|
|
||||||
"probeMode": "Kamerayı tara",
|
|
||||||
"manualMode": "Manuel seçim",
|
|
||||||
"detectionMethodDescription": "Kamera akış URL’lerini bulmak için kamerayı ONVIF ile tarayın (destekleniyorsa) veya ön tanımlı URL’leri kullanmak için kamera markasını manuel olarak seçin. Özel bir RTSP URL’si girmek için manuel yöntemi seçin ve “Diğer”i işaretleyin."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,17 +159,10 @@
|
|||||||
"plate_recognition": "Plaka Tanıma",
|
"plate_recognition": "Plaka Tanıma",
|
||||||
"face_recognition_speed": "Yüz Tanıma Hızı",
|
"face_recognition_speed": "Yüz Tanıma Hızı",
|
||||||
"yolov9_plate_detection_speed": "YOLOv9 Plaka Tanıma Hızı",
|
"yolov9_plate_detection_speed": "YOLOv9 Plaka Tanıma Hızı",
|
||||||
"yolov9_plate_detection": "YOLOv9 Plaka Tanıma",
|
"yolov9_plate_detection": "YOLOv9 Plaka Tanıma"
|
||||||
"review_description": "İnceleme Açıklaması",
|
|
||||||
"review_description_speed": "İnceleme Açıklama Hızı",
|
|
||||||
"review_description_events_per_second": "İnceleme Açıklaması",
|
|
||||||
"object_description": "Nesne Açıklaması",
|
|
||||||
"object_description_speed": "Nesne Açıklama Hızı",
|
|
||||||
"object_description_events_per_second": "Nesne Açıklaması"
|
|
||||||
},
|
},
|
||||||
"infPerSecond": "Saniye Başına Çıkarım",
|
"infPerSecond": "Saniye Başına Çıkarım",
|
||||||
"title": "Zenginleştirmeler",
|
"title": "Zenginleştirmeler"
|
||||||
"averageInf": "Ortalama Çıkarım Süresi"
|
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"download": {
|
"download": {
|
||||||
|
|||||||
@ -156,7 +156,7 @@
|
|||||||
"modelCreated": "Модель успішно створено. Використовуйте режим перегляду «Нещодавні класифікації», щоб додати зображення для відсутніх станів, а потім навчіть модель.",
|
"modelCreated": "Модель успішно створено. Використовуйте режим перегляду «Нещодавні класифікації», щоб додати зображення для відсутніх станів, а потім навчіть модель.",
|
||||||
"missingStatesWarning": {
|
"missingStatesWarning": {
|
||||||
"title": "Приклади відсутніх станів",
|
"title": "Приклади відсутніх станів",
|
||||||
"description": "Для найкращих результатів рекомендується вибрати приклади для всіх станів. Ви можете продовжити, не вибираючи всі стани, але модель не буде навчена, доки всі стани не матимуть зображень. Після продовження скористайтеся поданням «Нещодавні класифікації», щоб класифікувати зображення для відсутніх станів, а потім навчіть модель."
|
"description": "Ви не вибрали приклади для всіх станів. Модель не буде навчена, доки всі стани не матимуть зображень. Після продовження скористайтеся поданням «Нещодавні класифікації», щоб класифікувати зображення для відсутніх станів, а потім навчіть модель."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -161,7 +161,7 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"inputPlaceHolder": "Введіть назву…",
|
"inputPlaceHolder": "Введіть назву…",
|
||||||
"title": "Ім'я",
|
"title": "Ім'я",
|
||||||
"tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони на цій камері."
|
"tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони."
|
||||||
},
|
},
|
||||||
"desc": {
|
"desc": {
|
||||||
"title": "Зони дозволяють визначити певну область кадру, щоб ви могли визначити, чи знаходиться об'єкт у певній області.",
|
"title": "Зони дозволяють визначити певну область кадру, щоб ви могли визначити, чи знаходиться об'єкт у певній області.",
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
// Module-level flag to prevent multiple simultaneous redirects
|
|
||||||
// (eg, when multiple SWR queries fail with 401 at once, or when
|
|
||||||
// both ApiProvider and ProtectedRoute try to redirect)
|
|
||||||
let _isRedirectingToLogin = false;
|
|
||||||
|
|
||||||
export function isRedirectingToLogin(): boolean {
|
|
||||||
return _isRedirectingToLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRedirectingToLogin(value: boolean): void {
|
|
||||||
_isRedirectingToLogin = value;
|
|
||||||
}
|
|
||||||
@ -3,7 +3,6 @@ import { SWRConfig } from "swr";
|
|||||||
import { WsProvider } from "./ws";
|
import { WsProvider } from "./ws";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect";
|
|
||||||
|
|
||||||
axios.defaults.baseURL = `${baseUrl}api/`;
|
axios.defaults.baseURL = `${baseUrl}api/`;
|
||||||
|
|
||||||
@ -32,8 +31,7 @@ export function ApiProvider({ children, options }: ApiProviderType) {
|
|||||||
) {
|
) {
|
||||||
// redirect to the login page if not already there
|
// redirect to the login page if not already there
|
||||||
const loginPage = error.response.headers.get("location") ?? "login";
|
const loginPage = error.response.headers.get("location") ?? "login";
|
||||||
if (window.location.href !== loginPage && !isRedirectingToLogin()) {
|
if (window.location.href !== loginPage) {
|
||||||
setRedirectingToLogin(true);
|
|
||||||
window.location.href = loginPage;
|
window.location.href = loginPage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
import { useContext, useEffect } from "react";
|
import { useContext } from "react";
|
||||||
import { Navigate, Outlet } from "react-router-dom";
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
import { AuthContext } from "@/context/auth-context";
|
import { AuthContext } from "@/context/auth-context";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
import ActivityIndicator from "../indicators/activity-indicator";
|
||||||
import {
|
|
||||||
isRedirectingToLogin,
|
|
||||||
setRedirectingToLogin,
|
|
||||||
} from "@/api/auth-redirect";
|
|
||||||
|
|
||||||
export default function ProtectedRoute({
|
export default function ProtectedRoute({
|
||||||
requiredRoles,
|
requiredRoles,
|
||||||
@ -14,20 +10,6 @@ export default function ProtectedRoute({
|
|||||||
}) {
|
}) {
|
||||||
const { auth } = useContext(AuthContext);
|
const { auth } = useContext(AuthContext);
|
||||||
|
|
||||||
// Redirect to login page when not authenticated
|
|
||||||
// don't use <Navigate> because we need a full page load to reset state
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!auth.isLoading &&
|
|
||||||
auth.isAuthenticated &&
|
|
||||||
!auth.user &&
|
|
||||||
!isRedirectingToLogin()
|
|
||||||
) {
|
|
||||||
setRedirectingToLogin(true);
|
|
||||||
window.location.href = "/login";
|
|
||||||
}
|
|
||||||
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
|
|
||||||
|
|
||||||
if (auth.isLoading) {
|
if (auth.isLoading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
@ -41,9 +23,7 @@ export default function ProtectedRoute({
|
|||||||
|
|
||||||
// Authenticated mode (8971): require login
|
// Authenticated mode (8971): require login
|
||||||
if (!auth.user) {
|
if (!auth.user) {
|
||||||
return (
|
return <Navigate to="/login" replace />;
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback
|
// If role is null (shouldn’t happen if isAuthenticated, but type safety), fallback
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay";
|
import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay";
|
||||||
import BlurredIconButton from "../button/BlurredIconButton";
|
import BlurredIconButton from "../button/BlurredIconButton";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
|
||||||
|
|
||||||
type ExportProps = {
|
type ExportProps = {
|
||||||
className: string;
|
className: string;
|
||||||
@ -41,7 +40,6 @@ export default function ExportCard({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}: ExportProps) {
|
}: ExportProps) {
|
||||||
const { t } = useTranslation(["views/exports"]);
|
const { t } = useTranslation(["views/exports"]);
|
||||||
const isAdmin = useIsAdmin();
|
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [loading, setLoading] = useState(
|
const [loading, setLoading] = useState(
|
||||||
exportedRecording.thumb_path.length > 0,
|
exportedRecording.thumb_path.length > 0,
|
||||||
@ -197,7 +195,7 @@ export default function ExportCard({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{isAdmin && !exportedRecording.in_progress && (
|
{!exportedRecording.in_progress && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<BlurredIconButton
|
<BlurredIconButton
|
||||||
@ -214,7 +212,6 @@ export default function ExportCard({
|
|||||||
<TooltipContent>{t("tooltip.editName")}</TooltipContent>
|
<TooltipContent>{t("tooltip.editName")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<BlurredIconButton
|
<BlurredIconButton
|
||||||
@ -230,7 +227,6 @@ export default function ExportCard({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t("tooltip.deleteExport")}</TooltipContent>
|
<TooltipContent>{t("tooltip.deleteExport")}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -407,6 +407,30 @@ export default function Step3ChooseExamples({
|
|||||||
return allClasses.every((className) => statesWithExamples.has(className));
|
return allClasses.every((className) => statesWithExamples.has(className));
|
||||||
}, [step1Data.modelType, allClasses, statesWithExamples]);
|
}, [step1Data.modelType, allClasses, statesWithExamples]);
|
||||||
|
|
||||||
|
// For state models on the last class, require all images to be classified
|
||||||
|
// But allow proceeding even if not all states have examples (with warning)
|
||||||
|
const canProceed = useMemo(() => {
|
||||||
|
if (step1Data.modelType === "state" && isLastClass) {
|
||||||
|
// Check if all 24 images will be classified after current selections are applied
|
||||||
|
const totalImages = unknownImages.slice(0, 24).length;
|
||||||
|
|
||||||
|
// Count images that will be classified (either already classified or currently selected)
|
||||||
|
const allImages = unknownImages.slice(0, 24);
|
||||||
|
const willBeClassified = allImages.filter((img) => {
|
||||||
|
return imageClassifications[img] || selectedImages.has(img);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
return willBeClassified >= totalImages;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, [
|
||||||
|
step1Data.modelType,
|
||||||
|
isLastClass,
|
||||||
|
unknownImages,
|
||||||
|
imageClassifications,
|
||||||
|
selectedImages,
|
||||||
|
]);
|
||||||
|
|
||||||
const hasUnclassifiedImages = useMemo(() => {
|
const hasUnclassifiedImages = useMemo(() => {
|
||||||
if (!unknownImages) return false;
|
if (!unknownImages) return false;
|
||||||
const allImages = unknownImages.slice(0, 24);
|
const allImages = unknownImages.slice(0, 24);
|
||||||
@ -570,7 +594,9 @@ export default function Step3ChooseExamples({
|
|||||||
}
|
}
|
||||||
variant="select"
|
variant="select"
|
||||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||||
disabled={!hasGenerated || isGenerating || isProcessing}
|
disabled={
|
||||||
|
!hasGenerated || isGenerating || isProcessing || !canProceed
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isProcessing && <ActivityIndicator className="size-4" />}
|
{isProcessing && <ActivityIndicator className="size-4" />}
|
||||||
{t("button.continue", { ns: "common" })}
|
{t("button.continue", { ns: "common" })}
|
||||||
|
|||||||
@ -559,7 +559,6 @@ export function TrackingDetails({
|
|||||||
isDetailMode={true}
|
isDetailMode={true}
|
||||||
camera={event.camera}
|
camera={event.camera}
|
||||||
currentTimeOverride={currentTime}
|
currentTimeOverride={currentTime}
|
||||||
enableGapControllerRecovery={true}
|
|
||||||
/>
|
/>
|
||||||
{isVideoLoading && (
|
{isVideoLoading && (
|
||||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import Hls, { HlsConfig } from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
import VideoControls from "./VideoControls";
|
import VideoControls from "./VideoControls";
|
||||||
@ -57,7 +57,6 @@ type HlsVideoPlayerProps = {
|
|||||||
isDetailMode?: boolean;
|
isDetailMode?: boolean;
|
||||||
camera?: string;
|
camera?: string;
|
||||||
currentTimeOverride?: number;
|
currentTimeOverride?: number;
|
||||||
enableGapControllerRecovery?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HlsVideoPlayer({
|
export default function HlsVideoPlayer({
|
||||||
@ -82,7 +81,6 @@ export default function HlsVideoPlayer({
|
|||||||
isDetailMode = false,
|
isDetailMode = false,
|
||||||
camera,
|
camera,
|
||||||
currentTimeOverride,
|
currentTimeOverride,
|
||||||
enableGapControllerRecovery = false,
|
|
||||||
}: HlsVideoPlayerProps) {
|
}: HlsVideoPlayerProps) {
|
||||||
const { t } = useTranslation("components/player");
|
const { t } = useTranslation("components/player");
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
@ -172,23 +170,11 @@ export default function HlsVideoPlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base HLS configuration
|
hlsRef.current = new Hls({
|
||||||
const baseConfig: Partial<HlsConfig> = {
|
|
||||||
maxBufferLength: 10,
|
maxBufferLength: 10,
|
||||||
maxBufferSize: 20 * 1000 * 1000,
|
maxBufferSize: 20 * 1000 * 1000,
|
||||||
startPosition: currentSource.startPosition,
|
startPosition: currentSource.startPosition,
|
||||||
};
|
});
|
||||||
|
|
||||||
const hlsConfig = { ...baseConfig };
|
|
||||||
|
|
||||||
if (enableGapControllerRecovery) {
|
|
||||||
hlsConfig.highBufferWatchdogPeriod = 1; // Check for stalls every 1 second (default: 3)
|
|
||||||
hlsConfig.nudgeOffset = 0.2; // Nudge playhead forward 0.2s when stalled (default: 0.1)
|
|
||||||
hlsConfig.nudgeMaxRetry = 5; // Try up to 5 nudges before giving up (default: 3)
|
|
||||||
hlsConfig.maxBufferHole = 0.5; // Tolerate up to 0.5s gaps between fragments (default: 0.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
hlsRef.current = new Hls(hlsConfig);
|
|
||||||
hlsRef.current.attachMedia(videoRef.current);
|
hlsRef.current.attachMedia(videoRef.current);
|
||||||
hlsRef.current.loadSource(currentSource.playlist);
|
hlsRef.current.loadSource(currentSource.playlist);
|
||||||
videoRef.current.playbackRate = currentPlaybackRate;
|
videoRef.current.playbackRate = currentPlaybackRate;
|
||||||
@ -201,13 +187,7 @@ export default function HlsVideoPlayer({
|
|||||||
hlsRef.current.destroy();
|
hlsRef.current.destroy();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [videoRef, hlsRef, useHlsCompat, currentSource]);
|
||||||
videoRef,
|
|
||||||
hlsRef,
|
|
||||||
useHlsCompat,
|
|
||||||
currentSource,
|
|
||||||
enableGapControllerRecovery,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// state handling
|
// state handling
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user