mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-29 03:16:51 +03:00
279 lines
8.2 KiB
TypeScript
279 lines
8.2 KiB
TypeScript
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|
import type { FrigatePlusModel } from "@/components/config-form/theme/fields/ModelSourcePicker";
|
|
import { Button } from "@/components/ui/button";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
|
import type { FrigateConfig } from "@/types/frigateConfig";
|
|
import type { DetectionHardware } from "@/types/hardware";
|
|
import { recommendedDetectorCount } from "@/utils/detectionHardware";
|
|
import axios from "axios";
|
|
import { useCallback, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { LuExternalLink } from "react-icons/lu";
|
|
import { toast } from "sonner";
|
|
import useSWR from "swr";
|
|
|
|
// detectors that ship no default model; configuring them without one leaves
|
|
// the detector unable to start
|
|
const MODEL_REQUIRED_DETECTORS = ["onnx", "tensorrt"];
|
|
|
|
const CPU_FALLBACK: DetectionHardware[] = [
|
|
{
|
|
key: "cpu",
|
|
detector: "cpu",
|
|
name: "CPU",
|
|
units: [{ device: "cpu", label: "CPU" }],
|
|
count: 1,
|
|
unlimited: true,
|
|
},
|
|
];
|
|
|
|
type SetupDetectorProps = {
|
|
cameraCount: number;
|
|
onNext: (hardwareKey: string) => void;
|
|
onBack: () => void;
|
|
onSkip: (hardwareKey?: string) => void;
|
|
};
|
|
|
|
export default function SetupDetector({
|
|
cameraCount,
|
|
onNext,
|
|
onBack,
|
|
onSkip,
|
|
}: SetupDetectorProps) {
|
|
const { t } = useTranslation(["views/setup", "common"]);
|
|
const { getLocaleDocUrl } = useDocDomain();
|
|
|
|
const {
|
|
data: hardware,
|
|
isLoading,
|
|
error: probeError,
|
|
} = useSWR<DetectionHardware[]>("hardware/probe", {
|
|
revalidateOnFocus: false,
|
|
});
|
|
|
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
|
revalidateOnFocus: false,
|
|
});
|
|
const plusEnabled = Boolean(config?.plus?.enabled);
|
|
|
|
// the cpu is always probed, so an empty or failed probe leaves a fallback
|
|
const options = useMemo(
|
|
() => (hardware && hardware.length > 0 ? hardware : CPU_FALLBACK),
|
|
[hardware],
|
|
);
|
|
|
|
// the prober orders accelerators ahead of the cpu
|
|
const recommendedKey = options[0].key;
|
|
const [selectedKey, setSelectedKey] = useState<string>();
|
|
const selected =
|
|
options.find((entry) => entry.key === (selectedKey ?? recommendedKey)) ??
|
|
options[0];
|
|
|
|
const needsModel = MODEL_REQUIRED_DETECTORS.includes(selected.detector);
|
|
|
|
const { data: plusModels } = useSWR<FrigatePlusModel[]>(
|
|
plusEnabled && needsModel ? "/plus/models" : null,
|
|
{
|
|
fetcher: async (url) => {
|
|
const res = await axios.get(url, { withCredentials: true });
|
|
return res.data;
|
|
},
|
|
},
|
|
);
|
|
const [plusModelId, setPlusModelId] = useState("");
|
|
|
|
const compatiblePlusModels = useMemo(
|
|
() =>
|
|
(plusModels ?? []).filter((model) =>
|
|
model.supportedDetectors.includes(selected.detector),
|
|
),
|
|
[plusModels, selected.detector],
|
|
);
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const buildDevices = useCallback(
|
|
(entry: DetectionHardware): string[] => {
|
|
const first = entry.units[0]?.device;
|
|
|
|
if (!first) {
|
|
return [];
|
|
}
|
|
|
|
if (!entry.unlimited) {
|
|
return [first];
|
|
}
|
|
|
|
// repeating a device runs an extra inference process on it
|
|
const count = recommendedDetectorCount(Math.max(cameraCount, 1));
|
|
return Array.from({ length: count }, () => first);
|
|
},
|
|
[cameraCount],
|
|
);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
if (needsModel && !plusModelId) {
|
|
// no model to run on this hardware yet, so leave the config untouched
|
|
onSkip(selected.key);
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const model: Record<string, unknown> = {
|
|
devices: buildDevices(selected),
|
|
};
|
|
|
|
if (needsModel) {
|
|
model.path = `plus://${plusModelId}`;
|
|
}
|
|
|
|
await axios.put("config/set", {
|
|
config_data: {
|
|
models: [model],
|
|
detect: { enabled: true },
|
|
},
|
|
requires_restart: 1,
|
|
});
|
|
onNext(selected.key);
|
|
} catch {
|
|
toast.error(t("setupWizard.errors.saveFailed"));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [needsModel, plusModelId, selected, buildDevices, onNext, onSkip, t]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex flex-col items-center gap-4 py-12">
|
|
<ActivityIndicator />
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("setupWizard.detector.detecting")}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 py-4">
|
|
<div>
|
|
<h2 className="text-xl font-semibold">
|
|
{t("setupWizard.detector.title")}
|
|
</h2>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
{t("setupWizard.detector.description")}
|
|
</p>
|
|
</div>
|
|
|
|
{probeError && (
|
|
<p className="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
|
{t("setupWizard.detector.probeFailed")}
|
|
</p>
|
|
)}
|
|
|
|
<RadioGroup
|
|
value={selected.key}
|
|
onValueChange={(value) => {
|
|
setSelectedKey(value);
|
|
setPlusModelId("");
|
|
}}
|
|
>
|
|
{options.map((entry) => (
|
|
<div key={entry.key} className="flex items-center space-x-2">
|
|
<RadioGroupItem
|
|
value={entry.key}
|
|
id={`detector-${entry.key}`}
|
|
className={
|
|
selected.key === entry.key
|
|
? "bg-selected from-selected/50 to-selected/90 text-selected"
|
|
: "bg-secondary from-secondary/50 to-secondary/90 text-secondary"
|
|
}
|
|
/>
|
|
<label
|
|
htmlFor={`detector-${entry.key}`}
|
|
className="cursor-pointer text-sm font-medium"
|
|
>
|
|
{entry.name}
|
|
{entry.count > 1 ? ` (${entry.count})` : ""}
|
|
{entry.key === recommendedKey && entry.key !== "cpu" && (
|
|
<span className="ml-2 text-xs text-selected">
|
|
{t("setupWizard.detector.recommended")}
|
|
</span>
|
|
)}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</RadioGroup>
|
|
|
|
{needsModel && (
|
|
<div className="flex flex-col gap-3 rounded-md bg-muted p-3 text-sm">
|
|
<p>
|
|
{t("setupWizard.detector.modelRequired", { name: selected.name })}
|
|
</p>
|
|
{plusEnabled ? (
|
|
<Select value={plusModelId} onValueChange={setPlusModelId}>
|
|
<SelectTrigger className="max-w-xs">
|
|
<SelectValue
|
|
placeholder={t("setupWizard.detector.plusModelPlaceholder")}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{compatiblePlusModels.map((model) => (
|
|
<SelectItem key={model.id} value={model.id}>
|
|
{`${model.name} (${model.width}x${model.height})`}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<a
|
|
href={getLocaleDocUrl("configuration/object_detectors")}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center text-primary"
|
|
>
|
|
{t("readTheDocumentation", { ns: "common" })}
|
|
<LuExternalLink className="ml-2 size-3" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-3 pt-6 sm:flex-row sm:justify-end sm:gap-4">
|
|
<Button type="button" onClick={onBack}>
|
|
{t("setupWizard.actions.back")}
|
|
</Button>
|
|
<div className="flex flex-1 justify-end gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => onSkip(selected.key)}
|
|
>
|
|
{t("setupWizard.actions.skip")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="select"
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
>
|
|
{saving
|
|
? t("setupWizard.actions.saving")
|
|
: needsModel && !plusModelId
|
|
? t("setupWizard.detector.continueWithout")
|
|
: t("setupWizard.actions.next")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|