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
|
||||
|
||||
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
|
||||
|
||||
- **`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.
|
||||
|
||||
|
||||
@ -164,6 +164,12 @@ A Tensorflow Lite is provided in the container at `/openvino-model/ssdlite_mobil
|
||||
<details>
|
||||
<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:
|
||||
|
||||
```yaml
|
||||
@ -402,7 +408,7 @@ The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv
|
||||
|
||||
:::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
|
||||
|
||||
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):
|
||||
source_type: Optional[str] = "api"
|
||||
sub_label: Optional[str] = None
|
||||
score: Optional[float] = 0
|
||||
duration: Optional[int] = 30
|
||||
|
||||
@ -346,7 +346,7 @@ def events(
|
||||
"/events/explore",
|
||||
response_model=list[EventResponse],
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Get summary of objects",
|
||||
summary="Get summary of objects.",
|
||||
description="""Gets a summary of objects from the database.
|
||||
Returns a list of objects with a max of `limit` objects for each label.
|
||||
""",
|
||||
@ -439,7 +439,7 @@ def events_explore(
|
||||
"/event_ids",
|
||||
response_model=list[EventResponse],
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Get events by ids",
|
||||
summary="Get events by ids.",
|
||||
description="""Gets events by a list of ids.
|
||||
Returns a list of events.
|
||||
""",
|
||||
@ -473,7 +473,7 @@ async def event_ids(ids: str, request: Request):
|
||||
@router.get(
|
||||
"/events/search",
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Search events",
|
||||
summary="Search events.",
|
||||
description="""Searches for events in the database.
|
||||
Returns a list of events.
|
||||
""",
|
||||
@ -924,7 +924,7 @@ def events_summary(
|
||||
"/events/{event_id}",
|
||||
response_model=EventResponse,
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Get event by id",
|
||||
summary="Get event by id.",
|
||||
description="Gets an event by its id.",
|
||||
)
|
||||
async def event(event_id: str, request: Request):
|
||||
@ -968,7 +968,7 @@ def set_retain(event_id: str):
|
||||
"/events/{event_id}/plus",
|
||||
response_model=EventUploadPlusResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Send event to Frigate+",
|
||||
summary="Send event to Frigate+.",
|
||||
description="""Sends an event to Frigate+.
|
||||
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",
|
||||
response_model=GenericResponse,
|
||||
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.
|
||||
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.
|
||||
@ -1236,7 +1236,7 @@ async def delete_retain(event_id: str, request: Request):
|
||||
"/events/{event_id}/sub_label",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Set event sub label",
|
||||
summary="Set event sub label.",
|
||||
description="""Sets an event's sub label.
|
||||
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",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Set event license plate",
|
||||
summary="Set event license plate.",
|
||||
description="""Sets an event's license plate.
|
||||
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",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Set event description",
|
||||
summary="Set event description.",
|
||||
description="""Sets an event's description.
|
||||
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",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Regenerate event description",
|
||||
summary="Regenerate event description.",
|
||||
description="""Regenerates an event's description.
|
||||
Returns a success message or an error if the event is not found.
|
||||
""",
|
||||
@ -1463,8 +1463,8 @@ async def regenerate_description(
|
||||
@router.post(
|
||||
"/description/generate",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Generate description embedding",
|
||||
# dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Generate description embedding.",
|
||||
description="""Generates an embedding for an event's description.
|
||||
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}",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Delete event",
|
||||
summary="Delete event.",
|
||||
description="""Deletes an event from the database.
|
||||
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/",
|
||||
response_model=EventMultiDeleteResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Delete events",
|
||||
summary="Delete events.",
|
||||
description="""Deletes a list of events from the database.
|
||||
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",
|
||||
response_model=EventCreateResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Create manual event",
|
||||
summary="Create manual event.",
|
||||
description="""Creates a manual event in the database.
|
||||
Returns a success message or an error if the event is not found.
|
||||
NOTES:
|
||||
@ -1620,7 +1620,7 @@ def create_event(
|
||||
body.score,
|
||||
body.sub_label,
|
||||
body.duration,
|
||||
"api",
|
||||
body.source_type,
|
||||
body.draw,
|
||||
),
|
||||
EventMetadataTypeEnum.manual_event_create.value,
|
||||
@ -1642,7 +1642,7 @@ def create_event(
|
||||
"/events/{event_id}/end",
|
||||
response_model=GenericResponse,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="End manual event",
|
||||
summary="End manual event.",
|
||||
description="""Ends a manual event.
|
||||
Returns a success message or an error if the event is not found.
|
||||
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:
|
||||
event: Event = Event.get(Event.id == event_id)
|
||||
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()
|
||||
request.app.event_metadata_updater.publish(
|
||||
(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:
|
||||
return JSONResponse(
|
||||
content=(
|
||||
@ -1691,7 +1674,7 @@ async def end_event(request: Request, event_id: str, body: EventsEndBody):
|
||||
"/trigger/embedding",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Create trigger embedding",
|
||||
summary="Create trigger embedding.",
|
||||
description="""Creates a trigger embedding for a specific trigger.
|
||||
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}",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Update trigger embedding",
|
||||
summary="Update trigger embedding.",
|
||||
description="""Updates a trigger embedding for a specific trigger.
|
||||
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}",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Delete trigger embedding",
|
||||
summary="Delete trigger embedding.",
|
||||
description="""Deletes a trigger embedding for a specific trigger.
|
||||
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}",
|
||||
response_model=dict,
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
summary="Get triggers status",
|
||||
summary="Get triggers status.",
|
||||
description="""Gets the status of all triggers for a specific camera.
|
||||
Returns a success message or an error if the camera is not found.
|
||||
""",
|
||||
|
||||
@ -209,22 +209,10 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
||||
logger.debug(
|
||||
f"Found GenAI Review Summary request for {start_ts} to {end_ts}"
|
||||
)
|
||||
|
||||
# Query all review segments with camera and time information
|
||||
segments: list[dict[str, Any]] = [
|
||||
{
|
||||
"camera": r["camera"].replace("_", " ").title(),
|
||||
"start_time": r["start_time"],
|
||||
"end_time": r["end_time"],
|
||||
"metadata": r["data"]["metadata"],
|
||||
}
|
||||
items: list[dict[str, Any]] = [
|
||||
r["data"]["metadata"]
|
||||
for r in (
|
||||
ReviewSegment.select(
|
||||
ReviewSegment.camera,
|
||||
ReviewSegment.start_time,
|
||||
ReviewSegment.end_time,
|
||||
ReviewSegment.data,
|
||||
)
|
||||
ReviewSegment.select(ReviewSegment.data)
|
||||
.where(
|
||||
(ReviewSegment.data["metadata"].is_null(False))
|
||||
& (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")
|
||||
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)
|
||||
primary_segments = [
|
||||
seg
|
||||
for seg in segments
|
||||
if seg["metadata"].get("potential_threat_level", 0) > 0
|
||||
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"
|
||||
important_items = list(
|
||||
filter(
|
||||
lambda item: item.get("potential_threat_level", 0) > 0
|
||||
or item.get("other_concerns"),
|
||||
items,
|
||||
)
|
||||
)
|
||||
|
||||
if not important_items:
|
||||
return "No concerns were found during this time period."
|
||||
|
||||
if self.config.review.genai.debug_save_thumbnails:
|
||||
Path(
|
||||
@ -304,7 +247,7 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
||||
return self.genai_client.generate_review_summary(
|
||||
start_ts,
|
||||
end_ts,
|
||||
all_items_for_summary,
|
||||
important_items,
|
||||
self.config.review.genai.debug_save_thumbnails,
|
||||
)
|
||||
else:
|
||||
|
||||
@ -185,66 +185,44 @@ Each line represents a detection state, not necessarily unique individuals. Pare
|
||||
timeline_summary_prompt = f"""
|
||||
You are a security officer.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- Title & overview
|
||||
- Start with:
|
||||
# Security Summary - {time_range}
|
||||
- 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
|
||||
- **ONLY create bullet points for PRIMARY items (_is_primary: true)**
|
||||
- **Do NOT create sections or bullets for events that don't exist**
|
||||
- Do NOT create separate bullets for contextual items
|
||||
- Present primary events in chronological order as a bullet list.
|
||||
- **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.
|
||||
- Present events in chronological order as a bullet list.
|
||||
- **If multiple events occur within the same minute or overlapping time range, COMBINE them into a single bullet.**
|
||||
- Summarize the distinct activities as sub-points under the shared timestamp.
|
||||
- If no timestamp is given, preserve order but label as “Time not specified.”
|
||||
- Use bold timestamps for clarity.
|
||||
- Camera format: "Camera: [camera name]" or mention contextual cameras inline when relevant
|
||||
- Group bullets under subheadings ONLY when you have actual PRIMARY events to list (e.g., Porch Activity, Unusual Behavior).
|
||||
- Group bullets under subheadings when multiple events fall into the same category (e.g., Vehicle Activity, Porch Activity, Unusual Behavior).
|
||||
|
||||
- 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 1: "Needs review"
|
||||
- Threat level 2: "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**
|
||||
- **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.
|
||||
- If multiple events at the same time share the same threat level, only state it once.
|
||||
|
||||
- Final assessment
|
||||
- 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.
|
||||
- If threat level 1 events are present:
|
||||
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.
|
||||
- Keep this section brief - do not repeat details from the event descriptions above.
|
||||
|
||||
- Conciseness
|
||||
- Do not repeat benign clothing/appearance details unless they distinguish individuals.
|
||||
- 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:
|
||||
|
||||
@ -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.",
|
||||
"missingStatesWarning": {
|
||||
"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": {
|
||||
"inputPlaceHolder": "Introduïu un 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",
|
||||
"desc": {
|
||||
|
||||
@ -25,10 +25,7 @@
|
||||
"deletedModel_one": "Úspešne odstranený {{count}} model",
|
||||
"deletedModel_few": "Úspešne odstranené {{count}} modely",
|
||||
"deletedModel_other": "Úspěšne ostranených {{count}} modelov",
|
||||
"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."
|
||||
"deletedCategory": "Zmazať triedu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,8 +41,6 @@
|
||||
"zoomOut": "Oddálit",
|
||||
"detail": {
|
||||
"label": "Detail",
|
||||
"noDataFound": "Žádná detailní data k prohlédnutí",
|
||||
"aria": "Přepnout detailní zobrazení",
|
||||
"trackedObject_other": "{{count}} objektů"
|
||||
"noDataFound": "Žádná detailní data k prohlédnutí"
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
"desc": "Skutečně chcete vymazat kolekci {{name}}? Toto trvale vymaže všechny přiřazené obličeje."
|
||||
},
|
||||
"train": {
|
||||
"title": "Nedávná rozpoznání",
|
||||
"title": "Trénovat",
|
||||
"empty": "Nejsou zde žádné předchozí pokusy o rozpoznání obličeje",
|
||||
"aria": "Vybrat trénink"
|
||||
},
|
||||
|
||||
@ -307,9 +307,7 @@
|
||||
"frigateplus": "Frigate+",
|
||||
"enrichments": "Obohacení",
|
||||
"triggers": "Spouštěče",
|
||||
"cameraManagement": "Správa",
|
||||
"cameraReview": "Kontrola",
|
||||
"roles": "Role"
|
||||
"cameraManagement": "Správa"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
|
||||
@ -177,7 +177,7 @@
|
||||
"generateSuccess": "Successfully generated sample images",
|
||||
"missingStatesWarning": {
|
||||
"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": {
|
||||
"title": "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": {
|
||||
"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": {
|
||||
"label": "Ascunde traseul obiectului"
|
||||
},
|
||||
"downloadCleanSnapshot": {
|
||||
"label": "Descarcă un snapshot curat",
|
||||
"aria": "Descarcă snapshot curat"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
|
||||
@ -121,8 +121,7 @@
|
||||
"cancel": "İptal",
|
||||
"twoWayTalk": "Çift Yönlü Ses",
|
||||
"close": "Kapat",
|
||||
"delete": "Sil",
|
||||
"continue": "Devam Et"
|
||||
"delete": "Sil"
|
||||
},
|
||||
"menu": {
|
||||
"systemLogs": "Sistem günlükleri",
|
||||
@ -223,8 +222,7 @@
|
||||
"help": "Yardım",
|
||||
"faceLibrary": "Yüz Veritabanı",
|
||||
"systemMetrics": "Sistem metrikleri",
|
||||
"uiPlayground": "UI Deneme Alanı",
|
||||
"classification": "Sınıflandırma"
|
||||
"uiPlayground": "UI Deneme Alanı"
|
||||
},
|
||||
"label": {
|
||||
"back": "Geri",
|
||||
@ -300,8 +298,5 @@
|
||||
"field": {
|
||||
"optional": "İsteğe bağlı",
|
||||
"internalID": "Frigate’ın yapılandırma ve veritabanında kullandığı Dahili Kimlik"
|
||||
},
|
||||
"information": {
|
||||
"pixels": "{{area}}px"
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,64 +121,7 @@
|
||||
"objectLabel": "Nesne Etiketi",
|
||||
"objectLabelPlaceholder": "Nesne türünü seçin...",
|
||||
"classificationType": "Sınıflandırma Türü",
|
||||
"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."
|
||||
}
|
||||
"classificationTypeTip": "Sınıflandırma türleri hakkında bilgi edinin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,8 +55,5 @@
|
||||
"objectTrack": {
|
||||
"trackedPoint": "Takip edilen nokta",
|
||||
"clickToSeek": "Bu zamana gitmek için tıklayın"
|
||||
},
|
||||
"normalActivity": "Normal",
|
||||
"needsReview": "İnceleme Gerekiyor",
|
||||
"securityConcern": "Güvenlik endişesi"
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,20 +197,6 @@
|
||||
"audioTranscription": {
|
||||
"label": "Çözümle",
|
||||
"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ı",
|
||||
@ -222,13 +208,11 @@
|
||||
"success": "Takip edilen nesne başarıyla silindi."
|
||||
}
|
||||
},
|
||||
"tooltip": "Eşleşme: {{type}} (%{{confidence}})",
|
||||
"previousTrackedObject": "Önceki izlenen nesne",
|
||||
"nextTrackedObject": "Sonraki izlenen nesne"
|
||||
"tooltip": "Eşleşme: {{type}} (%{{confidence}})"
|
||||
},
|
||||
"dialog": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
@ -286,8 +270,5 @@
|
||||
"previous": "Önceki slayt",
|
||||
"next": "Sonraki slayt"
|
||||
}
|
||||
},
|
||||
"concerns": {
|
||||
"label": "Endişeler"
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
"validation": {
|
||||
"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:",
|
||||
"toast": {
|
||||
|
||||
@ -176,14 +176,5 @@
|
||||
"noVideoSource": "Anlık görüntü için kullanılabilir bir video kaynağı bulunamadı.",
|
||||
"captureFailed": "Anlık görüntü yakalanamadı.",
|
||||
"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}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"face_recognition_speed": "Yüz Tanıma Hızı",
|
||||
"yolov9_plate_detection_speed": "YOLOv9 Plaka Tanıma Hızı",
|
||||
"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ı"
|
||||
"yolov9_plate_detection": "YOLOv9 Plaka Tanıma"
|
||||
},
|
||||
"infPerSecond": "Saniye Başına Çıkarım",
|
||||
"title": "Zenginleştirmeler",
|
||||
"averageInf": "Ortalama Çıkarım Süresi"
|
||||
"title": "Zenginleştirmeler"
|
||||
},
|
||||
"logs": {
|
||||
"download": {
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
"modelCreated": "Модель успішно створено. Використовуйте режим перегляду «Нещодавні класифікації», щоб додати зображення для відсутніх станів, а потім навчіть модель.",
|
||||
"missingStatesWarning": {
|
||||
"title": "Приклади відсутніх станів",
|
||||
"description": "Для найкращих результатів рекомендується вибрати приклади для всіх станів. Ви можете продовжити, не вибираючи всі стани, але модель не буде навчена, доки всі стани не матимуть зображень. Після продовження скористайтеся поданням «Нещодавні класифікації», щоб класифікувати зображення для відсутніх станів, а потім навчіть модель."
|
||||
"description": "Ви не вибрали приклади для всіх станів. Модель не буде навчена, доки всі стани не матимуть зображень. Після продовження скористайтеся поданням «Нещодавні класифікації», щоб класифікувати зображення для відсутніх станів, а потім навчіть модель."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
"name": {
|
||||
"inputPlaceHolder": "Введіть назву…",
|
||||
"title": "Ім'я",
|
||||
"tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони на цій камері."
|
||||
"tips": "Назва має містити щонайменше 2 символи, принаймні одну літеру та не повинна бути назвою камери чи іншої зони."
|
||||
},
|
||||
"desc": {
|
||||
"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 axios from "axios";
|
||||
import { ReactNode } from "react";
|
||||
import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect";
|
||||
|
||||
axios.defaults.baseURL = `${baseUrl}api/`;
|
||||
|
||||
@ -32,8 +31,7 @@ export function ApiProvider({ children, options }: ApiProviderType) {
|
||||
) {
|
||||
// redirect to the login page if not already there
|
||||
const loginPage = error.response.headers.get("location") ?? "login";
|
||||
if (window.location.href !== loginPage && !isRedirectingToLogin()) {
|
||||
setRedirectingToLogin(true);
|
||||
if (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 { AuthContext } from "@/context/auth-context";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import {
|
||||
isRedirectingToLogin,
|
||||
setRedirectingToLogin,
|
||||
} from "@/api/auth-redirect";
|
||||
|
||||
export default function ProtectedRoute({
|
||||
requiredRoles,
|
||||
@ -14,20 +10,6 @@ export default function ProtectedRoute({
|
||||
}) {
|
||||
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) {
|
||||
return (
|
||||
<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
|
||||
if (!auth.user) {
|
||||
return (
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
);
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
// 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 BlurredIconButton from "../button/BlurredIconButton";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
|
||||
type ExportProps = {
|
||||
className: string;
|
||||
@ -41,7 +40,6 @@ export default function ExportCard({
|
||||
onDelete,
|
||||
}: ExportProps) {
|
||||
const { t } = useTranslation(["views/exports"]);
|
||||
const isAdmin = useIsAdmin();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [loading, setLoading] = useState(
|
||||
exportedRecording.thumb_path.length > 0,
|
||||
@ -197,7 +195,7 @@ export default function ExportCard({
|
||||
</Tooltip>
|
||||
</a>
|
||||
)}
|
||||
{isAdmin && !exportedRecording.in_progress && (
|
||||
{!exportedRecording.in_progress && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<BlurredIconButton
|
||||
@ -214,7 +212,6 @@ export default function ExportCard({
|
||||
<TooltipContent>{t("tooltip.editName")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<BlurredIconButton
|
||||
@ -230,7 +227,6 @@ export default function ExportCard({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("tooltip.deleteExport")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -407,6 +407,30 @@ export default function Step3ChooseExamples({
|
||||
return allClasses.every((className) => statesWithExamples.has(className));
|
||||
}, [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(() => {
|
||||
if (!unknownImages) return false;
|
||||
const allImages = unknownImages.slice(0, 24);
|
||||
@ -570,7 +594,9 @@ export default function Step3ChooseExamples({
|
||||
}
|
||||
variant="select"
|
||||
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" />}
|
||||
{t("button.continue", { ns: "common" })}
|
||||
|
||||
@ -559,7 +559,6 @@ export function TrackingDetails({
|
||||
isDetailMode={true}
|
||||
camera={event.camera}
|
||||
currentTimeOverride={currentTime}
|
||||
enableGapControllerRecovery={true}
|
||||
/>
|
||||
{isVideoLoading && (
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Hls, { HlsConfig } from "hls.js";
|
||||
import Hls from "hls.js";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
import VideoControls from "./VideoControls";
|
||||
@ -57,7 +57,6 @@ type HlsVideoPlayerProps = {
|
||||
isDetailMode?: boolean;
|
||||
camera?: string;
|
||||
currentTimeOverride?: number;
|
||||
enableGapControllerRecovery?: boolean;
|
||||
};
|
||||
|
||||
export default function HlsVideoPlayer({
|
||||
@ -82,7 +81,6 @@ export default function HlsVideoPlayer({
|
||||
isDetailMode = false,
|
||||
camera,
|
||||
currentTimeOverride,
|
||||
enableGapControllerRecovery = false,
|
||||
}: HlsVideoPlayerProps) {
|
||||
const { t } = useTranslation("components/player");
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
@ -172,23 +170,11 @@ export default function HlsVideoPlayer({
|
||||
return;
|
||||
}
|
||||
|
||||
// Base HLS configuration
|
||||
const baseConfig: Partial<HlsConfig> = {
|
||||
hlsRef.current = new Hls({
|
||||
maxBufferLength: 10,
|
||||
maxBufferSize: 20 * 1000 * 1000,
|
||||
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.loadSource(currentSource.playlist);
|
||||
videoRef.current.playbackRate = currentPlaybackRate;
|
||||
@ -201,13 +187,7 @@ export default function HlsVideoPlayer({
|
||||
hlsRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
videoRef,
|
||||
hlsRef,
|
||||
useHlsCompat,
|
||||
currentSource,
|
||||
enableGapControllerRecovery,
|
||||
]);
|
||||
}, [videoRef, hlsRef, useHlsCompat, currentSource]);
|
||||
|
||||
// state handling
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user