mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-03 06:50:58 +00:00
Compare commits
10 Commits
1ffba7caa8
...
0dbf648b3d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dbf648b3d | ||
|
|
01d52a8bf6 | ||
|
|
91965dfa4c | ||
|
|
16237c69ad | ||
|
|
c487d0aa93 | ||
|
|
ab3c12b89e | ||
|
|
5272b83959 | ||
|
|
afe7fbad14 | ||
|
|
05973f658a | ||
|
|
ff90dbf208 |
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
A complete and local NVR designed for [Home Assistant](https://www.home-assistant.io) with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras.
|
A complete and local NVR designed for [Home Assistant](https://www.home-assistant.io) with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras.
|
||||||
|
|
||||||
Use of a GPU or AI accelerator is highly recommended. AI accelerators will outperform even the best CPUs with very little overhead. See Frigate's supported [object detectors](https://docs.frigate.video/configuration/object_detectors/).
|
Use of a GPU or AI accelerator such as a [Google Coral](https://coral.ai/products/) or [Hailo](https://hailo.ai/) is highly recommended. AI accelerators will outperform even the best CPUs with very little overhead.
|
||||||
|
|
||||||
- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration)
|
- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration)
|
||||||
- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
|
- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
|
||||||
|
|||||||
@ -320,12 +320,6 @@ http {
|
|||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
}
|
}
|
||||||
|
|
||||||
location /fonts/ {
|
|
||||||
access_log off;
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
location /locales/ {
|
location /locales/ {
|
||||||
access_log off;
|
access_log off;
|
||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
|
|||||||
@ -70,7 +70,7 @@ You should have at least 8 GB of RAM available (or VRAM if running on GPU) to ru
|
|||||||
genai:
|
genai:
|
||||||
provider: ollama
|
provider: ollama
|
||||||
base_url: http://localhost:11434
|
base_url: http://localhost:11434
|
||||||
model: qwen3-vl:4b
|
model: llava:7b
|
||||||
```
|
```
|
||||||
|
|
||||||
## Google Gemini
|
## Google Gemini
|
||||||
|
|||||||
@ -35,18 +35,19 @@ Each model is available in multiple parameter sizes (3b, 4b, 8b, etc.). Larger s
|
|||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
If you are trying to use a single model for Frigate and HomeAssistant, it will need to support vision and tools calling. qwen3-VL supports vision and tools simultaneously in Ollama.
|
If you are trying to use a single model for Frigate and HomeAssistant, it will need to support vision and tools calling. https://github.com/skye-harris/ollama-modelfiles contains optimized model configs for this task.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
The following models are recommended:
|
The following models are recommended:
|
||||||
|
|
||||||
| Model | Notes |
|
| Model | Notes |
|
||||||
| ----------------- | -------------------------------------------------------------------- |
|
| ----------------- | ----------------------------------------------------------- |
|
||||||
| `qwen3-vl` | Strong visual and situational understanding, higher vram requirement |
|
| `qwen3-vl` | Strong visual and situational understanding |
|
||||||
| `Intern3.5VL` | Relatively fast with good vision comprehension |
|
| `Intern3.5VL` | Relatively fast with good vision comprehension |
|
||||||
| `gemma3` | Strong frame-to-frame understanding, slower inference times |
|
| `gemma3` | Strong frame-to-frame understanding, slower inference times |
|
||||||
| `qwen2.5-vl` | Fast but capable model with good vision comprehension |
|
| `qwen2.5-vl` | Fast but capable model with good vision comprehension |
|
||||||
|
| `llava-phi3` | Lightweight and fast model with vision comprehension |
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
|
|
||||||
|
|||||||
@ -12,11 +12,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
|
import HlsVideoPlayer from "@/components/player/HlsVideoPlayer";
|
||||||
import { baseUrl } from "@/api/baseUrl";
|
import { baseUrl } from "@/api/baseUrl";
|
||||||
import { REVIEW_PADDING } from "@/types/review";
|
import { REVIEW_PADDING } from "@/types/review";
|
||||||
import {
|
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||||
ASPECT_VERTICAL_LAYOUT,
|
|
||||||
ASPECT_WIDE_LAYOUT,
|
|
||||||
Recording,
|
|
||||||
} from "@/types/record";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
@ -79,139 +75,6 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// Fetch recording segments for the event's time range to handle motion-only gaps
|
|
||||||
const eventStartRecord = useMemo(
|
|
||||||
() => (event.start_time ?? 0) + annotationOffset / 1000,
|
|
||||||
[event.start_time, annotationOffset],
|
|
||||||
);
|
|
||||||
const eventEndRecord = useMemo(
|
|
||||||
() => (event.end_time ?? Date.now() / 1000) + annotationOffset / 1000,
|
|
||||||
[event.end_time, annotationOffset],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: recordings } = useSWR<Recording[]>(
|
|
||||||
event.camera
|
|
||||||
? [
|
|
||||||
`${event.camera}/recordings`,
|
|
||||||
{
|
|
||||||
after: eventStartRecord - REVIEW_PADDING,
|
|
||||||
before: eventEndRecord + REVIEW_PADDING,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert a timeline timestamp to actual video player time, accounting for
|
|
||||||
// motion-only recording gaps. Uses the same algorithm as DynamicVideoController.
|
|
||||||
const timestampToVideoTime = useCallback(
|
|
||||||
(timestamp: number): number => {
|
|
||||||
if (!recordings || recordings.length === 0) {
|
|
||||||
// Fallback to simple calculation if no recordings data
|
|
||||||
return timestamp - (eventStartRecord - REVIEW_PADDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
|
||||||
|
|
||||||
// If timestamp is before video start, return 0
|
|
||||||
if (timestamp < videoStartTime) return 0;
|
|
||||||
|
|
||||||
// Check if timestamp is before the first recording or after the last
|
|
||||||
if (
|
|
||||||
timestamp < recordings[0].start_time ||
|
|
||||||
timestamp > recordings[recordings.length - 1].end_time
|
|
||||||
) {
|
|
||||||
// No recording available at this timestamp
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the inpoint offset - the HLS video may start partway through the first segment
|
|
||||||
let inpointOffset = 0;
|
|
||||||
if (
|
|
||||||
videoStartTime > recordings[0].start_time &&
|
|
||||||
videoStartTime < recordings[0].end_time
|
|
||||||
) {
|
|
||||||
inpointOffset = videoStartTime - recordings[0].start_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
let seekSeconds = 0;
|
|
||||||
for (const segment of recordings) {
|
|
||||||
// Skip segments that end before our timestamp
|
|
||||||
if (segment.end_time <= timestamp) {
|
|
||||||
// Add this segment's duration, but subtract inpoint offset from first segment
|
|
||||||
if (segment === recordings[0]) {
|
|
||||||
seekSeconds += segment.duration - inpointOffset;
|
|
||||||
} else {
|
|
||||||
seekSeconds += segment.duration;
|
|
||||||
}
|
|
||||||
} else if (segment.start_time <= timestamp) {
|
|
||||||
// The timestamp is within this segment
|
|
||||||
if (segment === recordings[0]) {
|
|
||||||
// For the first segment, account for the inpoint offset
|
|
||||||
seekSeconds +=
|
|
||||||
timestamp - Math.max(segment.start_time, videoStartTime);
|
|
||||||
} else {
|
|
||||||
seekSeconds += timestamp - segment.start_time;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return seekSeconds;
|
|
||||||
},
|
|
||||||
[recordings, eventStartRecord],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert video player time back to timeline timestamp, accounting for
|
|
||||||
// motion-only recording gaps. Reverse of timestampToVideoTime.
|
|
||||||
const videoTimeToTimestamp = useCallback(
|
|
||||||
(playerTime: number): number => {
|
|
||||||
if (!recordings || recordings.length === 0) {
|
|
||||||
// Fallback to simple calculation if no recordings data
|
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
|
||||||
return playerTime + videoStartTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
|
||||||
|
|
||||||
// Calculate the inpoint offset - the video may start partway through the first segment
|
|
||||||
let inpointOffset = 0;
|
|
||||||
if (
|
|
||||||
videoStartTime > recordings[0].start_time &&
|
|
||||||
videoStartTime < recordings[0].end_time
|
|
||||||
) {
|
|
||||||
inpointOffset = videoStartTime - recordings[0].start_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
let timestamp = 0;
|
|
||||||
let totalTime = 0;
|
|
||||||
|
|
||||||
for (const segment of recordings) {
|
|
||||||
const segmentDuration =
|
|
||||||
segment === recordings[0]
|
|
||||||
? segment.duration - inpointOffset
|
|
||||||
: segment.duration;
|
|
||||||
|
|
||||||
if (totalTime + segmentDuration > playerTime) {
|
|
||||||
// The player time is within this segment
|
|
||||||
if (segment === recordings[0]) {
|
|
||||||
// For the first segment, add the inpoint offset
|
|
||||||
timestamp =
|
|
||||||
Math.max(segment.start_time, videoStartTime) +
|
|
||||||
(playerTime - totalTime);
|
|
||||||
} else {
|
|
||||||
timestamp = segment.start_time + (playerTime - totalTime);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
totalTime += segmentDuration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return timestamp;
|
|
||||||
},
|
|
||||||
[recordings, eventStartRecord],
|
|
||||||
);
|
|
||||||
|
|
||||||
eventSequence?.map((event) => {
|
eventSequence?.map((event) => {
|
||||||
event.data.zones_friendly_names = event.data?.zones?.map((zone) => {
|
event.data.zones_friendly_names = event.data?.zones?.map((zone) => {
|
||||||
return resolveZoneName(config, zone);
|
return resolveZoneName(config, zone);
|
||||||
@ -285,14 +148,17 @@ export function TrackingDetails({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For video mode: convert to video-relative time (accounting for motion-only gaps)
|
// For video mode: convert to video-relative time and seek player
|
||||||
const relativeTime = timestampToVideoTime(targetTimeRecord);
|
const eventStartRecord =
|
||||||
|
(event.start_time ?? 0) + annotationOffset / 1000;
|
||||||
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const relativeTime = targetTimeRecord - videoStartTime;
|
||||||
|
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.currentTime = relativeTime;
|
videoRef.current.currentTime = relativeTime;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[annotationOffset, displaySource, timestampToVideoTime],
|
[event.start_time, annotationOffset, displaySource],
|
||||||
);
|
);
|
||||||
|
|
||||||
const formattedStart = config
|
const formattedStart = config
|
||||||
@ -345,14 +211,24 @@ export function TrackingDetails({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// seekToTimestamp is a record stream timestamp
|
// seekToTimestamp is a record stream timestamp
|
||||||
// Convert to video position (accounting for motion-only recording gaps)
|
// event.start_time is detect stream time, convert to record
|
||||||
|
// The video clip starts at (eventStartRecord - REVIEW_PADDING)
|
||||||
if (!videoRef.current) return;
|
if (!videoRef.current) return;
|
||||||
const relativeTime = timestampToVideoTime(seekToTimestamp);
|
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
||||||
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const relativeTime = seekToTimestamp - videoStartTime;
|
||||||
if (relativeTime >= 0) {
|
if (relativeTime >= 0) {
|
||||||
videoRef.current.currentTime = relativeTime;
|
videoRef.current.currentTime = relativeTime;
|
||||||
}
|
}
|
||||||
setSeekToTimestamp(null);
|
setSeekToTimestamp(null);
|
||||||
}, [seekToTimestamp, displaySource, timestampToVideoTime]);
|
}, [
|
||||||
|
seekToTimestamp,
|
||||||
|
event.start_time,
|
||||||
|
annotationOffset,
|
||||||
|
apiHost,
|
||||||
|
event.camera,
|
||||||
|
displaySource,
|
||||||
|
]);
|
||||||
|
|
||||||
const isWithinEventRange = useMemo(() => {
|
const isWithinEventRange = useMemo(() => {
|
||||||
if (effectiveTime === undefined || event.start_time === undefined) {
|
if (effectiveTime === undefined || event.start_time === undefined) {
|
||||||
@ -459,13 +335,14 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
const handleTimeUpdate = useCallback(
|
const handleTimeUpdate = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
// Convert video player time back to timeline timestamp
|
// event.start_time is detect stream time, convert to record
|
||||||
// accounting for motion-only recording gaps
|
const eventStartRecord = event.start_time + annotationOffset / 1000;
|
||||||
const absoluteTime = videoTimeToTimestamp(time);
|
const videoStartTime = eventStartRecord - REVIEW_PADDING;
|
||||||
|
const absoluteTime = time + videoStartTime;
|
||||||
|
|
||||||
setCurrentTime(absoluteTime);
|
setCurrentTime(absoluteTime);
|
||||||
},
|
},
|
||||||
[videoTimeToTimestamp],
|
[event.start_time, annotationOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [src, setSrc] = useState(
|
const [src, setSrc] = useState(
|
||||||
|
|||||||
@ -198,9 +198,9 @@ export default function TriggerView({
|
|||||||
|
|
||||||
return axios
|
return axios
|
||||||
.put("config/set", configBody)
|
.put("config/set", configBody)
|
||||||
.then(async (configResponse) => {
|
.then((configResponse) => {
|
||||||
if (configResponse.status === 200) {
|
if (configResponse.status === 200) {
|
||||||
await updateConfig();
|
updateConfig();
|
||||||
const displayName =
|
const displayName =
|
||||||
friendly_name && friendly_name !== ""
|
friendly_name && friendly_name !== ""
|
||||||
? `${friendly_name} (${name})`
|
? `${friendly_name} (${name})`
|
||||||
@ -353,9 +353,9 @@ export default function TriggerView({
|
|||||||
|
|
||||||
return axios
|
return axios
|
||||||
.put("config/set", configBody)
|
.put("config/set", configBody)
|
||||||
.then(async (configResponse) => {
|
.then((configResponse) => {
|
||||||
if (configResponse.status === 200) {
|
if (configResponse.status === 200) {
|
||||||
await updateConfig();
|
updateConfig();
|
||||||
const friendly =
|
const friendly =
|
||||||
config?.cameras?.[selectedCamera]?.semantic_search
|
config?.cameras?.[selectedCamera]?.semantic_search
|
||||||
?.triggers?.[name]?.friendly_name;
|
?.triggers?.[name]?.friendly_name;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user