Miscellaneous fixes (0.18 beta) (#23934)
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s

* add host npu requirements to docs

* allow toggling live audio transcription via mqtt

* improve spacing consistency on mobile drawers

* fix clearing the region grid not surviving a restart
This commit is contained in:
Josh Hawkins
2026-08-08 07:20:09 -06:00
committed by GitHub
parent 8e55da67b0
commit 344efb6bc1
15 changed files with 103 additions and 26 deletions
+1 -1
View File
@@ -256,7 +256,7 @@ The only field that is valid at the camera level is `enabled`.
#### Live transcription #### Live transcription
The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate/<camera_name>/audio/transcription` will also be updated in real-time with transcribed text. The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing, or toggle it outside of the UI with the [`frigate/<camera_name>/audio_transcription/set`](/integrations/mqtt#frigatecamera_nameaudio_transcriptionset) MQTT topic or the HTTP API. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate/<camera_name>/audio/transcription` will also be updated in real-time with transcribed text.
Results can be error-prone due to a number of factors, including: Results can be error-prone due to a number of factors, including:
@@ -297,6 +297,14 @@ detectors:
::: :::
### Intel NPU host requirements {#intel-npu-requirements}
The NPU firmware is loaded by the host kernel and is not part of the Frigate image. Everything else the NPU needs is bundled in the container, so host NPU libraries should never be mounted in.
Frigate bundles a specific version of Intel's [linux-npu-driver](https://github.com/intel/linux-npu-driver/releases), and the host firmware must come from that release or a newer one. Firmware older than the bundled driver may fail with `MAPPED_INFERENCE_VERSION is NOT compatible with the ELF`, where `Expected` is the version the firmware supports and `received` is the version the bundled compiler produced. Distributions often package older firmware than the driver Frigate ships, so check the build date on the host with `sudo dmesg | grep -i vpu` and update it there if needed.
Intel NPUs cannot be used under Home Assistant OS, which does not include the NPU firmware.
### Configuration {#configuration-openvino} ### Configuration {#configuration-openvino}
<ModelConfigDropdown detectorTitle="OpenVINO" models={objectDetectorsModels.openvino.models} /> <ModelConfigDropdown detectorTitle="OpenVINO" models={objectDetectorsModels.openvino.models} />
+12
View File
@@ -390,6 +390,18 @@ Topic to turn audio detection for a camera on and off. Expected values are `ON`
Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`. Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/audio_transcription/set`
Topic to turn [live audio transcription](/configuration/audio_detectors#live-transcription) for a camera on and off. Expected values are `ON` and `OFF`. Transcribed text is published to `frigate/<camera_name>/audio/transcription`.
`ON` is ignored unless audio transcription is enabled in the config for the camera. Unlike the other camera toggles, this one is not persisted across Frigate restarts.
**NOTE:** Requires audio detection and transcription to be enabled
### `frigate/<camera_name>/audio_transcription/state`
Topic with current state of live audio transcription for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/recordings/set` ### `frigate/<camera_name>/recordings/set`
Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
+16 -1
View File
@@ -53,6 +53,7 @@ from frigate.util.file import (
) )
from frigate.util.image import get_image_from_recording, get_image_quality_params from frigate.util.image import get_image_from_recording, get_image_quality_params
from frigate.util.media import get_keyframe_before from frigate.util.media import get_keyframe_before
from frigate.util.object import create_empty_regions_grid
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -1083,7 +1084,21 @@ def clear_region_grid(request: Request, camera_name: str):
status_code=404, status_code=404,
) )
Regions.delete().where(Regions.camera == camera_name).execute() # store an empty grid instead of deleting the row so the grid is
# rebuilt from newly tracked objects and not from all past history
region = {
Regions.camera: camera_name,
Regions.grid: create_empty_regions_grid(),
Regions.last_update: datetime.now().timestamp(),
}
(
Regions.insert(region)
.on_conflict(
conflict_target=[Regions.camera],
update=region,
)
.execute()
)
return JSONResponse( return JSONResponse(
content={"success": True, "message": "Region grid cleared"}, content={"success": True, "message": "Region grid cleared"},
) )
+6
View File
@@ -77,6 +77,11 @@ class MqttClient(Communicator):
"ON" if camera.audio.enabled_in_config else "OFF", "ON" if camera.audio.enabled_in_config else "OFF",
retain=True, retain=True,
) )
self.publish(
f"{camera_name}/audio_transcription/state",
"ON" if camera.audio_transcription.live_enabled else "OFF",
retain=True,
)
self.publish( self.publish(
f"{camera_name}/detect/state", f"{camera_name}/detect/state",
"ON" if camera.detect.enabled else "OFF", "ON" if camera.detect.enabled else "OFF",
@@ -258,6 +263,7 @@ class MqttClient(Communicator):
"snapshots", "snapshots",
"detect", "detect",
"audio", "audio",
"audio_transcription",
"motion", "motion",
"improve_contrast", "improve_contrast",
"ptz_autotracker", "ptz_autotracker",
+6 -6
View File
@@ -35,6 +35,11 @@ logger = logging.getLogger(__name__)
GRID_SIZE = 8 GRID_SIZE = 8
def create_empty_regions_grid() -> list[list[dict[str, Any]]]:
"""Create a region grid with no learned sizes."""
return [[{"sizes": []} for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
def get_camera_regions_grid( def get_camera_regions_grid(
name: str, name: str,
detect: DetectConfig, detect: DetectConfig,
@@ -47,12 +52,7 @@ def get_camera_regions_grid(
grid = regions.grid grid = regions.grid
last_update = regions.last_update last_update = regions.last_update
except DoesNotExist: except DoesNotExist:
grid = [] grid = create_empty_regions_grid()
for x in range(GRID_SIZE):
row = []
for y in range(GRID_SIZE):
row.append({"sizes": []})
grid.append(row)
last_update = 0 last_update = 0
# get events for timeline entries # get events for timeline entries
@@ -37,7 +37,7 @@ export function LogSettingsButton({
</Button> </Button>
); );
const content = ( const content = (
<div className={cn("my-3 space-y-3 py-3 md:mt-0 md:py-0")}> <div className={cn("my-3 space-y-3 px-3 py-3 md:mt-0 md:px-0 md:py-0")}>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<div>{t("filter")}</div> <div>{t("filter")}</div>
@@ -77,7 +77,7 @@ export function LogSettingsButton({
return ( return (
<Drawer> <Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger> <DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden p-3"> <DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden">
{content} {content}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@@ -26,6 +26,7 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n"; import { getTranslatedLabel } from "@/utils/i18n";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { cn } from "@/lib/utils";
const REVIEW_FILTERS = [ const REVIEW_FILTERS = [
"cameras", "cameras",
@@ -409,6 +410,7 @@ function GeneralFilterButton({
onUpdateFilter(resetFilter); onUpdateFilter(resetFilter);
}} }}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
contentClassName="p-4"
/> />
); );
@@ -416,6 +418,7 @@ function GeneralFilterButton({
<PlatformAwareDialog <PlatformAwareDialog
trigger={trigger} trigger={trigger}
content={content} content={content}
contentClassName="p-1"
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
@@ -444,6 +447,7 @@ type GeneralFilterContentProps = {
onApply: () => void; onApply: () => void;
onReset: () => void; onReset: () => void;
onClose: () => void; onClose: () => void;
contentClassName?: string;
}; };
export function GeneralFilterContent({ export function GeneralFilterContent({
allLabels, allLabels,
@@ -454,6 +458,7 @@ export function GeneralFilterContent({
onApply, onApply,
onReset, onReset,
onClose, onClose,
contentClassName,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter", "views/events"]); const { t } = useTranslation(["components/filter", "views/events"]);
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
@@ -476,7 +481,12 @@ export function GeneralFilterContent({
}, [config]); }, [config]);
return ( return (
<> <>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden"> <div
className={cn(
"scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden",
contentClassName,
)}
>
{currentSeverity && ( {currentSeverity && (
<div className="my-2.5 flex flex-col gap-2.5"> <div className="my-2.5 flex flex-col gap-2.5">
<FilterSwitch <FilterSwitch
+15 -3
View File
@@ -8,6 +8,7 @@ import { Label } from "../ui/label";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
import { DropdownMenuSeparator } from "../ui/dropdown-menu"; import { DropdownMenuSeparator } from "../ui/dropdown-menu";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
type ZoneMaskFilterButtonProps = { type ZoneMaskFilterButtonProps = {
selectedZoneMask?: PolygonType[]; selectedZoneMask?: PolygonType[];
@@ -46,8 +47,12 @@ export function ZoneMaskFilterButton({
return ( return (
<Drawer> <Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger> <DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden p-3"> <DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden">
{content} <GeneralFilterContent
selectedZoneMask={selectedZoneMask}
updateZoneMaskFilter={updateZoneMaskFilter}
contentClassName="p-3"
/>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );
@@ -64,15 +69,22 @@ export function ZoneMaskFilterButton({
type GeneralFilterContentProps = { type GeneralFilterContentProps = {
selectedZoneMask: PolygonType[] | undefined; selectedZoneMask: PolygonType[] | undefined;
updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void; updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void;
contentClassName?: string;
}; };
export function GeneralFilterContent({ export function GeneralFilterContent({
selectedZoneMask, selectedZoneMask,
updateZoneMaskFilter, updateZoneMaskFilter,
contentClassName,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
const { t } = useTranslation(["components/filter"]); const { t } = useTranslation(["components/filter"]);
return ( return (
<> <>
<div className="h-auto overflow-y-auto overflow-x-hidden"> <div
className={cn(
"h-auto overflow-y-auto overflow-x-hidden",
contentClassName,
)}
>
<div className="my-2.5 flex items-center justify-between"> <div className="my-2.5 flex items-center justify-between">
<Label <Label
className="mx-2 cursor-pointer text-primary" className="mx-2 cursor-pointer text-primary"
+7 -2
View File
@@ -257,7 +257,7 @@ export default function GeneralSettings({
className={ className={
isDesktop isDesktop
? "scrollbar-container mr-5 w-72 overflow-y-auto" ? "scrollbar-container mr-5 w-72 overflow-y-auto"
: "max-h-[75dvh] overflow-hidden p-2" : "max-h-[75dvh] overflow-hidden"
} }
> >
{!isDesktop && ( {!isDesktop && (
@@ -270,7 +270,12 @@ export default function GeneralSettings({
</DrawerDescription> </DrawerDescription>
</> </>
)} )}
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden"> <div
className={cn(
"scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden",
!isDesktop && "p-2",
)}
>
{isMobile && ( {isMobile && (
<div className="mb-2"> <div className="mb-2">
<DropdownMenuLabel> <DropdownMenuLabel>
+2 -2
View File
@@ -174,11 +174,11 @@ function StatusAlertNav({ className, large }: StatusAlertNavProps) {
</DrawerTrigger> </DrawerTrigger>
<DrawerContent <DrawerContent
className={cn( className={cn(
"mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-2", "mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl",
className, className,
)} )}
> >
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden py-4"> <div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden px-2 py-4">
{Object.entries(messages).map(([key, messageArray]) => ( {Object.entries(messages).map(([key, messageArray]) => (
<div key={key} className="flex w-full items-center gap-2"> <div key={key} className="flex w-full items-center gap-2">
{messageArray.map(({ id, text, color, link }: StatusMessage) => { {messageArray.map(({ id, text, color, link }: StatusMessage) => {
@@ -34,8 +34,8 @@ export default function MobileCameraDrawer({
<FaVideo className="text-secondary-foreground" /> <FaVideo className="text-secondary-foreground" />
</Button> </Button>
</DrawerTrigger> </DrawerTrigger>
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-4"> <DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl">
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden py-4"> <div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden p-4">
{allCameras.map((cam) => ( {allCameras.map((cam) => (
<div <div
key={cam} key={cam}
@@ -30,6 +30,7 @@ import { useNavigate } from "react-router-dom";
import { StartExportResponse } from "@/types/export"; import { StartExportResponse } from "@/types/export";
import { ShareTimestampContent } from "./ShareTimestampDialog"; import { ShareTimestampContent } from "./ShareTimestampDialog";
import { useIsAdmin } from "@/hooks/use-is-admin"; import { useIsAdmin } from "@/hooks/use-is-admin";
import { cn } from "@/lib/utils";
type DrawerMode = type DrawerMode =
| "none" | "none"
@@ -518,9 +519,9 @@ export default function MobileReviewSettingsDrawer({
} else if (drawerMode == "filter") { } else if (drawerMode == "filter") {
content = ( content = (
<div className="scrollbar-container flex h-auto w-full flex-col overflow-y-auto overflow-x-hidden"> <div className="scrollbar-container flex h-auto w-full flex-col overflow-y-auto overflow-x-hidden">
<div className="relative mb-2 h-8 w-full"> <div className="relative mb-4 h-8 w-full">
<div <div
className="absolute left-0 text-selected" className="absolute left-4 text-selected"
onClick={() => setDrawerMode("select")} onClick={() => setDrawerMode("select")}
> >
{t("button.back", { ns: "common" })} {t("button.back", { ns: "common" })}
@@ -548,6 +549,7 @@ export default function MobileReviewSettingsDrawer({
onUpdateFilter(resetFilter); onUpdateFilter(resetFilter);
}} }}
onClose={() => setDrawerMode("select")} onClose={() => setDrawerMode("select")}
contentClassName="px-4"
/> />
</div> </div>
); );
@@ -685,7 +687,14 @@ export default function MobileReviewSettingsDrawer({
</Button> </Button>
</DrawerTrigger> </DrawerTrigger>
<DrawerContent <DrawerContent
className={`mx-1 flex max-h-[80dvh] flex-col items-center gap-2 rounded-t-2xl px-4 pb-4 ${drawerMode == "export" || drawerMode == "debug-replay" ? "overflow-visible" : "overflow-hidden"}`} className={cn(
"mx-1 flex max-h-[80dvh] flex-col items-center gap-2 rounded-t-2xl pb-4",
// the filter content pads itself so its scrollbar reaches the drawer edge
drawerMode != "filter" && "px-4",
drawerMode == "export" || drawerMode == "debug-replay"
? "overflow-visible"
: "overflow-hidden",
)}
> >
{content} {content}
</DrawerContent> </DrawerContent>
@@ -491,7 +491,7 @@ export default function Step3StreamConfig({
</Button> </Button>
</div> </div>
</DrawerTrigger> </DrawerTrigger>
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-2"> <DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl">
<div className="mt-2"> <div className="mt-2">
<Command> <Command>
<CommandInput <CommandInput
@@ -500,7 +500,7 @@ export default function Step3StreamConfig({
)} )}
className="h-9" className="h-9"
/> />
<CommandList> <CommandList className="px-2">
<CommandEmpty> <CommandEmpty>
{t("cameraWizard.step3.noStreamFound")} {t("cameraWizard.step3.noStreamFound")}
</CommandEmpty> </CommandEmpty>
+1 -1
View File
@@ -47,7 +47,7 @@ const DrawerContent = React.forwardRef<
)} )}
{...props} {...props}
> >
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> <div className="mx-auto mt-4 h-2 w-[100px] shrink-0 rounded-full bg-muted" />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>