mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-08 12:31:43 -05:00
Compare commits
17 Commits
dependabot
...
misc-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21565a209 | ||
|
|
e658a70e0f | ||
|
|
82cb69526b | ||
|
|
fe3677c7df | ||
|
|
1fec95f88e | ||
|
|
6accc38275 | ||
|
|
ff20be58b4 | ||
|
|
fc3f798bd6 | ||
|
|
44e695362a | ||
|
|
9fbc854bf5 | ||
|
|
334acd6078 | ||
|
|
92c503070c | ||
|
|
ecd7d04228 | ||
|
|
11576e9e68 | ||
|
|
2cfb118981 | ||
|
|
e1c273be8d | ||
|
|
ea1533f456 |
@@ -166,6 +166,10 @@ In this example:
|
||||
- If no mapping matches, Frigate falls back to `default_role` if configured.
|
||||
- If `role_map` is not defined, Frigate assumes the role header directly contains `admin`, `viewer`, or a custom role name.
|
||||
|
||||
**Note on matching semantics:**
|
||||
|
||||
- Admin precedence: if the `admin` mapping matches, Frigate resolves the session to `admin` to avoid accidental downgrade when a user belongs to multiple groups (for example both `admin` and `viewer` groups).
|
||||
|
||||
#### Port Considerations
|
||||
|
||||
**Authenticated Port (8971)**
|
||||
|
||||
@@ -439,10 +439,11 @@ def resolve_role(
|
||||
Determine the effective role for a request based on proxy headers and configuration.
|
||||
|
||||
Order of resolution:
|
||||
1. If a role header is defined in proxy_config.header_map.role:
|
||||
- If a role_map is configured, treat the header as group claims
|
||||
(split by proxy_config.separator) and map to roles.
|
||||
- If no role_map is configured, treat the header as role names directly.
|
||||
1. If a role header is defined in proxy_config.header_map.role:
|
||||
- If a role_map is configured, treat the header as group claims
|
||||
(split by proxy_config.separator) and map to roles.
|
||||
Admin matches short-circuit to admin.
|
||||
- If no role_map is configured, treat the header as role names directly.
|
||||
2. If no valid role is found, return proxy_config.default_role if it's valid in config_roles, else 'viewer'.
|
||||
|
||||
Args:
|
||||
@@ -492,6 +493,12 @@ def resolve_role(
|
||||
}
|
||||
logger.debug("Matched roles from role_map: %s", matched_roles)
|
||||
|
||||
# If admin matches, prioritize it to avoid accidental downgrade when
|
||||
# users belong to both admin and lower-privilege groups.
|
||||
if "admin" in matched_roles and "admin" in config_roles:
|
||||
logger.debug("Resolved role (with role_map) to 'admin'.")
|
||||
return "admin"
|
||||
|
||||
if matched_roles:
|
||||
resolved = next(
|
||||
(r for r in config_roles if r in matched_roles), validated_default
|
||||
|
||||
@@ -31,6 +31,21 @@ class TestProxyRoleResolution(unittest.TestCase):
|
||||
role = resolve_role(headers, self.proxy_config, self.config_roles)
|
||||
self.assertEqual(role, "admin")
|
||||
|
||||
def test_role_map_or_matching(self):
|
||||
config = self.proxy_config
|
||||
config.header_map.role_map = {
|
||||
"admin": ["group_admin", "group_privileged"],
|
||||
}
|
||||
|
||||
# OR semantics: a single matching group should map to the role
|
||||
headers = {"x-remote-role": "group_admin"}
|
||||
role = resolve_role(headers, config, self.config_roles)
|
||||
self.assertEqual(role, "admin")
|
||||
|
||||
headers = {"x-remote-role": "group_admin|group_privileged"}
|
||||
role = resolve_role(headers, config, self.config_roles)
|
||||
self.assertEqual(role, "admin")
|
||||
|
||||
def test_direct_role_header_with_separator(self):
|
||||
config = self.proxy_config
|
||||
config.header_map.role_map = None # disable role_map
|
||||
|
||||
@@ -214,6 +214,7 @@ class CameraWatchdog(threading.Thread):
|
||||
self.latest_valid_segment_time: float = 0
|
||||
self.latest_invalid_segment_time: float = 0
|
||||
self.latest_cache_segment_time: float = 0
|
||||
self.record_enable_time: datetime | None = None
|
||||
|
||||
def _update_enabled_state(self) -> bool:
|
||||
"""Fetch the latest config and update enabled state."""
|
||||
@@ -261,6 +262,9 @@ class CameraWatchdog(threading.Thread):
|
||||
def run(self) -> None:
|
||||
if self._update_enabled_state():
|
||||
self.start_all_ffmpeg()
|
||||
# If recording is enabled at startup, set the grace period timer
|
||||
if self.config.record.enabled:
|
||||
self.record_enable_time = datetime.now().astimezone(timezone.utc)
|
||||
|
||||
time.sleep(self.sleeptime)
|
||||
while not self.stop_event.wait(self.sleeptime):
|
||||
@@ -270,13 +274,15 @@ class CameraWatchdog(threading.Thread):
|
||||
self.logger.debug(f"Enabling camera {self.config.name}")
|
||||
self.start_all_ffmpeg()
|
||||
|
||||
# reset all timestamps
|
||||
# reset all timestamps and record the enable time for grace period
|
||||
self.latest_valid_segment_time = 0
|
||||
self.latest_invalid_segment_time = 0
|
||||
self.latest_cache_segment_time = 0
|
||||
self.record_enable_time = datetime.now().astimezone(timezone.utc)
|
||||
else:
|
||||
self.logger.debug(f"Disabling camera {self.config.name}")
|
||||
self.stop_all_ffmpeg()
|
||||
self.record_enable_time = None
|
||||
|
||||
# update camera status
|
||||
self.requestor.send_data(
|
||||
@@ -361,6 +367,12 @@ class CameraWatchdog(threading.Thread):
|
||||
if self.config.record.enabled and "record" in p["roles"]:
|
||||
now_utc = datetime.now().astimezone(timezone.utc)
|
||||
|
||||
# Check if we're within the grace period after enabling recording
|
||||
# Grace period: 90 seconds allows time for ffmpeg to start and create first segment
|
||||
in_grace_period = self.record_enable_time is not None and (
|
||||
now_utc - self.record_enable_time
|
||||
) < timedelta(seconds=90)
|
||||
|
||||
latest_cache_dt = (
|
||||
datetime.fromtimestamp(
|
||||
self.latest_cache_segment_time, tz=timezone.utc
|
||||
@@ -386,10 +398,16 @@ class CameraWatchdog(threading.Thread):
|
||||
)
|
||||
|
||||
# ensure segments are still being created and that they have valid video data
|
||||
cache_stale = now_utc > (latest_cache_dt + timedelta(seconds=120))
|
||||
valid_stale = now_utc > (latest_valid_dt + timedelta(seconds=120))
|
||||
# Skip checks during grace period to allow segments to start being created
|
||||
cache_stale = not in_grace_period and now_utc > (
|
||||
latest_cache_dt + timedelta(seconds=120)
|
||||
)
|
||||
valid_stale = not in_grace_period and now_utc > (
|
||||
latest_valid_dt + timedelta(seconds=120)
|
||||
)
|
||||
invalid_stale_condition = (
|
||||
self.latest_invalid_segment_time > 0
|
||||
and not in_grace_period
|
||||
and now_utc > (latest_invalid_dt + timedelta(seconds=120))
|
||||
and self.latest_valid_segment_time
|
||||
<= self.latest_invalid_segment_time
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"bg": "Български (Búlgar)",
|
||||
"gl": "Galego (Gallec)",
|
||||
"id": "Bahasa Indonesia (Indonesi)",
|
||||
"ur": "اردو (Urdú)"
|
||||
"ur": "اردو (Urdú)",
|
||||
"hr": "Hrvatski (croat)"
|
||||
},
|
||||
"system": "Sistema",
|
||||
"systemMetrics": "Mètriques del sistema",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
"description": {
|
||||
"addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.",
|
||||
"placeholder": "Introduïu un nom per a aquesta col·lecció",
|
||||
"invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions."
|
||||
"invalidName": "Nom no vàlid. Els noms només poden incloure lletres, números, espais, apòstrofs, guions baixos i guions.",
|
||||
"nameCannotContainHash": "El nom no pot contenir #."
|
||||
},
|
||||
"documentTitle": "Biblioteca de rostres - Frigate",
|
||||
"uploadFaceImage": {
|
||||
|
||||
@@ -532,7 +532,7 @@
|
||||
"hide": "Amaga contrasenya",
|
||||
"requirements": {
|
||||
"title": "Requisits contrasenya:",
|
||||
"length": "Com a mínim 8 carácters",
|
||||
"length": "Com a mínim 12 carácters",
|
||||
"uppercase": "Com a mínim una majúscula",
|
||||
"digit": "Com a mínim un digit",
|
||||
"special": "Com a mínim un carácter especial (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"harp": "Harpe",
|
||||
"bell": "Klokke",
|
||||
"harmonica": "Harmonika",
|
||||
"bagpipes": "Sækkepibe",
|
||||
"bagpipes": "Sækkepiber",
|
||||
"didgeridoo": "Didgeridoo",
|
||||
"jazz": "Jazz",
|
||||
"opera": "Opera",
|
||||
@@ -78,7 +78,7 @@
|
||||
"camera": "Kamera",
|
||||
"tools": "Værktøj",
|
||||
"hammer": "Hammer",
|
||||
"drill": "Bore",
|
||||
"drill": "Boremaskine",
|
||||
"explosion": "Eksplosion",
|
||||
"fireworks": "Nytårskrudt",
|
||||
"babbling": "Pludren",
|
||||
|
||||
@@ -193,7 +193,8 @@
|
||||
"bg": "Български (Bulgarsk)",
|
||||
"gl": "Galego (Galisisk)",
|
||||
"id": "Bahasa Indonesia (Indonesisk)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Kroatisk)"
|
||||
},
|
||||
"appearance": "Udseende",
|
||||
"darkMode": {
|
||||
@@ -221,7 +222,7 @@
|
||||
},
|
||||
"restart": "Genstart Frigate",
|
||||
"live": {
|
||||
"title": "Live",
|
||||
"title": "Direkte",
|
||||
"allCameras": "Alle kameraer",
|
||||
"cameras": {
|
||||
"title": "Kameraer",
|
||||
@@ -240,17 +241,17 @@
|
||||
"current": "Aktiv bruger: {{user}}",
|
||||
"anonymous": "anonym",
|
||||
"logout": "Log ud",
|
||||
"setPassword": "Set Password"
|
||||
"setPassword": "Vælg kodeord"
|
||||
},
|
||||
"classification": "Kategorisering"
|
||||
},
|
||||
"toast": {
|
||||
"copyUrlToClipboard": "Kopieret URL til klippebord.",
|
||||
"copyUrlToClipboard": "Kopieret URL til udklipsholder.",
|
||||
"save": {
|
||||
"title": "Gem",
|
||||
"error": {
|
||||
"title": "Ændringer kan ikke gemmes: {{errorMessage}}",
|
||||
"noMessage": "Kan ikke gemme konfigurationsændringer"
|
||||
"title": "Ændringer kunne ikke gemmes: {{errorMessage}}",
|
||||
"noMessage": "Kunne ikke gemme konfigurationsændringer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -261,7 +262,7 @@
|
||||
"desc": "Admins har fuld adgang til Frigate UI. Viewers er begrænset til at se kameraer, gennemse items, og historik i UI."
|
||||
},
|
||||
"pagination": {
|
||||
"label": "paginering",
|
||||
"label": "sideinddeling",
|
||||
"previous": {
|
||||
"title": "Forrige",
|
||||
"label": "Gå til forrige side"
|
||||
@@ -273,9 +274,9 @@
|
||||
"more": "Flere sider"
|
||||
},
|
||||
"accessDenied": {
|
||||
"documentTitle": "Adgang forbudt - Frigate",
|
||||
"title": "Adgang forbudt",
|
||||
"desc": "Du har ikke tiiladelse til at se denne side."
|
||||
"documentTitle": "Adgang nægtet - Frigate",
|
||||
"title": "Adgang nægtet",
|
||||
"desc": "Du har ikke rettigheder til at se denne side."
|
||||
},
|
||||
"notFound": {
|
||||
"documentTitle": "Ikke fundet - Frigate",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"cameraDisabled": "Kamera er deaktiveret",
|
||||
"noPreviewFoundFor": "Ingen forhåndsvisning fundet for {{cameraName}}",
|
||||
"submitFrigatePlus": {
|
||||
"title": "Indsend denne frame til Frigate+?",
|
||||
"title": "Indsend dette billede til Frigate+?",
|
||||
"submit": "Indsend"
|
||||
},
|
||||
"livePlayerRequiredIOSVersion": "iOS 17.1 eller nyere kræves for denne type livestream.",
|
||||
"streamOffline": {
|
||||
"title": "Stream offline",
|
||||
"desc": "Der er ikke modtaget nogen frames på {{cameraName}}-<code>detect</code>-streamen, tjek fejlloggene."
|
||||
"desc": "Der er ikke modtaget nogen billeder på {{cameraName}}-<code>detect</code>-streamen, tjek fejllogs."
|
||||
},
|
||||
"stats": {
|
||||
"streamType": {
|
||||
@@ -18,8 +18,8 @@
|
||||
"short": "Type"
|
||||
},
|
||||
"bandwidth": {
|
||||
"title": "Bandbredde:",
|
||||
"short": "Bandbredde"
|
||||
"title": "Båndbredde:",
|
||||
"short": "Båndbredde"
|
||||
},
|
||||
"latency": {
|
||||
"title": "Latenstid:",
|
||||
@@ -31,8 +31,21 @@
|
||||
},
|
||||
"droppedFrames": {
|
||||
"short": {
|
||||
"title": "Tabt"
|
||||
}
|
||||
"title": "Tabt",
|
||||
"value": "{{droppedFrames}} billeder"
|
||||
},
|
||||
"title": "Tabte billeder:"
|
||||
},
|
||||
"totalFrames": "Antal billeder i alt:",
|
||||
"decodedFrames": "Dekodede billeder:",
|
||||
"droppedFrameRate": "Rate for tabte billeder:"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"submittedFrigatePlus": "Billede sendt til Frigate+"
|
||||
},
|
||||
"error": {
|
||||
"submitFrigatePlusFailed": "Kunne ikke sende billede til Frigate+"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,104 @@
|
||||
"move": {
|
||||
"clickMove": {
|
||||
"label": "Klik i billedrammen for at centrere kameraet",
|
||||
"enable": "Aktivér klik for at flytte"
|
||||
"enable": "Aktivér klik for at flytte",
|
||||
"disable": "Deaktiver klik for at flytte"
|
||||
},
|
||||
"left": {
|
||||
"label": "Flyt PTZ-kameraet til venstre"
|
||||
},
|
||||
"up": {
|
||||
"label": "Flyt PTZ kamera op"
|
||||
},
|
||||
"down": {
|
||||
"label": "Flyt PTZ-kameraet ned"
|
||||
},
|
||||
"right": {
|
||||
"label": "Flyt PTZ-kameraet til højre"
|
||||
}
|
||||
}
|
||||
},
|
||||
"zoom": {
|
||||
"in": {
|
||||
"label": "Zoom PTZ-kamera ind"
|
||||
},
|
||||
"out": {
|
||||
"label": "Zoom PTZ kamera ud"
|
||||
}
|
||||
},
|
||||
"focus": {
|
||||
"in": {
|
||||
"label": "Focus PTZ kamera ind"
|
||||
},
|
||||
"out": {
|
||||
"label": "Focus PTZ kamera ud"
|
||||
}
|
||||
},
|
||||
"frame": {
|
||||
"center": {
|
||||
"label": "Klik på billedet for at centrere PTZ-kameraet"
|
||||
}
|
||||
},
|
||||
"presets": "PTZ kamera forudindstillinger"
|
||||
},
|
||||
"camera": {
|
||||
"enable": "Aktivér kamera",
|
||||
"disable": "Deaktivér kamera"
|
||||
},
|
||||
"muteCameras": {
|
||||
"enable": "Slå lyd på alle kameraer fra",
|
||||
"disable": "Slå lyd på alle kameraer til"
|
||||
},
|
||||
"detect": {
|
||||
"enable": "Aktiver detektering",
|
||||
"disable": "Deaktiver detektering"
|
||||
},
|
||||
"recording": {
|
||||
"enable": "Aktivér optagelse",
|
||||
"disable": "Deaktiver optagelse"
|
||||
},
|
||||
"snapshots": {
|
||||
"enable": "Aktivér Snapshots",
|
||||
"disable": "Deaktivér Snapshots"
|
||||
},
|
||||
"snapshot": {
|
||||
"takeSnapshot": "Hent instant snapshot",
|
||||
"noVideoSource": "Ingen videokilde til snapshot.",
|
||||
"captureFailed": "Kunne ikke tage snapshot.",
|
||||
"downloadStarted": "Hentning af snapshot startet."
|
||||
},
|
||||
"audioDetect": {
|
||||
"enable": "Aktiver lyddetektor",
|
||||
"disable": "Deaktiver lyddetektor"
|
||||
},
|
||||
"transcription": {
|
||||
"enable": "Aktiver Live Audio Transkription",
|
||||
"disable": "Deaktiver Live Audio Transkription"
|
||||
},
|
||||
"autotracking": {
|
||||
"enable": "Aktiver Autotracking",
|
||||
"disable": "Deaktiver Autotracking"
|
||||
},
|
||||
"streamStats": {
|
||||
"enable": "Vis Stream statistik",
|
||||
"disable": "Skjul Stream statistik"
|
||||
},
|
||||
"manualRecording": {
|
||||
"title": "Manuel optagelse",
|
||||
"tips": "Hent et øjebliksbillede eller start en manuel begivenhed baseret på dette kameras indstillinger for optagelse af opbevaring.",
|
||||
"playInBackground": {
|
||||
"label": "Afspil i baggrunden",
|
||||
"desc": "Aktiver denne mulighed for at fortsætte streaming, når afspilleren er skjult."
|
||||
},
|
||||
"showStats": {
|
||||
"label": "Vis statistik",
|
||||
"desc": "Aktiver denne mulighed for at vise streamstatistikker som en overlejring på kameraets feed."
|
||||
},
|
||||
"debugView": "Debug View",
|
||||
"start": "Start on-demand optagelse",
|
||||
"started": "Start manuel optagelse.",
|
||||
"failedToStart": "Manuel optagelse fejlede.",
|
||||
"recordDisabledTips": "Da optagelsen er deaktiveret eller begrænset i konfig for dette kamera, gemmes der kun et snapshot.",
|
||||
"end": "Afslut manuel optagelse",
|
||||
"ended": "Afsluttet manuel optagelse."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,8 @@
|
||||
"lt": "Lietuvių (Litauisch)",
|
||||
"bg": "Български (bulgarisch)",
|
||||
"gl": "Galego (Galicisch)",
|
||||
"id": "Bahasa Indonesia (Indonesisch)"
|
||||
"id": "Bahasa Indonesia (Indonesisch)",
|
||||
"hr": "Hrvatski (Kroatisch)"
|
||||
},
|
||||
"appearance": "Erscheinung",
|
||||
"theme": {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"placeholder": "Gib einen Name für diese Kollektion ein",
|
||||
"addFace": "Füge der Gesichtsbibliothek eine neue Sammlung hinzu, indem du ein Bild hochlädst.",
|
||||
"invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten."
|
||||
"invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten.",
|
||||
"nameCannotContainHash": "Der Name darf keine # enthalten."
|
||||
},
|
||||
"details": {
|
||||
"person": "Person",
|
||||
|
||||
@@ -544,7 +544,7 @@
|
||||
"placeholder": "Passwort eingeben",
|
||||
"requirements": {
|
||||
"title": "Passwort Anforderungen:",
|
||||
"length": "Mindestens 8 Zeichen",
|
||||
"length": "Mindestens 12 Zeichen",
|
||||
"uppercase": "Mindestens ein Großbuchstabe",
|
||||
"digit": "Mindestens eine Ziffer",
|
||||
"special": "Mindestens ein Sonderzeichen (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"bg": "Български (bulgaaria keel)",
|
||||
"gl": "Galego (galeegi keel)",
|
||||
"id": "Bahasa Indonesia (indoneesia keel)",
|
||||
"ur": "اردو (urdu keel)"
|
||||
"ur": "اردو (urdu keel)",
|
||||
"hr": "Hrvatski (horvaadi keel)"
|
||||
},
|
||||
"system": "Süsteem",
|
||||
"systemMetrics": "Süsteemi meetrika",
|
||||
|
||||
@@ -30,11 +30,21 @@
|
||||
"stationary": "{{label}} jäi paigale",
|
||||
"active": "{{label}} muutus aktiivseks",
|
||||
"entered_zone": "{{label}} sisenes tsooni {{zones}}",
|
||||
"visible": "{{label}} on tuvastatud"
|
||||
"visible": "{{label}} on tuvastatud",
|
||||
"header": {
|
||||
"zones": "Tsoonid",
|
||||
"ratio": "Suhtarv",
|
||||
"area": "Ala",
|
||||
"score": "Punktiskoor"
|
||||
}
|
||||
},
|
||||
"title": "Jälgimise üksikasjad",
|
||||
"noImageFound": "Selle ajatempli kohta ei leidu pilti.",
|
||||
"createObjectMask": "Loo objektimask"
|
||||
"createObjectMask": "Loo objektimask",
|
||||
"carousel": {
|
||||
"previous": "Eelmine slaid",
|
||||
"next": "Järgmine slaid"
|
||||
}
|
||||
},
|
||||
"documentTitle": "Avasta - Frigate",
|
||||
"generativeAI": "Generatiivne tehisaru",
|
||||
@@ -63,12 +73,15 @@
|
||||
"tips": {
|
||||
"mismatch_one": "Tuvastasin {{count}} võõra objekti ja need on lisatud ülevaatamiseks. Need objektid kas ei ole piisavad häire või tuvastamise jaoks, aga ka võivad juba olla eemaldatud või kustutatud.",
|
||||
"mismatch_other": "Tuvastasin {{count}} võõrast objekti ja need on lisatud ülevaatamiseks. Need objektid kas ei ole piisavad häire või tuvastamise jaoks, aga ka võivad juba olla eemaldatud või kustutatud."
|
||||
}
|
||||
},
|
||||
"title": "Vaata objekti üksikasju",
|
||||
"desc": "Vaata objekti üksikasju"
|
||||
},
|
||||
"snapshotScore": {
|
||||
"label": "Hetkvõttete punktiskoor"
|
||||
},
|
||||
"regenerateFromSnapshot": "Loo uuesti hetkvõttest",
|
||||
"timestamp": "Ajatampel"
|
||||
}
|
||||
},
|
||||
"trackedObjectDetails": "Jälgitava objekti üksikasjad"
|
||||
}
|
||||
|
||||
@@ -4,10 +4,23 @@
|
||||
"password": "Salasõna",
|
||||
"passwordPlaceholder": "Valikuline",
|
||||
"customUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht",
|
||||
"connectionSettings": "Ühenduse seadistused"
|
||||
"connectionSettings": "Ühenduse seadistused",
|
||||
"port": "Port",
|
||||
"username": "Kasutajanimi",
|
||||
"usernamePlaceholder": "Valikuline"
|
||||
},
|
||||
"step3": {
|
||||
"streamUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht"
|
||||
"streamUrlPlaceholder": "rtsp://kasutajanimi:salasõna@host:port/asukoht",
|
||||
"url": "Võrguaadress",
|
||||
"resolution": "Resolutsioon",
|
||||
"quality": "Kvaliteet",
|
||||
"roles": "Rollid",
|
||||
"roleLabels": {
|
||||
"record": "Salvestamine",
|
||||
"audio": "Heliriba"
|
||||
},
|
||||
"connected": "Ühendatud",
|
||||
"featuresTitle": "Funktsionaalsused"
|
||||
},
|
||||
"steps": {
|
||||
"probeOrSnapshot": "Võta proov või tee hetkvõte"
|
||||
@@ -15,7 +28,34 @@
|
||||
"step2": {
|
||||
"testing": {
|
||||
"fetchingSnapshot": "Laadin kaamera hetkvõtet alla..."
|
||||
}
|
||||
},
|
||||
"retry": "Proovi uuesti",
|
||||
"manufacturer": "Tootja",
|
||||
"model": "Mudel",
|
||||
"firmware": "Püsivara",
|
||||
"profiles": "Profiilid",
|
||||
"presets": "Eelseadistused",
|
||||
"useCandidate": "Kasuta",
|
||||
"uriCopy": "Kopeeri",
|
||||
"connected": "Ühendatud"
|
||||
},
|
||||
"testResultLabels": {
|
||||
"resolution": "Resolutsioon",
|
||||
"video": "Video",
|
||||
"audio": "Heliriba",
|
||||
"fps": "Kaadrisagedus"
|
||||
},
|
||||
"step4": {
|
||||
"reload": "Laadi uuesti",
|
||||
"connecting": "Ühendan…",
|
||||
"valid": "Kehtiv",
|
||||
"failed": "Ebaõnnestunud",
|
||||
"connectStream": "Ühenda",
|
||||
"connectingStream": "Ühendan",
|
||||
"disconnectStream": "Katkesta ühendus",
|
||||
"roles": "Rollid",
|
||||
"none": "Määramata",
|
||||
"error": "Viga"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
@@ -29,7 +69,10 @@
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"password": "Lähtesta salasõna"
|
||||
"password": "Lähtesta salasõna",
|
||||
"username": "Kasutajanimi",
|
||||
"actions": "Tegevused",
|
||||
"role": "Roll"
|
||||
},
|
||||
"dialog": {
|
||||
"form": {
|
||||
@@ -53,7 +96,7 @@
|
||||
"hide": "Peida salasõna",
|
||||
"requirements": {
|
||||
"title": "Salasõna reeglid:",
|
||||
"length": "Vähemalt 8 tähemärki",
|
||||
"length": "Vähemalt 12 tähemärki",
|
||||
"uppercase": "Vähemalt üks suurtäht",
|
||||
"digit": "Vähemalt üks number",
|
||||
"special": "Vähemalt üks erimärk (!@#$%^&*(),.?\":{}|<>)"
|
||||
@@ -70,6 +113,9 @@
|
||||
"currentPassword": {
|
||||
"title": "Senine salasõna",
|
||||
"placeholder": "Sisesta oma senine salasõna"
|
||||
},
|
||||
"user": {
|
||||
"title": "Kasutajanimi"
|
||||
}
|
||||
},
|
||||
"createUser": {
|
||||
@@ -84,12 +130,42 @@
|
||||
"currentPasswordRequired": "Senine salasõna on vajalik",
|
||||
"incorrectCurrentPassword": "Senine salasõna pole õige",
|
||||
"passwordVerificationFailed": "Salasõna kontrollimine ei õnnestunud"
|
||||
},
|
||||
"changeRole": {
|
||||
"roleInfo": {
|
||||
"admin": "Peakasutaja",
|
||||
"viewer": "Vaataja"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Kasutajad"
|
||||
},
|
||||
"debug": {
|
||||
"boundingBoxes": {
|
||||
"desc": "Näita jälgitavate objektide ümber märgiskaste"
|
||||
},
|
||||
"title": "Silumine ja veaotsing",
|
||||
"debugging": "Veaotsing ja silumine",
|
||||
"audio": {
|
||||
"title": "Heliriba",
|
||||
"score": "punktiskoor"
|
||||
},
|
||||
"timestamp": {
|
||||
"title": "Ajatempel"
|
||||
},
|
||||
"zones": {
|
||||
"title": "Tsoonid"
|
||||
},
|
||||
"regions": {
|
||||
"title": "Alad"
|
||||
},
|
||||
"paths": {
|
||||
"title": "Asukohad"
|
||||
},
|
||||
"objectShapeFilterDrawing": {
|
||||
"score": "Punktiskoor",
|
||||
"ratio": "Suhtarv",
|
||||
"area": "Ala"
|
||||
}
|
||||
},
|
||||
"documentTitle": {
|
||||
@@ -113,10 +189,31 @@
|
||||
"automaticLiveView": {
|
||||
"label": "Automaatne otseülekande vaade"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Kalender",
|
||||
"firstWeekday": {
|
||||
"sunday": "Pühapäev",
|
||||
"monday": "Esmaspäev",
|
||||
"label": "Esimene nädalapäev"
|
||||
}
|
||||
},
|
||||
"storedLayouts": {
|
||||
"title": "Salvestatud paigutused"
|
||||
},
|
||||
"recordingsViewer": {
|
||||
"title": "Salvestuste vaataja"
|
||||
}
|
||||
},
|
||||
"cameraManagement": {
|
||||
"backToSettings": "Tagasi kaameraseadistuste juurde"
|
||||
"backToSettings": "Tagasi kaameraseadistuste juurde",
|
||||
"cameraConfig": {
|
||||
"enabled": "Kasutusel",
|
||||
"ffmpeg": {
|
||||
"pathPlaceholder": "rtsp://...",
|
||||
"roles": "Rollid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"notificationSettings": {
|
||||
@@ -130,6 +227,16 @@
|
||||
"success": {
|
||||
"settingSaved": "Teavituste seadistused on salvestatud."
|
||||
}
|
||||
},
|
||||
"title": "Teavitused",
|
||||
"email": {
|
||||
"title": "E-post"
|
||||
},
|
||||
"cameras": {
|
||||
"title": "Kaamerad"
|
||||
},
|
||||
"suspendTime": {
|
||||
"suspend": "Peata arvuti töö"
|
||||
}
|
||||
},
|
||||
"frigatePlus": {
|
||||
@@ -145,20 +252,48 @@
|
||||
"cleanCopySnapshots": "<code>clean_copy</code> Hetkvõtted",
|
||||
"camera": "Kaamera"
|
||||
}
|
||||
},
|
||||
"modelInfo": {
|
||||
"plusModelType": {
|
||||
"userModel": "Peenhäälestatud"
|
||||
},
|
||||
"cameras": "Kaamerad"
|
||||
}
|
||||
},
|
||||
"masksAndZones": {
|
||||
"zones": {
|
||||
"point_one": "{{count}} punkt",
|
||||
"point_other": "{{count}} punkti"
|
||||
"point_other": "{{count}} punkti",
|
||||
"label": "Tsoonid",
|
||||
"desc": {
|
||||
"documentation": "Dokumentatsioon"
|
||||
},
|
||||
"name": {
|
||||
"title": "Nimi"
|
||||
},
|
||||
"inertia": {
|
||||
"title": "Inerts"
|
||||
},
|
||||
"objects": {
|
||||
"title": "Objektid"
|
||||
}
|
||||
},
|
||||
"motionMasks": {
|
||||
"point_one": "{{count}} punkt",
|
||||
"point_other": "{{count}} punkti"
|
||||
"point_other": "{{count}} punkti",
|
||||
"desc": {
|
||||
"documentation": "Dokumentatsioon"
|
||||
}
|
||||
},
|
||||
"objectMasks": {
|
||||
"point_one": "{{count}} punkt",
|
||||
"point_other": "{{count}} punkti"
|
||||
"point_other": "{{count}} punkti",
|
||||
"desc": {
|
||||
"documentation": "Dokumentatsioon"
|
||||
},
|
||||
"objects": {
|
||||
"title": "Objektid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
@@ -167,6 +302,21 @@
|
||||
"userRolesUpdated_one": "{{count}} selle rolliga kasutaja on nüüd määratud Vaatajaks, kellel on ligipääs kõikidele kaameratele.",
|
||||
"userRolesUpdated_other": "{{count}} selle rolliga kasutajat on nüüd määratud Vaatajaks, kellel on ligipääs kõikidele kaameratele."
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"role": "Roll",
|
||||
"cameras": "Kaamerad",
|
||||
"actions": "Tegevused"
|
||||
},
|
||||
"dialog": {
|
||||
"deleteRole": {
|
||||
"deleting": "Kustutan..."
|
||||
},
|
||||
"form": {
|
||||
"cameras": {
|
||||
"title": "Kaamerad"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -178,7 +328,8 @@
|
||||
"users": "Kasutajad",
|
||||
"roles": "Rollid",
|
||||
"notifications": "Teavitused",
|
||||
"frigateplus": "Frigate+"
|
||||
"frigateplus": "Frigate+",
|
||||
"cameraReview": "Ülevaatamine"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
@@ -189,5 +340,84 @@
|
||||
"cameraSetting": {
|
||||
"camera": "Kaamera",
|
||||
"noCamera": "Kaamerat pole"
|
||||
},
|
||||
"enrichments": {
|
||||
"semanticSearch": {
|
||||
"reindexNow": {
|
||||
"confirmButton": "Indekseeri uuesti",
|
||||
"label": "Indekseeri uuesti kohe"
|
||||
},
|
||||
"modelSize": {
|
||||
"small": {
|
||||
"title": "väike"
|
||||
},
|
||||
"large": {
|
||||
"title": "suur"
|
||||
}
|
||||
},
|
||||
"title": "Semantiline otsing"
|
||||
},
|
||||
"faceRecognition": {
|
||||
"modelSize": {
|
||||
"small": {
|
||||
"title": "väike"
|
||||
},
|
||||
"large": {
|
||||
"title": "suur"
|
||||
}
|
||||
}
|
||||
},
|
||||
"birdClassification": {
|
||||
"title": "Lindude klassifikatsioon"
|
||||
}
|
||||
},
|
||||
"cameraReview": {
|
||||
"review": {
|
||||
"title": "Ülevaatamine",
|
||||
"alerts": "Hoiatused ",
|
||||
"detections": "Tuvastamise tulemused "
|
||||
}
|
||||
},
|
||||
"motionDetectionTuner": {
|
||||
"Threshold": {
|
||||
"title": "Lävi"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"documentTitle": "Päästikud",
|
||||
"management": {
|
||||
"title": "Päästikud"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nimi",
|
||||
"type": "Tüüp",
|
||||
"content": "Sisu",
|
||||
"threshold": "Lävi",
|
||||
"actions": "Tegevused",
|
||||
"edit": "Muuda"
|
||||
},
|
||||
"type": {
|
||||
"thumbnail": "Pisipilt",
|
||||
"description": "Kirjeldus"
|
||||
},
|
||||
"dialog": {
|
||||
"form": {
|
||||
"name": {
|
||||
"title": "Nimi"
|
||||
},
|
||||
"type": {
|
||||
"title": "Tüüp"
|
||||
},
|
||||
"content": {
|
||||
"title": "Sisu"
|
||||
},
|
||||
"threshold": {
|
||||
"title": "Lävi"
|
||||
},
|
||||
"actions": {
|
||||
"title": "Tegevused"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,8 @@
|
||||
"bg": "Български (Bulgare)",
|
||||
"gl": "Galego (Galicien)",
|
||||
"id": "Bahasa Indonesia (Indonésien)",
|
||||
"ur": "اردو (Ourdou)"
|
||||
"ur": "اردو (Ourdou)",
|
||||
"hr": "Hrvatski (Croate)"
|
||||
},
|
||||
"appearance": "Apparence",
|
||||
"darkMode": {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"addFace": "Ajoutez une nouvelle collection à la bibliothèque de visages en téléversant votre première image.",
|
||||
"placeholder": "Saisissez un nom pour cette collection.",
|
||||
"invalidName": "Nom invalide. Les noms ne peuvent contenir que des lettres, des chiffres, des espaces, des apostrophes, des traits de soulignement et des tirets."
|
||||
"invalidName": "Nom invalide. Les noms ne peuvent contenir que des lettres, des chiffres, des espaces, des apostrophes, des traits de soulignement et des tirets.",
|
||||
"nameCannotContainHash": "Le nom ne peut pas contenir le caractère #."
|
||||
},
|
||||
"details": {
|
||||
"person": "Personne",
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
"hide": "Masquer le mot de passe",
|
||||
"requirements": {
|
||||
"title": "Critères du mot de passe :",
|
||||
"length": "Au moins 8 caractères",
|
||||
"length": "Au moins 12 caractères",
|
||||
"uppercase": "Au moins une lettre majuscule",
|
||||
"digit": "Au moins un chiffre",
|
||||
"special": "Au moins un caractère spécial (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -69,7 +69,8 @@
|
||||
},
|
||||
"inProgress": "処理中",
|
||||
"invalidStartTime": "開始時刻が無効です",
|
||||
"invalidEndTime": "終了時刻が無効です"
|
||||
"invalidEndTime": "終了時刻が無効です",
|
||||
"never": "なし"
|
||||
},
|
||||
"readTheDocumentation": "ドキュメントを見る",
|
||||
"unit": {
|
||||
@@ -232,7 +233,8 @@
|
||||
"ur": "اردو (ウルドゥー語)",
|
||||
"withSystem": {
|
||||
"label": "システム設定に従う"
|
||||
}
|
||||
},
|
||||
"hr": "Hrvatski (クロアチア語)"
|
||||
},
|
||||
"classification": "分類"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"placeholder": "このコレクションの名前を入力",
|
||||
"addFace": "最初の画像をアップロードして、フェイスライブラリに新しいコレクションを追加してください。",
|
||||
"invalidName": "無効な名前です。使用できるのは、英数字、空白、アポストロフィ、アンダースコア、ハイフンのみです。"
|
||||
"invalidName": "無効な名前です。使用できるのは、英数字、空白、アポストロフィ、アンダースコア、ハイフンのみです。",
|
||||
"nameCannotContainHash": "名前に # は使用できません。"
|
||||
},
|
||||
"details": {
|
||||
"person": "人物",
|
||||
|
||||
@@ -176,6 +176,16 @@
|
||||
"restricted": {
|
||||
"title": "利用可能なカメラがありません",
|
||||
"description": "このグループ内のカメラを表示する権限がありません。"
|
||||
},
|
||||
"default": {
|
||||
"title": "設定済みのカメラがありません",
|
||||
"description": "Frigate にカメラを接続して開始しましょう。",
|
||||
"buttonText": "カメラを追加"
|
||||
},
|
||||
"group": {
|
||||
"title": "このグループにカメラがありません",
|
||||
"description": "このカメラグループには、割り当て済みまたは有効なカメラがありません。",
|
||||
"buttonText": "グループを管理"
|
||||
}
|
||||
},
|
||||
"snapshot": {
|
||||
|
||||
@@ -86,7 +86,13 @@
|
||||
"otherProcesses": {
|
||||
"title": "その他のプロセス",
|
||||
"processCpuUsage": "プロセスの CPU 使用率",
|
||||
"processMemoryUsage": "プロセスのメモリ使用量"
|
||||
"processMemoryUsage": "プロセスのメモリ使用量",
|
||||
"series": {
|
||||
"recording": "録画",
|
||||
"review_segment": "レビューセグメント",
|
||||
"audio_detector": "音声検知",
|
||||
"go2rtc": "go2rtc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
|
||||
@@ -203,7 +203,8 @@
|
||||
"bg": "Български (Bulgarsk)",
|
||||
"gl": "Galego (Galisisk)",
|
||||
"id": "Bahasa Indonesia (Indonesisk)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Kroatisk)"
|
||||
},
|
||||
"appearance": "Utseende",
|
||||
"darkMode": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"description": {
|
||||
"addFace": "Legg til en ny samling i ansiktsbiblioteket ved å laste opp ditt første bilde.",
|
||||
"placeholder": "Skriv inn et navn for denne samlingen",
|
||||
"invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek."
|
||||
"invalidName": "Ugyldig navn. Navn kan kun inneholde bokstaver, tall, mellomrom, apostrof, understrek og bindestrek.",
|
||||
"nameCannotContainHash": "Navn kan ikke inneholde #."
|
||||
},
|
||||
"details": {
|
||||
"person": "Person",
|
||||
|
||||
@@ -537,7 +537,7 @@
|
||||
"hide": "Skjul passord",
|
||||
"requirements": {
|
||||
"title": "Passordkrav:",
|
||||
"length": "Minst 8 tegn",
|
||||
"length": "Minst 12 tegn",
|
||||
"uppercase": "Minst en stor bokstav",
|
||||
"digit": "Minst ett tall",
|
||||
"special": "Minst ett spesialtegn (!@#$%^&*(),.?\":{}|<>)"
|
||||
@@ -591,7 +591,7 @@
|
||||
"incorrectCurrentPassword": "Nåværende passord er feil",
|
||||
"passwordVerificationFailed": "Kunne ikke verifisere passord",
|
||||
"multiDeviceWarning": "Andre enheter du er logget inn på vil kreve ny innlogging innen {{refresh_time}}.",
|
||||
"multiDeviceAdmin": "Du kan også tvinge alle brukere til å logge inn på nytt umiddelbart ved å rotere JWT-hemmeligheten din."
|
||||
"multiDeviceAdmin": "Du kan også tvinge alle brukere til å logge inn på nytt ved å endre JWT (JSON Web Token)-nøkkelen."
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -206,7 +206,8 @@
|
||||
"bg": "Български (Bułgarski)",
|
||||
"gl": "Galego (Galicyjski)",
|
||||
"id": "Bahasa Indonesia (Indonezyjski)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Chorwacki)"
|
||||
},
|
||||
"appearance": "Wygląd",
|
||||
"darkMode": {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"description": {
|
||||
"addFace": "Dodaj nową kolekcję do biblioteki twarzy, przesyłając swoje pierwsze zdjęcie.",
|
||||
"placeholder": "Wprowadź nazwę tej kolekcji",
|
||||
"invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki."
|
||||
"invalidName": "Niepoprawna nazwa. Nazwy mogą zawierać tylko: litery, cyfry, spacje, cudzysłowy, podkreślniniki i myślniki.",
|
||||
"nameCannotContainHash": "Nazwa nie może zawierać #."
|
||||
},
|
||||
"details": {
|
||||
"person": "Osoba",
|
||||
|
||||
@@ -543,7 +543,7 @@
|
||||
"hide": "Ukryj hasło",
|
||||
"requirements": {
|
||||
"title": "Wymagania hasła:",
|
||||
"length": "Co najmniej 8 znaków",
|
||||
"length": "Co najmniej 12 znaków",
|
||||
"uppercase": "Co najmniej jedna duża litera",
|
||||
"digit": "Co najmniej jedna cyfra",
|
||||
"special": "Co najmniej jeden znak specjalny (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -135,7 +135,8 @@
|
||||
"bg": "Български (Bulgară)",
|
||||
"gl": "Galego (Galiciană)",
|
||||
"id": "Bahasa Indonesia (Indoneziană)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Croată)"
|
||||
},
|
||||
"theme": {
|
||||
"default": "Implicit",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"addFace": "Adaugă o colecție nouă în Biblioteca de fețe încărcând prima ta imagine.",
|
||||
"placeholder": "Introduceti un nume pentru aceasta colectie",
|
||||
"invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe."
|
||||
"invalidName": "Nume invalid. Numele pot include doar litere, cifre, spații, apostrofuri, underscore-uri și liniuțe.",
|
||||
"nameCannotContainHash": "Numele nu poate conține #."
|
||||
},
|
||||
"details": {
|
||||
"person": "Persoană",
|
||||
|
||||
@@ -508,7 +508,7 @@
|
||||
"hide": "Ascunde parola",
|
||||
"requirements": {
|
||||
"title": "Cerințe parolă:",
|
||||
"length": "Cel puțin 8 caracter",
|
||||
"length": "Cel puțin 12 caractere",
|
||||
"uppercase": "Cel puțin o literă majusculă",
|
||||
"digit": "Cel puțin o cifră",
|
||||
"special": "Cel puțin un caracter special (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -159,7 +159,8 @@
|
||||
"bg": "Български (Bulgariska)",
|
||||
"gl": "Galego (Galiciska)",
|
||||
"id": "Bahasa Indonesia (Indonesiska)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (kroatiska)"
|
||||
},
|
||||
"darkMode": {
|
||||
"withSystem": {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"description": {
|
||||
"placeholder": "Ange ett namn för denna samling",
|
||||
"addFace": "Lägg till en ny samling i ansiktsbiblioteket genom att ladda upp din första bild.",
|
||||
"invalidName": "Ogiltigt namn. Namn får endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck."
|
||||
"invalidName": "Ogiltigt namn. Namn får endast innehålla bokstäver, siffror, mellanslag, apostrofer, understreck och bindestreck.",
|
||||
"nameCannotContainHash": "Namn får inte innehålla #."
|
||||
},
|
||||
"documentTitle": "Ansiktsbibliotek - Frigate",
|
||||
"steps": {
|
||||
|
||||
@@ -540,7 +540,7 @@
|
||||
"hide": "Dölj lösenord",
|
||||
"requirements": {
|
||||
"title": "Lösenordskrav:",
|
||||
"length": "Minst 8 tecken",
|
||||
"length": "Minst 12 tecken",
|
||||
"uppercase": "Minst en stor bokstav",
|
||||
"digit": "Minst en siffra",
|
||||
"special": "Minst ett specialtecken (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -208,7 +208,8 @@
|
||||
"bg": "保加利亚语 (Български)",
|
||||
"gl": "加利西亚语 (Galego)",
|
||||
"id": "印度尼西亚语 (Bahasa Indonesia)",
|
||||
"ur": "乌尔都语 (اردو)"
|
||||
"ur": "乌尔都语 (اردو)",
|
||||
"hr": "克罗地亚语(Hrvatski)"
|
||||
},
|
||||
"appearance": "外观",
|
||||
"darkMode": {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"addFace": "我们将引导你如何向人脸库中添加新的合集。",
|
||||
"placeholder": "请输入此合集的名称",
|
||||
"invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。"
|
||||
"invalidName": "名称无效。名称只能包含字母、数字、空格、撇号、下划线和连字符。",
|
||||
"nameCannotContainHash": "名称中不允许包含“#”符号。"
|
||||
},
|
||||
"details": {
|
||||
"person": "人",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
},
|
||||
"delete": {
|
||||
"title": "确认删除",
|
||||
"desc": "你确定要删除{{type}} <em>{{name}}</em> 吗?",
|
||||
"desc": "你确定要删除{{type}} “<strong>{{name}}</strong>” 吗?",
|
||||
"success": "{{name}} 已被删除。"
|
||||
},
|
||||
"error": {
|
||||
@@ -543,7 +543,7 @@
|
||||
"hide": "隐藏密码",
|
||||
"requirements": {
|
||||
"title": "密码要求:",
|
||||
"length": "至少8个字符",
|
||||
"length": "至少需要 12 位字符",
|
||||
"uppercase": "至少一个大写字母",
|
||||
"digit": "至少一位数字",
|
||||
"special": "至少一个特殊符号 (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@@ -12,13 +12,15 @@ import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { VideoPreview } from "../preview/ScrubbablePreview";
|
||||
import { useApiHost } from "@/api";
|
||||
import { isDesktop, isSafari } from "react-device-detect";
|
||||
import { isSafari } from "react-device-detect";
|
||||
import { useUserPersistence } from "@/hooks/use-user-persistence";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaCircleCheck } from "react-icons/fa6";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { formatList } from "@/utils/stringUtil";
|
||||
|
||||
type AnimatedEventCardProps = {
|
||||
event: ReviewSegment;
|
||||
@@ -50,26 +52,37 @@ export function AnimatedEventCard({
|
||||
fetchPreviews: !currentHour,
|
||||
});
|
||||
|
||||
const getEventType = useCallback(
|
||||
(text: string) => {
|
||||
if (event.data.sub_labels?.includes(text)) return "manual";
|
||||
if (event.data.audio.includes(text)) return "audio";
|
||||
return "object";
|
||||
},
|
||||
[event],
|
||||
);
|
||||
|
||||
const tooltipText = useMemo(() => {
|
||||
if (event?.data?.metadata?.title) {
|
||||
return event.data.metadata.title;
|
||||
}
|
||||
|
||||
return (
|
||||
`${[
|
||||
...new Set([
|
||||
...(event.data.objects || []),
|
||||
...(event.data.sub_labels || []),
|
||||
...(event.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter((item) => item !== undefined && !item.includes("-verified"))
|
||||
.map((text) => text.charAt(0).toUpperCase() + text.substring(1))
|
||||
.sort()
|
||||
.join(", ")
|
||||
.replaceAll("-verified", "")} ` + t("detected")
|
||||
`${formatList(
|
||||
[
|
||||
...new Set([
|
||||
...(event.data.objects || []).map((text) =>
|
||||
text.replace("-verified", ""),
|
||||
),
|
||||
...(event.data.sub_labels || []),
|
||||
...(event.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter((item) => item !== undefined)
|
||||
.map((text) => getTranslatedLabel(text, getEventType(text)))
|
||||
.sort(),
|
||||
)} ` + t("detected")
|
||||
);
|
||||
}, [event, t]);
|
||||
}, [event, getEventType, t]);
|
||||
|
||||
// visibility
|
||||
|
||||
@@ -87,7 +100,6 @@ export function AnimatedEventCard({
|
||||
}, [visibilityListener]);
|
||||
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// interaction
|
||||
|
||||
@@ -134,31 +146,27 @@ export function AnimatedEventCard({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="relative h-24 flex-shrink-0 overflow-hidden rounded md:rounded-lg 4k:h-32"
|
||||
className="group relative h-24 flex-shrink-0 overflow-hidden rounded md:rounded-lg 4k:h-32"
|
||||
style={{
|
||||
aspectRatio: alertVideos ? aspectRatio : undefined,
|
||||
}}
|
||||
onMouseEnter={isDesktop ? () => setIsHovered(true) : undefined}
|
||||
onMouseLeave={isDesktop ? () => setIsHovered(false) : undefined}
|
||||
>
|
||||
{isHovered && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className="absolute left-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
|
||||
size="xs"
|
||||
aria-label={t("markAsReviewed")}
|
||||
onClick={async () => {
|
||||
await axios.post(`reviews/viewed`, { ids: [event.id] });
|
||||
updateEvents();
|
||||
}}
|
||||
>
|
||||
<FaCircleCheck className="size-3 text-white" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className="pointer-events-none absolute left-2 top-1 z-40 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100"
|
||||
size="xs"
|
||||
aria-label={t("markAsReviewed")}
|
||||
onClick={async () => {
|
||||
await axios.post(`reviews/viewed`, { ids: [event.id] });
|
||||
updateEvents();
|
||||
}}
|
||||
>
|
||||
<FaCircleCheck className="size-3 text-white" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("markAsReviewed")}</TooltipContent>
|
||||
</Tooltip>
|
||||
{previews != undefined && alertVideosLoaded && (
|
||||
<div
|
||||
className="size-full cursor-pointer"
|
||||
|
||||
@@ -33,13 +33,14 @@ import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { Button, buttonVariants } from "../ui/button";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LuCircle } from "react-icons/lu";
|
||||
import { MdAutoAwesome } from "react-icons/md";
|
||||
import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { formatList } from "@/utils/stringUtil";
|
||||
|
||||
type ReviewCardProps = {
|
||||
event: ReviewSegment;
|
||||
@@ -123,6 +124,12 @@ export default function ReviewCard({
|
||||
}
|
||||
}, [bypassDialogRef, onDelete]);
|
||||
|
||||
const getEventType = (text: string) => {
|
||||
if (event.data.sub_labels?.includes(text)) return "manual";
|
||||
if (event.data.audio.includes(text)) return "audio";
|
||||
return "object";
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className="relative flex w-full cursor-pointer flex-col gap-1.5"
|
||||
@@ -197,20 +204,20 @@ export default function ReviewCard({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="smart-capitalize">
|
||||
{[
|
||||
...new Set([
|
||||
...(event.data.objects || []),
|
||||
...(event.data.sub_labels || []),
|
||||
...(event.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter(
|
||||
(item) => item !== undefined && !item.includes("-verified"),
|
||||
)
|
||||
.map((text) => capitalizeFirstLetter(text))
|
||||
.sort()
|
||||
.join(", ")
|
||||
.replaceAll("-verified", "")}
|
||||
{formatList(
|
||||
[
|
||||
...new Set([
|
||||
...(event.data.objects || []).map((text) =>
|
||||
text.replace("-verified", ""),
|
||||
),
|
||||
...(event.data.sub_labels || []),
|
||||
...(event.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter((item) => item !== undefined)
|
||||
.map((text) => getTranslatedLabel(text, getEventType(text)))
|
||||
.sort(),
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TimeAgo
|
||||
|
||||
@@ -371,22 +371,23 @@ export default function LivePlayer({
|
||||
</TooltipTrigger>
|
||||
</div>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="smart-capitalize">
|
||||
<TooltipContent>
|
||||
{formatList(
|
||||
[
|
||||
...new Set([
|
||||
...(objects || []).map(({ label, sub_label }) =>
|
||||
label.endsWith("verified")
|
||||
? sub_label
|
||||
: label.replaceAll("_", " "),
|
||||
),
|
||||
]),
|
||||
]
|
||||
.filter((label) => label?.includes("-verified") == false)
|
||||
.map((label) =>
|
||||
getTranslatedLabel(label.replace("-verified", "")),
|
||||
)
|
||||
.sort(),
|
||||
...new Set(
|
||||
(objects || [])
|
||||
.map(({ label, sub_label }) => {
|
||||
const isManual = label.endsWith("verified");
|
||||
const text = isManual ? sub_label : label;
|
||||
const type = isManual ? "manual" : "object";
|
||||
return getTranslatedLabel(text, type);
|
||||
})
|
||||
.filter(
|
||||
(translated) =>
|
||||
translated && !translated.includes("-verified"),
|
||||
),
|
||||
),
|
||||
].sort(),
|
||||
)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { FaExclamationTriangle } from "react-icons/fa";
|
||||
import { MdOutlinePersonSearch } from "react-icons/md";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { formatList } from "@/utils/stringUtil";
|
||||
|
||||
type PreviewPlayerProps = {
|
||||
review: ReviewSegment;
|
||||
@@ -181,6 +182,12 @@ export default function PreviewThumbnailPlayer({
|
||||
config?.ui?.timezone,
|
||||
);
|
||||
|
||||
const getEventType = (text: string) => {
|
||||
if (review.data.sub_labels?.includes(text)) return "manual";
|
||||
if (review.data.audio.includes(text)) return "audio";
|
||||
return "object";
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative size-full cursor-pointer"
|
||||
@@ -261,13 +268,16 @@ export default function PreviewThumbnailPlayer({
|
||||
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} bg-gradient-to-br ${review.has_been_reviewed ? "bg-green-600 from-green-600 to-green-700" : "bg-gray-500 from-gray-400 to-gray-500"} z-0`}
|
||||
onClick={() => onClick(review, false, true)}
|
||||
>
|
||||
{review.data.objects.sort().map((object) => {
|
||||
return getIconForLabel(
|
||||
object,
|
||||
"object",
|
||||
"size-3 text-white",
|
||||
);
|
||||
})}
|
||||
{review.data.objects
|
||||
.sort()
|
||||
.map((object, idx) =>
|
||||
getIconForLabel(
|
||||
object,
|
||||
"object",
|
||||
"size-3 text-white",
|
||||
`${object}-${idx}`,
|
||||
),
|
||||
)}
|
||||
{review.data.audio.map((audio) => {
|
||||
return getIconForLabel(
|
||||
audio,
|
||||
@@ -281,23 +291,25 @@ export default function PreviewThumbnailPlayer({
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
</div>
|
||||
<TooltipContent className="smart-capitalize">
|
||||
<TooltipContent>
|
||||
{review.data.metadata
|
||||
? review.data.metadata.title
|
||||
: [
|
||||
...new Set([
|
||||
...(review.data.objects || []),
|
||||
...(review.data.sub_labels || []),
|
||||
...(review.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter(
|
||||
(item) =>
|
||||
item !== undefined && !item.includes("-verified"),
|
||||
)
|
||||
.map((text) => getTranslatedLabel(text))
|
||||
.sort()
|
||||
.join(", ")}
|
||||
: formatList(
|
||||
[
|
||||
...new Set([
|
||||
...(review.data.objects || []).map((text) =>
|
||||
text.replace("-verified", ""),
|
||||
),
|
||||
...(review.data.sub_labels || []),
|
||||
...(review.data.audio || []),
|
||||
]),
|
||||
]
|
||||
.filter((item) => item !== undefined)
|
||||
.map((text) =>
|
||||
getTranslatedLabel(text, getEventType(text)),
|
||||
)
|
||||
.sort(),
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{!!(
|
||||
|
||||
@@ -62,83 +62,86 @@ export function getIconForLabel(
|
||||
label: string,
|
||||
type: EventType = "object",
|
||||
className?: string,
|
||||
key?: string,
|
||||
) {
|
||||
const iconKey = key || label;
|
||||
|
||||
if (label.endsWith("-verified")) {
|
||||
return getVerifiedIcon(label, className, type);
|
||||
return getVerifiedIcon(label, className, type, iconKey);
|
||||
} else if (label.endsWith("-plate")) {
|
||||
return getRecognizedPlateIcon(label, className, type);
|
||||
return getRecognizedPlateIcon(label, className, type, iconKey);
|
||||
}
|
||||
|
||||
switch (label) {
|
||||
// objects
|
||||
case "bear":
|
||||
return <GiPolarBear key={label} className={className} />;
|
||||
return <GiPolarBear key={iconKey} className={className} />;
|
||||
case "bicycle":
|
||||
return <FaBicycle key={label} className={className} />;
|
||||
return <FaBicycle key={iconKey} className={className} />;
|
||||
case "bird":
|
||||
return <PiBirdFill key={label} className={className} />;
|
||||
return <PiBirdFill key={iconKey} className={className} />;
|
||||
case "boat":
|
||||
return <GiSailboat key={label} className={className} />;
|
||||
return <GiSailboat key={iconKey} className={className} />;
|
||||
case "bus":
|
||||
case "school_bus":
|
||||
return <FaBus key={label} className={className} />;
|
||||
return <FaBus key={iconKey} className={className} />;
|
||||
case "car":
|
||||
case "vehicle":
|
||||
return <FaCarSide key={label} className={className} />;
|
||||
return <FaCarSide key={iconKey} className={className} />;
|
||||
case "cat":
|
||||
return <FaCat key={label} className={className} />;
|
||||
return <FaCat key={iconKey} className={className} />;
|
||||
case "deer":
|
||||
return <GiDeer key={label} className={className} />;
|
||||
return <GiDeer key={iconKey} className={className} />;
|
||||
case "animal":
|
||||
case "bark":
|
||||
case "dog":
|
||||
return <FaDog key={label} className={className} />;
|
||||
return <FaDog key={iconKey} className={className} />;
|
||||
case "fox":
|
||||
return <GiFox key={label} className={className} />;
|
||||
return <GiFox key={iconKey} className={className} />;
|
||||
case "goat":
|
||||
return <GiGoat key={label} className={className} />;
|
||||
return <GiGoat key={iconKey} className={className} />;
|
||||
case "horse":
|
||||
return <FaHorse key={label} className={className} />;
|
||||
return <FaHorse key={iconKey} className={className} />;
|
||||
case "kangaroo":
|
||||
return <GiKangaroo key={label} className={className} />;
|
||||
return <GiKangaroo key={iconKey} className={className} />;
|
||||
case "license_plate":
|
||||
return <LuScanBarcode key={label} className={className} />;
|
||||
return <LuScanBarcode key={iconKey} className={className} />;
|
||||
case "motorcycle":
|
||||
return <FaMotorcycle key={label} className={className} />;
|
||||
return <FaMotorcycle key={iconKey} className={className} />;
|
||||
case "mouse":
|
||||
return <FaMouse key={label} className={className} />;
|
||||
return <FaMouse key={iconKey} className={className} />;
|
||||
case "package":
|
||||
return <LuBox key={label} className={className} />;
|
||||
return <LuBox key={iconKey} className={className} />;
|
||||
case "person":
|
||||
return <BsPersonWalking key={label} className={className} />;
|
||||
return <BsPersonWalking key={iconKey} className={className} />;
|
||||
case "rabbit":
|
||||
return <GiRabbit key={label} className={className} />;
|
||||
return <GiRabbit key={iconKey} className={className} />;
|
||||
case "raccoon":
|
||||
return <GiRaccoonHead key={label} className={className} />;
|
||||
return <GiRaccoonHead key={iconKey} className={className} />;
|
||||
case "robot_lawnmower":
|
||||
return <FaHockeyPuck key={label} className={className} />;
|
||||
return <FaHockeyPuck key={iconKey} className={className} />;
|
||||
case "sports_ball":
|
||||
return <FaFootballBall key={label} className={className} />;
|
||||
return <FaFootballBall key={iconKey} className={className} />;
|
||||
case "skunk":
|
||||
return <GiSquirrel key={label} className={className} />;
|
||||
return <GiSquirrel key={iconKey} className={className} />;
|
||||
case "squirrel":
|
||||
return <LuIcons.LuSquirrel key={label} className={className} />;
|
||||
return <LuIcons.LuSquirrel key={iconKey} className={className} />;
|
||||
case "umbrella":
|
||||
return <FaUmbrella key={label} className={className} />;
|
||||
return <FaUmbrella key={iconKey} className={className} />;
|
||||
case "waste_bin":
|
||||
return <FaRegTrashAlt key={label} className={className} />;
|
||||
return <FaRegTrashAlt key={iconKey} className={className} />;
|
||||
// audio
|
||||
case "crying":
|
||||
case "laughter":
|
||||
case "scream":
|
||||
case "speech":
|
||||
case "yell":
|
||||
return <MdRecordVoiceOver key={label} className={className} />;
|
||||
return <MdRecordVoiceOver key={iconKey} className={className} />;
|
||||
case "fire_alarm":
|
||||
return <FaFire key={label} className={className} />;
|
||||
return <FaFire key={iconKey} className={className} />;
|
||||
// sub labels
|
||||
case "amazon":
|
||||
return <FaAmazon key={label} className={className} />;
|
||||
return <FaAmazon key={iconKey} className={className} />;
|
||||
case "an_post":
|
||||
case "canada_post":
|
||||
case "dpd":
|
||||
@@ -148,20 +151,20 @@ export function getIconForLabel(
|
||||
case "postnord":
|
||||
case "purolator":
|
||||
case "royal_mail":
|
||||
return <GiPostStamp key={label} className={className} />;
|
||||
return <GiPostStamp key={iconKey} className={className} />;
|
||||
case "dhl":
|
||||
return <FaDhl key={label} className={className} />;
|
||||
return <FaDhl key={iconKey} className={className} />;
|
||||
case "fedex":
|
||||
return <FaFedex key={label} className={className} />;
|
||||
return <FaFedex key={iconKey} className={className} />;
|
||||
case "ups":
|
||||
return <FaUps key={label} className={className} />;
|
||||
return <FaUps key={iconKey} className={className} />;
|
||||
case "usps":
|
||||
return <FaUsps key={label} className={className} />;
|
||||
return <FaUsps key={iconKey} className={className} />;
|
||||
default:
|
||||
if (type === "audio") {
|
||||
return <GiSoundWaves key={label} className={className} />;
|
||||
return <GiSoundWaves key={iconKey} className={className} />;
|
||||
}
|
||||
return <LuLassoSelect key={label} className={className} />;
|
||||
return <LuLassoSelect key={iconKey} className={className} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +172,12 @@ function getVerifiedIcon(
|
||||
label: string,
|
||||
className?: string,
|
||||
type: EventType = "object",
|
||||
key?: string,
|
||||
) {
|
||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||
|
||||
return (
|
||||
<div key={label} className="relative flex items-center">
|
||||
<div key={key} className="relative flex items-center">
|
||||
{getIconForLabel(simpleLabel, type, className)}
|
||||
<FaCheckCircle className="absolute -bottom-0.5 -right-0.5 size-2" />
|
||||
</div>
|
||||
@@ -184,11 +188,12 @@ function getRecognizedPlateIcon(
|
||||
label: string,
|
||||
className?: string,
|
||||
type: EventType = "object",
|
||||
key?: string,
|
||||
) {
|
||||
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
|
||||
|
||||
return (
|
||||
<div key={label} className="relative inline-flex items-center">
|
||||
<div key={key} className="relative inline-flex items-center">
|
||||
{getIconForLabel(simpleLabel, type, className)}
|
||||
<LuScanBarcode className="absolute -bottom-0.5 -right-0.5 size-2" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user