Compare commits

..

No commits in common. "e95e9b52f3351709d2eee54e72f6531f6238afd9" and "d8c35d5a0f7553dc31a552f286a7ea11c232059f" have entirely different histories.

8 changed files with 15 additions and 62 deletions

View File

@ -16,7 +16,7 @@ jobs:
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
script: | script: |
const maintainers = ['blakeblackshear', 'NickM-27', 'hawkeye217', 'dependabot[bot]', 'weblate']; const maintainers = ['blakeblackshear', 'NickM-27', 'hawkeye217', 'dependabot[bot]'];
const author = context.payload.pull_request.user.login; const author = context.payload.pull_request.user.login;
if (maintainers.includes(author)) { if (maintainers.includes(author)) {

View File

@ -20,7 +20,7 @@ When a profile is activated, Frigate merges each camera's profile overrides on t
:::info :::info
Profile changes are applied in-memory and take effect immediately — no restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.profiles` file). Profile changes are applied in-memory and take effect immediately — no restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.active_profile` file).
::: :::
@ -130,14 +130,14 @@ Profiles can be activated and deactivated from the Frigate UI. Open the Settings
## Example: Home / Away Setup ## Example: Home / Away Setup
A common use case is having different detection and notification settings based on whether you are home or away. This example below is for a system with two cameras, `front_door` and `indoor_cam`. A common use case is having different detection and notification settings based on whether you are home or away.
<ConfigTabs> <ConfigTabs>
<TabItem value="ui"> <TabItem value="ui">
1. Navigate to <NavPath path="Settings > Camera configuration > Profiles" /> and create two profiles: **Home** and **Away**. 1. Navigate to <NavPath path="Settings > Camera configuration > Profiles" /> and create two profiles: **Home** and **Away**.
2. From to the Camera configuration section in Settings, choose the **front_door** camera, and select the **Away** profile from the profile dropdown. Then, enable notifications from the Notifications pane, and set alert labels to `person` and `car` from the Review pane. Then, from the profile dropdown choose **Home** profile, then navigate to Notifications to disable notifications. 2. For the **front_door** camera, configure the **Away** profile to enable notifications and set alert labels to `person` and `car`. Configure the **Home** profile to disable notifications.
3. For the **indoor_cam** camera, perform similar steps - configure the **Away** profile to enable the camera, detection, and recording. Configure the **Home** profile to disable the camera entirely for privacy. 3. For the **indoor_cam** camera, configure the **Away** profile to enable the camera, detection, and recording. Configure the **Home** profile to disable the camera entirely for privacy.
4. Activate the desired profile from <NavPath path="Settings > Camera configuration > Profiles" /> or from the **Profiles** option in Frigate's main menu. 4. Activate the desired profile from <NavPath path="Settings > Camera configuration > Profiles" /> or from the **Profiles** option in Frigate's main menu.
</TabItem> </TabItem>

View File

@ -199,9 +199,6 @@ Each line represents a detection state, not necessarily unique individuals. The
) )
return None return None
else: else:
logger.debug(
f"Invalid response received from GenAI provider for review description on {review_data['camera']}. Response: {response}",
)
return None return None
def generate_review_summary( def generate_review_summary(

View File

@ -238,15 +238,10 @@ class LlamaCppClient(GenAIClient):
def list_models(self) -> list[str]: def list_models(self) -> list[str]:
"""Return available model IDs from the llama.cpp server.""" """Return available model IDs from the llama.cpp server."""
base_url = self.provider or ( if self.provider is None:
self.genai_config.base_url.rstrip("/")
if self.genai_config.base_url
else None
)
if base_url is None:
return [] return []
try: try:
response = requests.get(f"{base_url}/v1/models", timeout=10) response = requests.get(f"{self.provider}/v1/models", timeout=10)
response.raise_for_status() response.raise_for_status()
models = [] models = []
for m in response.json().get("data", []): for m in response.json().get("data", []):

View File

@ -134,20 +134,10 @@ class OllamaClient(GenAIClient):
def list_models(self) -> list[str]: def list_models(self) -> list[str]:
"""Return available model names from the Ollama server.""" """Return available model names from the Ollama server."""
client = self.provider if self.provider is None:
if client is None: return []
# Provider init may have failed due to invalid model, but we can
# still list available models with a fresh client.
if not self.genai_config.base_url:
return []
try:
client = ApiClient(
host=self.genai_config.base_url, timeout=self.timeout
)
except Exception:
return []
try: try:
response = client.list() response = self.provider.list()
return sorted( return sorted(
m.get("name", m.get("model", "")) for m in response.get("models", []) m.get("name", m.get("model", "")) for m in response.get("models", [])
) )

View File

@ -275,7 +275,7 @@ export default function ReviewCard({
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<ContextMenu key={event.id} modal={false}> <ContextMenu key={event.id}>
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger> <ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem> <ContextMenuItem>

View File

@ -1,6 +1,6 @@
// Combobox widget for genai *.model fields. // Combobox widget for genai *.model fields.
// Fetches available models from the provider's backend and shows them in a dropdown. // Fetches available models from the provider's backend and shows them in a dropdown.
import { useState, useMemo, useEffect, useRef } from "react"; import { useState, useMemo } from "react";
import type { WidgetProps } from "@rjsf/utils"; import type { WidgetProps } from "@rjsf/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useSWR from "swr"; import useSWR from "swr";
@ -19,7 +19,6 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import type { ConfigFormContext } from "@/types/configForm";
import { getSizedFieldClassName } from "../utils"; import { getSizedFieldClassName } from "../utils";
/** /**
@ -38,45 +37,17 @@ function getProviderKey(widgetId: string): string | undefined {
} }
export function GenAIModelWidget(props: WidgetProps) { export function GenAIModelWidget(props: WidgetProps) {
const { id, value, disabled, readonly, onChange, options, registry } = props; const { id, value, disabled, readonly, onChange, options } = props;
const { t } = useTranslation(["views/settings"]); const { t } = useTranslation(["views/settings"]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const fieldClassName = getSizedFieldClassName(options, "sm"); const fieldClassName = getSizedFieldClassName(options, "sm");
const providerKey = useMemo(() => getProviderKey(id), [id]); const providerKey = useMemo(() => getProviderKey(id), [id]);
const formContext = registry?.formContext as ConfigFormContext | undefined; const { data: allModels } = useSWR<Record<string, string[]>>("genai/models", {
// Build a fingerprint from the saved config's provider + base_url so the
// SWR key changes (and models are refetched) whenever those fields are saved.
const configFingerprint = useMemo(() => {
if (!providerKey) return "";
const genai = (
formContext?.fullConfig as Record<string, unknown> | undefined
)?.genai;
if (!genai || typeof genai !== "object" || Array.isArray(genai)) return "";
const entry = (genai as Record<string, unknown>)[providerKey];
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return "";
const e = entry as Record<string, unknown>;
return `${e.provider ?? ""}|${e.base_url ?? ""}`;
}, [providerKey, formContext?.fullConfig]);
const { data: allModels, mutate: mutateModels } = useSWR<
Record<string, string[]>
>("genai/models", {
revalidateOnFocus: false, revalidateOnFocus: false,
}); });
// Revalidate models when the saved config fingerprint changes (e.g. after
// switching provider or base_url and saving).
const prevFingerprint = useRef(configFingerprint);
useEffect(() => {
if (configFingerprint !== prevFingerprint.current) {
prevFingerprint.current = configFingerprint;
mutateModels();
}
}, [configFingerprint, mutateModels]);
const models = useMemo(() => { const models = useMemo(() => {
if (!allModels || !providerKey) return []; if (!allModels || !providerKey) return [];
return allModels[providerKey] ?? []; return allModels[providerKey] ?? [];

View File

@ -272,7 +272,7 @@ export default function LiveContextMenu({
return ( return (
<div className={cn("w-full", className)}> <div className={cn("w-full", className)}>
<ContextMenu key={camera} modal={false} onOpenChange={handleOpenChange}> <ContextMenu key={camera} onOpenChange={handleOpenChange}>
<ContextMenuTrigger>{children}</ContextMenuTrigger> <ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<div className="flex flex-col items-start gap-1 py-1 pl-2"> <div className="flex flex-col items-start gap-1 py-1 pl-2">