Compare commits

...

4 Commits

Author SHA1 Message Date
Josh Hawkins
b147b53522
add padding to dropdown text (#22420)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
2026-03-13 09:43:07 -05:00
leccelecce
d2b2faa2d7
Update dev contrib docs with Python checks (#22419) 2026-03-13 08:16:10 -06:00
Josh Hawkins
614a6b39d4
consistently sort class names (#22415)
keep None at the bottom
2026-03-13 08:05:56 -05:00
Nicolas Mowen
f29ee53fb4
Add handler for license plate which is not expected to be stationary (#22416) 2026-03-13 07:02:42 -06:00
5 changed files with 125 additions and 72 deletions

View File

@ -89,6 +89,14 @@ After closing VS Code, you may still have containers running. To close everythin
### Testing ### Testing
#### Unit Tests
GitHub will execute unit tests on new PRs. You must ensure that all tests pass.
```shell
python3 -u -m unittest
```
#### FFMPEG Hardware Acceleration #### FFMPEG Hardware Acceleration
The following commands are used inside the container to ensure hardware acceleration is working properly. The following commands are used inside the container to ensure hardware acceleration is working properly.
@ -125,6 +133,28 @@ ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
``` ```
### Submitting a pull request
Code must be formatted, linted and type-tested. GitHub will run these checks on pull requests, so it is advised to run them yourself prior to opening.
**Formatting**
```shell
ruff format frigate migrations docker *.py
```
**Linting**
```shell
ruff check frigate migrations docker *.py
```
**MyPy Static Typing**
```shell
python3 -u -m mypy --config-file frigate/mypy.ini frigate
```
## Web Interface ## Web Interface
### Prerequisites ### Prerequisites

View File

@ -55,6 +55,14 @@ DYNAMIC_OBJECT_THRESHOLDS = StationaryThresholds(
motion_classifier_enabled=True, motion_classifier_enabled=True,
) )
# Thresholds for objects that are not expected to be stationary
NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds(
objects=["license_plate"],
known_active_iou=0.9,
stationary_check_iou=0.9,
max_stationary_history=4,
)
def get_stationary_threshold(label: str) -> StationaryThresholds: def get_stationary_threshold(label: str) -> StationaryThresholds:
"""Get the stationary thresholds for a given object label.""" """Get the stationary thresholds for a given object label."""
@ -65,6 +73,9 @@ def get_stationary_threshold(label: str) -> StationaryThresholds:
if label in DYNAMIC_OBJECT_THRESHOLDS.objects: if label in DYNAMIC_OBJECT_THRESHOLDS.objects:
return DYNAMIC_OBJECT_THRESHOLDS return DYNAMIC_OBJECT_THRESHOLDS
if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects:
return NON_STATIONARY_OBJECT_THRESHOLDS
return StationaryThresholds() return StationaryThresholds()

View File

@ -125,17 +125,23 @@ export default function ClassificationSelectionDialog({
isMobile && "gap-2 pb-4", isMobile && "gap-2 pb-4",
)} )}
> >
{classes.sort().map((category) => ( {classes
<SelectorItem .sort((a, b) => {
key={category} if (a === "none") return 1;
className="flex cursor-pointer gap-2 smart-capitalize" if (b === "none") return -1;
onClick={() => onCategorizeImage(category)} return a.localeCompare(b);
> })
{category === "none" .map((category) => (
? t("details.none") <SelectorItem
: category.replaceAll("_", " ")} key={category}
</SelectorItem> className="flex cursor-pointer gap-2 smart-capitalize"
))} onClick={() => onCategorizeImage(category)}
>
{category === "none"
? t("details.none")
: category.replaceAll("_", " ")}
</SelectorItem>
))}
<Separator /> <Separator />
<SelectorItem <SelectorItem
className="flex cursor-pointer gap-2 smart-capitalize" className="flex cursor-pointer gap-2 smart-capitalize"

View File

@ -598,18 +598,18 @@ function LibrarySelector({
{Object.values(faces).map((face) => ( {Object.values(faces).map((face) => (
<DropdownMenuItem <DropdownMenuItem
key={face} key={face}
className="group flex items-center justify-between" className="group flex items-center justify-between p-0"
> >
<div <div
className="flex-grow cursor-pointer" className="flex-grow cursor-pointer"
onClick={() => setPageToggle(face)} onClick={() => setPageToggle(face)}
> >
{face} {face}
<span className="ml-2 text-muted-foreground"> <span className="ml-2 px-2 py-1.5 text-muted-foreground">
({faceData?.[face].length}) ({faceData?.[face].length})
</span> </span>
</div> </div>
<div className="flex gap-0.5"> <div className="flex gap-0.5 px-2">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button

View File

@ -700,66 +700,72 @@ function LibrarySelector({
</div> </div>
</> </>
)} )}
{Object.keys(dataset).map((id) => ( {Object.keys(dataset)
<DropdownMenuItem .sort((a, b) => {
key={id} if (a === "none") return 1;
className="group flex items-center justify-between" if (b === "none") return -1;
> return a.localeCompare(b);
<div })
className="flex-grow cursor-pointer capitalize" .map((id) => (
onClick={() => setPageToggle(id)} <DropdownMenuItem
key={id}
className="group flex items-center justify-between p-0"
> >
{id === "none" ? t("details.none") : id.replaceAll("_", " ")} <div
<span className="ml-2 text-muted-foreground"> className="flex-grow cursor-pointer px-2 py-1.5 capitalize"
({dataset?.[id].length}) onClick={() => setPageToggle(id)}
</span> >
</div> {id === "none" ? t("details.none") : id.replaceAll("_", " ")}
{id != "none" && ( <span className="ml-2 text-muted-foreground">
<div className="flex gap-0.5"> ({dataset?.[id].length})
<Tooltip> </span>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setRenameClass(id);
}}
>
<LuPencil className="size-4 text-primary" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.renameCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setConfirmDelete(id);
}}
>
<LuTrash2 className="size-4 text-destructive" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.deleteCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div> </div>
)} {id != "none" && (
</DropdownMenuItem> <div className="flex gap-0.5 px-2">
))} <Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setRenameClass(id);
}}
>
<LuPencil className="size-4 text-primary" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.renameCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setConfirmDelete(id);
}}
>
<LuTrash2 className="size-4 text-destructive" />
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{t("button.deleteCategory")}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</> </>