Use identifier field for unknown license plates (#17123)

* backend

* backend fixes

* api for search queries

* frontend

* docs

* add filterable scroll list to more filters pane for identifiers

* always publish identifier
This commit is contained in:
Josh Hawkins
2025-03-12 14:38:28 -06:00
committed by GitHub
parent 9e70bddc9d
commit 6360802612
16 changed files with 332 additions and 15 deletions
+33
View File
@@ -619,6 +619,39 @@ def get_sub_labels(split_joined: Optional[int] = None):
return JSONResponse(content=sub_labels)
@router.get("/identifiers")
def get_identifiers(split_joined: Optional[int] = None):
try:
events = Event.select(Event.data).distinct()
except Exception:
return JSONResponse(
content=({"success": False, "message": "Failed to get identifiers"}),
status_code=404,
)
identifiers = []
for e in events:
if e.data is not None and "identifier" in e.data:
identifiers.append(e.data["identifier"])
while None in identifiers:
identifiers.remove(None)
if split_joined:
original_identifiers = identifiers.copy()
for identifier in original_identifiers:
if identifier and "," in identifier:
identifiers.remove(identifier)
parts = identifier.split(",")
for part in parts:
if part.strip() not in identifiers:
identifiers.append(part.strip())
identifiers = list(set(identifiers))
identifiers.sort()
return JSONResponse(content=identifiers)
@router.get("/timeline")
def timeline(camera: str = "all", limit: int = 100, source_id: Optional[str] = None):
clauses = []
@@ -27,6 +27,7 @@ class EventsQueryParams(BaseModel):
max_score: Optional[float] = None
min_speed: Optional[float] = None
max_speed: Optional[float] = None
identifier: Optional[str] = "all"
is_submitted: Optional[int] = None
min_length: Optional[float] = None
max_length: Optional[float] = None
@@ -55,6 +56,7 @@ class EventsSearchQueryParams(BaseModel):
max_score: Optional[float] = None
min_speed: Optional[float] = None
max_speed: Optional[float] = None
identifier: Optional[str] = "all"
sort: Optional[str] = None
+60
View File
@@ -101,6 +101,7 @@ def events(params: EventsQueryParams = Depends()):
min_length = params.min_length
max_length = params.max_length
event_id = params.event_id
identifier = params.identifier
sort = params.sort
@@ -158,6 +159,32 @@ def events(params: EventsQueryParams = Depends()):
sub_label_clause = reduce(operator.or_, sub_label_clauses)
clauses.append((sub_label_clause))
if identifier != "all":
# use matching so joined identifiers are included
# for example an identifier 'ABC123' would get events
# with identifiers 'ABC123' and 'ABC123, XYZ789'
identifier_clauses = []
filtered_identifiers = identifier.split(",")
if "None" in filtered_identifiers:
filtered_identifiers.remove("None")
identifier_clauses.append((Event.data["identifier"].is_null()))
for identifier in filtered_identifiers:
# Exact matching plus list inclusion
identifier_clauses.append(
(Event.data["identifier"].cast("text") == identifier)
)
identifier_clauses.append(
(Event.data["identifier"].cast("text") % f"*{identifier},*")
)
identifier_clauses.append(
(Event.data["identifier"].cast("text") % f"*, {identifier}*")
)
identifier_clause = reduce(operator.or_, identifier_clauses)
clauses.append((identifier_clause))
if zones != "all":
# use matching so events with multiple zones
# still match on a search where any zone matches
@@ -340,6 +367,8 @@ def events_explore(limit: int = 10):
"average_estimated_speed",
"velocity_angle",
"path_data",
"identifier",
"identifier_score",
]
},
"event_count": label_counts[event.label],
@@ -397,6 +426,7 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
has_clip = params.has_clip
has_snapshot = params.has_snapshot
is_submitted = params.is_submitted
identifier = params.identifier
# for similarity search
event_id = params.event_id
@@ -466,6 +496,32 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
event_filters.append((reduce(operator.or_, zone_clauses)))
if identifier != "all":
# use matching so joined identifiers are included
# for example an identifier 'ABC123' would get events
# with identifiers 'ABC123' and 'ABC123, XYZ789'
identifier_clauses = []
filtered_identifiers = identifier.split(",")
if "None" in filtered_identifiers:
filtered_identifiers.remove("None")
identifier_clauses.append((Event.data["identifier"].is_null()))
for identifier in filtered_identifiers:
# Exact matching plus list inclusion
identifier_clauses.append(
(Event.data["identifier"].cast("text") == identifier)
)
identifier_clauses.append(
(Event.data["identifier"].cast("text") % f"*{identifier},*")
)
identifier_clauses.append(
(Event.data["identifier"].cast("text") % f"*, {identifier}*")
)
identifier_clause = reduce(operator.or_, identifier_clauses)
event_filters.append((identifier_clause))
if after:
event_filters.append((Event.start_time > after))
@@ -627,6 +683,8 @@ def events_search(request: Request, params: EventsSearchQueryParams = Depends())
"average_estimated_speed",
"velocity_angle",
"path_data",
"identifier",
"identifier_score",
]
}
@@ -681,6 +739,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
Event.camera,
Event.label,
Event.sub_label,
Event.data,
fn.strftime(
"%Y-%m-%d",
fn.datetime(
@@ -695,6 +754,7 @@ def events_summary(params: EventsSummaryQueryParams = Depends()):
Event.camera,
Event.label,
Event.sub_label,
Event.data,
(Event.start_time + seconds_offset).cast("int") / (3600 * 24),
Event.zones,
)