mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Miscellaneous Fixes (#21024)
* fix wording in reference config * spacing tweaks * make live view settings drawer scrollable * clarify audio transcription docs * change audio transcription icon to activity indicator when transcription is in progress the backend doesn't implement any kind of queueing for speech event transcription * tracking details tweaks - Add attribute box overlay and area - Add score - Throttle swr revalidation during video component rerendering * add mse codecs to console debug on errors * add camera name
This commit is contained in:
parent
2d8b6c8301
commit
aa8b423b68
@ -144,4 +144,10 @@ In order to use transcription and translation for past events, you must enable a
|
|||||||
|
|
||||||
The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type.
|
The transcribed/translated speech will appear in the description box in the Tracked Object Details pane. If Semantic Search is enabled, embeddings are generated for the transcription text and are fully searchable using the description search type.
|
||||||
|
|
||||||
Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient.
|
:::note
|
||||||
|
|
||||||
|
Only one `speech` event may be transcribed at a time. Frigate does not automatically transcribe `speech` events or implement a queue for long-running transcription model inference.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient.
|
||||||
|
|||||||
@ -700,11 +700,11 @@ genai:
|
|||||||
# Optional: Configuration for audio transcription
|
# Optional: Configuration for audio transcription
|
||||||
# NOTE: only the enabled option can be overridden at the camera level
|
# NOTE: only the enabled option can be overridden at the camera level
|
||||||
audio_transcription:
|
audio_transcription:
|
||||||
# Optional: Enable license plate recognition (default: shown below)
|
# Optional: Enable live and speech event audio transcription (default: shown below)
|
||||||
enabled: False
|
enabled: False
|
||||||
# Optional: The device to run the models on (default: shown below)
|
# Optional: The device to run the models on for live transcription. (default: shown below)
|
||||||
device: CPU
|
device: CPU
|
||||||
# Optional: Set the model size used for transcription. (default: shown below)
|
# Optional: Set the model size used for live transcription. (default: shown below)
|
||||||
model_size: small
|
model_size: small
|
||||||
# Optional: Set the language used for transcription translation. (default: shown below)
|
# Optional: Set the language used for transcription translation. (default: shown below)
|
||||||
# List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
|
# List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
|
||||||
|
|||||||
@ -542,6 +542,7 @@ def transcribe_audio(request: Request, body: AudioTranscriptionBody):
|
|||||||
status_code=409, # 409 Conflict
|
status_code=409, # 409 Conflict
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
logger.debug(f"Failed to transcribe audio, response: {response}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
|
|||||||
@ -23,6 +23,7 @@ from frigate.const import (
|
|||||||
NOTIFICATION_TEST,
|
NOTIFICATION_TEST,
|
||||||
REQUEST_REGION_GRID,
|
REQUEST_REGION_GRID,
|
||||||
UPDATE_AUDIO_ACTIVITY,
|
UPDATE_AUDIO_ACTIVITY,
|
||||||
|
UPDATE_AUDIO_TRANSCRIPTION_STATE,
|
||||||
UPDATE_BIRDSEYE_LAYOUT,
|
UPDATE_BIRDSEYE_LAYOUT,
|
||||||
UPDATE_CAMERA_ACTIVITY,
|
UPDATE_CAMERA_ACTIVITY,
|
||||||
UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
|
UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
|
||||||
@ -61,6 +62,7 @@ class Dispatcher:
|
|||||||
self.model_state: dict[str, ModelStatusTypesEnum] = {}
|
self.model_state: dict[str, ModelStatusTypesEnum] = {}
|
||||||
self.embeddings_reindex: dict[str, Any] = {}
|
self.embeddings_reindex: dict[str, Any] = {}
|
||||||
self.birdseye_layout: dict[str, Any] = {}
|
self.birdseye_layout: dict[str, Any] = {}
|
||||||
|
self.audio_transcription_state: str = "idle"
|
||||||
self._camera_settings_handlers: dict[str, Callable] = {
|
self._camera_settings_handlers: dict[str, Callable] = {
|
||||||
"audio": self._on_audio_command,
|
"audio": self._on_audio_command,
|
||||||
"audio_transcription": self._on_audio_transcription_command,
|
"audio_transcription": self._on_audio_transcription_command,
|
||||||
@ -178,6 +180,19 @@ class Dispatcher:
|
|||||||
def handle_model_state() -> None:
|
def handle_model_state() -> None:
|
||||||
self.publish("model_state", json.dumps(self.model_state.copy()))
|
self.publish("model_state", json.dumps(self.model_state.copy()))
|
||||||
|
|
||||||
|
def handle_update_audio_transcription_state() -> None:
|
||||||
|
if payload:
|
||||||
|
self.audio_transcription_state = payload
|
||||||
|
self.publish(
|
||||||
|
"audio_transcription_state",
|
||||||
|
json.dumps(self.audio_transcription_state),
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_audio_transcription_state() -> None:
|
||||||
|
self.publish(
|
||||||
|
"audio_transcription_state", json.dumps(self.audio_transcription_state)
|
||||||
|
)
|
||||||
|
|
||||||
def handle_update_embeddings_reindex_progress() -> None:
|
def handle_update_embeddings_reindex_progress() -> None:
|
||||||
self.embeddings_reindex = payload
|
self.embeddings_reindex = payload
|
||||||
self.publish(
|
self.publish(
|
||||||
@ -264,10 +279,12 @@ class Dispatcher:
|
|||||||
UPDATE_MODEL_STATE: handle_update_model_state,
|
UPDATE_MODEL_STATE: handle_update_model_state,
|
||||||
UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress,
|
UPDATE_EMBEDDINGS_REINDEX_PROGRESS: handle_update_embeddings_reindex_progress,
|
||||||
UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout,
|
UPDATE_BIRDSEYE_LAYOUT: handle_update_birdseye_layout,
|
||||||
|
UPDATE_AUDIO_TRANSCRIPTION_STATE: handle_update_audio_transcription_state,
|
||||||
NOTIFICATION_TEST: handle_notification_test,
|
NOTIFICATION_TEST: handle_notification_test,
|
||||||
"restart": handle_restart,
|
"restart": handle_restart,
|
||||||
"embeddingsReindexProgress": handle_embeddings_reindex_progress,
|
"embeddingsReindexProgress": handle_embeddings_reindex_progress,
|
||||||
"modelState": handle_model_state,
|
"modelState": handle_model_state,
|
||||||
|
"audioTranscriptionState": handle_audio_transcription_state,
|
||||||
"birdseyeLayout": handle_birdseye_layout,
|
"birdseyeLayout": handle_birdseye_layout,
|
||||||
"onConnect": handle_on_connect,
|
"onConnect": handle_on_connect,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -113,6 +113,7 @@ CLEAR_ONGOING_REVIEW_SEGMENTS = "clear_ongoing_review_segments"
|
|||||||
UPDATE_CAMERA_ACTIVITY = "update_camera_activity"
|
UPDATE_CAMERA_ACTIVITY = "update_camera_activity"
|
||||||
UPDATE_AUDIO_ACTIVITY = "update_audio_activity"
|
UPDATE_AUDIO_ACTIVITY = "update_audio_activity"
|
||||||
EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity"
|
EXPIRE_AUDIO_ACTIVITY = "expire_audio_activity"
|
||||||
|
UPDATE_AUDIO_TRANSCRIPTION_STATE = "update_audio_transcription_state"
|
||||||
UPDATE_EVENT_DESCRIPTION = "update_event_description"
|
UPDATE_EVENT_DESCRIPTION = "update_event_description"
|
||||||
UPDATE_REVIEW_DESCRIPTION = "update_review_description"
|
UPDATE_REVIEW_DESCRIPTION = "update_review_description"
|
||||||
UPDATE_MODEL_STATE = "update_model_state"
|
UPDATE_MODEL_STATE = "update_model_state"
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from frigate.config import FrigateConfig
|
|||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
CACHE_DIR,
|
CACHE_DIR,
|
||||||
MODEL_CACHE_DIR,
|
MODEL_CACHE_DIR,
|
||||||
|
UPDATE_AUDIO_TRANSCRIPTION_STATE,
|
||||||
UPDATE_EVENT_DESCRIPTION,
|
UPDATE_EVENT_DESCRIPTION,
|
||||||
)
|
)
|
||||||
from frigate.data_processing.types import PostProcessDataEnum
|
from frigate.data_processing.types import PostProcessDataEnum
|
||||||
@ -190,6 +191,8 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
|||||||
self.transcription_running = False
|
self.transcription_running = False
|
||||||
self.transcription_thread = None
|
self.transcription_thread = None
|
||||||
|
|
||||||
|
self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "idle")
|
||||||
|
|
||||||
def handle_request(self, topic: str, request_data: dict[str, any]) -> str | None:
|
def handle_request(self, topic: str, request_data: dict[str, any]) -> str | None:
|
||||||
if topic == "transcribe_audio":
|
if topic == "transcribe_audio":
|
||||||
event = request_data["event"]
|
event = request_data["event"]
|
||||||
@ -203,6 +206,8 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
|||||||
|
|
||||||
# Mark as running and start the thread
|
# Mark as running and start the thread
|
||||||
self.transcription_running = True
|
self.transcription_running = True
|
||||||
|
self.requestor.send_data(UPDATE_AUDIO_TRANSCRIPTION_STATE, "processing")
|
||||||
|
|
||||||
self.transcription_thread = threading.Thread(
|
self.transcription_thread = threading.Thread(
|
||||||
target=self._transcription_wrapper, args=(event,), daemon=True
|
target=self._transcription_wrapper, args=(event,), daemon=True
|
||||||
)
|
)
|
||||||
|
|||||||
@ -109,6 +109,7 @@ class TimelineProcessor(threading.Thread):
|
|||||||
event_data["region"],
|
event_data["region"],
|
||||||
),
|
),
|
||||||
"attribute": "",
|
"attribute": "",
|
||||||
|
"score": event_data["score"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -461,6 +461,40 @@ export function useEmbeddingsReindexProgress(
|
|||||||
return { payload: data };
|
return { payload: data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAudioTranscriptionProcessState(
|
||||||
|
revalidateOnFocus: boolean = true,
|
||||||
|
): { payload: string } {
|
||||||
|
const {
|
||||||
|
value: { payload },
|
||||||
|
send: sendCommand,
|
||||||
|
} = useWs("audio_transcription_state", "audioTranscriptionState");
|
||||||
|
|
||||||
|
const data = useDeepMemo(
|
||||||
|
payload ? (JSON.parse(payload as string) as string) : "idle",
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let listener = undefined;
|
||||||
|
if (revalidateOnFocus) {
|
||||||
|
sendCommand("audioTranscriptionState");
|
||||||
|
listener = () => {
|
||||||
|
if (document.visibilityState == "visible") {
|
||||||
|
sendCommand("audioTranscriptionState");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addEventListener("visibilitychange", listener);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (listener) {
|
||||||
|
removeEventListener("visibilitychange", listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [revalidateOnFocus]);
|
||||||
|
|
||||||
|
return { payload: data || "idle" };
|
||||||
|
}
|
||||||
|
|
||||||
export function useBirdseyeLayout(revalidateOnFocus: boolean = true): {
|
export function useBirdseyeLayout(revalidateOnFocus: boolean = true): {
|
||||||
payload: string;
|
payload: string;
|
||||||
} {
|
} {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type ObjectData = {
|
|||||||
pathPoints: PathPoint[];
|
pathPoints: PathPoint[];
|
||||||
currentZones: string[];
|
currentZones: string[];
|
||||||
currentBox?: number[];
|
currentBox?: number[];
|
||||||
|
currentAttributeBox?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ObjectTrackOverlay({
|
export default function ObjectTrackOverlay({
|
||||||
@ -105,6 +106,12 @@ export default function ObjectTrackOverlay({
|
|||||||
selectedObjectIds.length > 0
|
selectedObjectIds.length > 0
|
||||||
? ["event_ids", { ids: selectedObjectIds.join(",") }]
|
? ["event_ids", { ids: selectedObjectIds.join(",") }]
|
||||||
: null,
|
: null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch timeline data for each object ID using fixed number of hooks
|
// Fetch timeline data for each object ID using fixed number of hooks
|
||||||
@ -112,7 +119,12 @@ export default function ObjectTrackOverlay({
|
|||||||
selectedObjectIds.length > 0
|
selectedObjectIds.length > 0
|
||||||
? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000`
|
? `timeline?source_id=${selectedObjectIds.join(",")}&limit=1000`
|
||||||
: null,
|
: null,
|
||||||
{ revalidateOnFocus: false },
|
null,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
|
const getZonesFriendlyNames = (zones: string[], config: FrigateConfig) => {
|
||||||
@ -270,6 +282,7 @@ export default function ObjectTrackOverlay({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const currentBox = nearbyTimelineEvent?.data?.box;
|
const currentBox = nearbyTimelineEvent?.data?.box;
|
||||||
|
const currentAttributeBox = nearbyTimelineEvent?.data?.attribute_box;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
objectId,
|
objectId,
|
||||||
@ -278,6 +291,7 @@ export default function ObjectTrackOverlay({
|
|||||||
pathPoints: combinedPoints,
|
pathPoints: combinedPoints,
|
||||||
currentZones,
|
currentZones,
|
||||||
currentBox,
|
currentBox,
|
||||||
|
currentAttributeBox,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data
|
.filter((obj: ObjectData) => obj.pathPoints.length > 0); // Only include objects with path data
|
||||||
@ -482,6 +496,20 @@ export default function ObjectTrackOverlay({
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
)}
|
)}
|
||||||
|
{objData.currentAttributeBox && showBoundingBoxes && (
|
||||||
|
<g>
|
||||||
|
<rect
|
||||||
|
x={objData.currentAttributeBox[0] * videoWidth}
|
||||||
|
y={objData.currentAttributeBox[1] * videoHeight}
|
||||||
|
width={objData.currentAttributeBox[2] * videoWidth}
|
||||||
|
height={objData.currentAttributeBox[3] * videoHeight}
|
||||||
|
fill="none"
|
||||||
|
stroke={objData.color}
|
||||||
|
strokeWidth={boxStroke}
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -92,6 +92,7 @@ import { DialogPortal } from "@radix-ui/react-dialog";
|
|||||||
import { useDetailStream } from "@/context/detail-stream-context";
|
import { useDetailStream } from "@/context/detail-stream-context";
|
||||||
import { PiSlidersHorizontalBold } from "react-icons/pi";
|
import { PiSlidersHorizontalBold } from "react-icons/pi";
|
||||||
import { HiSparkles } from "react-icons/hi";
|
import { HiSparkles } from "react-icons/hi";
|
||||||
|
import { useAudioTranscriptionProcessState } from "@/api/ws";
|
||||||
|
|
||||||
const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
|
const SEARCH_TABS = ["snapshot", "tracking_details"] as const;
|
||||||
export type SearchTab = (typeof SEARCH_TABS)[number];
|
export type SearchTab = (typeof SEARCH_TABS)[number];
|
||||||
@ -1076,6 +1077,11 @@ function ObjectDetailsTab({
|
|||||||
});
|
});
|
||||||
}, [search, t]);
|
}, [search, t]);
|
||||||
|
|
||||||
|
// audio transcription processing state
|
||||||
|
|
||||||
|
const { payload: audioTranscriptionProcessState } =
|
||||||
|
useAudioTranscriptionProcessState();
|
||||||
|
|
||||||
// frigate+ submission
|
// frigate+ submission
|
||||||
|
|
||||||
type SubmissionState = "reviewing" | "uploading" | "submitted";
|
type SubmissionState = "reviewing" | "uploading" | "submitted";
|
||||||
@ -1431,10 +1437,20 @@ function ObjectDetailsTab({
|
|||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
aria-label={t("itemMenu.audioTranscription.label")}
|
aria-label={t("itemMenu.audioTranscription.label")}
|
||||||
className="text-primary/40 hover:text-primary/80"
|
className={cn(
|
||||||
|
"text-primary/40",
|
||||||
|
audioTranscriptionProcessState === "processing"
|
||||||
|
? "cursor-not-allowed"
|
||||||
|
: "hover:text-primary/80",
|
||||||
|
)}
|
||||||
onClick={onTranscribe}
|
onClick={onTranscribe}
|
||||||
|
disabled={audioTranscriptionProcessState === "processing"}
|
||||||
>
|
>
|
||||||
|
{audioTranscriptionProcessState === "processing" ? (
|
||||||
|
<ActivityIndicator className="size-4" />
|
||||||
|
) : (
|
||||||
<FaMicrophone className="size-4" />
|
<FaMicrophone className="size-4" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
|
|||||||
@ -75,12 +75,15 @@ export function TrackingDetails({
|
|||||||
setIsVideoLoading(true);
|
setIsVideoLoading(true);
|
||||||
}, [event.id]);
|
}, [event.id]);
|
||||||
|
|
||||||
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>([
|
const { data: eventSequence } = useSWR<TrackingDetailsSequence[]>(
|
||||||
"timeline",
|
["timeline", { source_id: event.id }],
|
||||||
|
null,
|
||||||
{
|
{
|
||||||
source_id: event.id,
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
},
|
},
|
||||||
]);
|
);
|
||||||
|
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -104,6 +107,12 @@ export function TrackingDetails({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: null,
|
: null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 30000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert a timeline timestamp to actual video player time, accounting for
|
// Convert a timeline timestamp to actual video player time, accounting for
|
||||||
@ -714,53 +723,6 @@ export function TrackingDetails({
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{eventSequence.map((item, idx) => {
|
{eventSequence.map((item, idx) => {
|
||||||
const isActive =
|
|
||||||
Math.abs(
|
|
||||||
(effectiveTime ?? 0) - (item.timestamp ?? 0),
|
|
||||||
) <= 0.5;
|
|
||||||
const formattedEventTimestamp = config
|
|
||||||
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
|
|
||||||
timezone: config.ui.timezone,
|
|
||||||
date_format:
|
|
||||||
config.ui.time_format == "24hour"
|
|
||||||
? t(
|
|
||||||
"time.formattedTimestampHourMinuteSecond.24hour",
|
|
||||||
{ ns: "common" },
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"time.formattedTimestampHourMinuteSecond.12hour",
|
|
||||||
{ ns: "common" },
|
|
||||||
),
|
|
||||||
time_style: "medium",
|
|
||||||
date_style: "medium",
|
|
||||||
})
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const ratio =
|
|
||||||
Array.isArray(item.data.box) &&
|
|
||||||
item.data.box.length >= 4
|
|
||||||
? (
|
|
||||||
aspectRatio *
|
|
||||||
(item.data.box[2] / item.data.box[3])
|
|
||||||
).toFixed(2)
|
|
||||||
: "N/A";
|
|
||||||
const areaPx =
|
|
||||||
Array.isArray(item.data.box) &&
|
|
||||||
item.data.box.length >= 4
|
|
||||||
? Math.round(
|
|
||||||
(config.cameras[event.camera]?.detect?.width ??
|
|
||||||
0) *
|
|
||||||
(config.cameras[event.camera]?.detect
|
|
||||||
?.height ?? 0) *
|
|
||||||
(item.data.box[2] * item.data.box[3]),
|
|
||||||
)
|
|
||||||
: undefined;
|
|
||||||
const areaPct =
|
|
||||||
Array.isArray(item.data.box) &&
|
|
||||||
item.data.box.length >= 4
|
|
||||||
? (item.data.box[2] * item.data.box[3]).toFixed(4)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
|
key={`${item.timestamp}-${item.source_id ?? ""}-${idx}`}
|
||||||
@ -770,11 +732,7 @@ export function TrackingDetails({
|
|||||||
>
|
>
|
||||||
<LifecycleIconRow
|
<LifecycleIconRow
|
||||||
item={item}
|
item={item}
|
||||||
isActive={isActive}
|
event={event}
|
||||||
formattedEventTimestamp={formattedEventTimestamp}
|
|
||||||
ratio={ratio}
|
|
||||||
areaPx={areaPx}
|
|
||||||
areaPct={areaPct}
|
|
||||||
onClick={() => handleLifecycleClick(item)}
|
onClick={() => handleLifecycleClick(item)}
|
||||||
setSelectedZone={setSelectedZone}
|
setSelectedZone={setSelectedZone}
|
||||||
getZoneColor={getZoneColor}
|
getZoneColor={getZoneColor}
|
||||||
@ -798,11 +756,7 @@ export function TrackingDetails({
|
|||||||
|
|
||||||
type LifecycleIconRowProps = {
|
type LifecycleIconRowProps = {
|
||||||
item: TrackingDetailsSequence;
|
item: TrackingDetailsSequence;
|
||||||
isActive?: boolean;
|
event: Event;
|
||||||
formattedEventTimestamp: string;
|
|
||||||
ratio: string;
|
|
||||||
areaPx?: number;
|
|
||||||
areaPct?: string;
|
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
setSelectedZone: (z: string) => void;
|
setSelectedZone: (z: string) => void;
|
||||||
getZoneColor: (zoneName: string) => number[] | undefined;
|
getZoneColor: (zoneName: string) => number[] | undefined;
|
||||||
@ -812,11 +766,7 @@ type LifecycleIconRowProps = {
|
|||||||
|
|
||||||
function LifecycleIconRow({
|
function LifecycleIconRow({
|
||||||
item,
|
item,
|
||||||
isActive,
|
event,
|
||||||
formattedEventTimestamp,
|
|
||||||
ratio,
|
|
||||||
areaPx,
|
|
||||||
areaPct,
|
|
||||||
onClick,
|
onClick,
|
||||||
setSelectedZone,
|
setSelectedZone,
|
||||||
getZoneColor,
|
getZoneColor,
|
||||||
@ -826,9 +776,101 @@ function LifecycleIconRow({
|
|||||||
const { t } = useTranslation(["views/explore", "components/player"]);
|
const { t } = useTranslation(["views/explore", "components/player"]);
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const aspectRatio = useMemo(() => {
|
||||||
|
if (!config) {
|
||||||
|
return 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
config.cameras[event.camera].detect.width /
|
||||||
|
config.cameras[event.camera].detect.height
|
||||||
|
);
|
||||||
|
}, [config, event]);
|
||||||
|
|
||||||
|
const isActive = useMemo(
|
||||||
|
() => Math.abs((effectiveTime ?? 0) - (item.timestamp ?? 0)) <= 0.5,
|
||||||
|
[effectiveTime, item.timestamp],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedEventTimestamp = useMemo(
|
||||||
|
() =>
|
||||||
|
config
|
||||||
|
? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
|
||||||
|
timezone: config.ui.timezone,
|
||||||
|
date_format:
|
||||||
|
config.ui.time_format == "24hour"
|
||||||
|
? t("time.formattedTimestampHourMinuteSecond.24hour", {
|
||||||
|
ns: "common",
|
||||||
|
})
|
||||||
|
: t("time.formattedTimestampHourMinuteSecond.12hour", {
|
||||||
|
ns: "common",
|
||||||
|
}),
|
||||||
|
time_style: "medium",
|
||||||
|
date_style: "medium",
|
||||||
|
})
|
||||||
|
: "",
|
||||||
|
[config, item.timestamp, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ratio = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
|
? (aspectRatio * (item.data.box[2] / item.data.box[3])).toFixed(2)
|
||||||
|
: "N/A",
|
||||||
|
[aspectRatio, item.data.box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaPx = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
|
? Math.round(
|
||||||
|
(config?.cameras[event.camera]?.detect?.width ?? 0) *
|
||||||
|
(config?.cameras[event.camera]?.detect?.height ?? 0) *
|
||||||
|
(item.data.box[2] * item.data.box[3]),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
[config, event.camera, item.data.box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeAreaPx = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.attribute_box) &&
|
||||||
|
item.data.attribute_box.length >= 4
|
||||||
|
? Math.round(
|
||||||
|
(config?.cameras[event.camera]?.detect?.width ?? 0) *
|
||||||
|
(config?.cameras[event.camera]?.detect?.height ?? 0) *
|
||||||
|
(item.data.attribute_box[2] * item.data.attribute_box[3]),
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
[config, event.camera, item.data.attribute_box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeAreaPct = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.attribute_box) &&
|
||||||
|
item.data.attribute_box.length >= 4
|
||||||
|
? (item.data.attribute_box[2] * item.data.attribute_box[3]).toFixed(4)
|
||||||
|
: undefined,
|
||||||
|
[item.data.attribute_box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaPct = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.isArray(item.data.box) && item.data.box.length >= 4
|
||||||
|
? (item.data.box[2] * item.data.box[3]).toFixed(4)
|
||||||
|
: undefined,
|
||||||
|
[item.data.box],
|
||||||
|
);
|
||||||
|
|
||||||
|
const score = useMemo(() => {
|
||||||
|
if (item.data.score !== undefined) {
|
||||||
|
return (item.data.score * 100).toFixed(0) + "%";
|
||||||
|
}
|
||||||
|
return "N/A";
|
||||||
|
}, [item.data.score]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
@ -856,16 +898,28 @@ function LifecycleIconRow({
|
|||||||
<div className="text-md flex items-start break-words text-left">
|
<div className="text-md flex items-start break-words text-left">
|
||||||
{getLifecycleItemDescription(item)}
|
{getLifecycleItemDescription(item)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-secondary-foreground md:gap-5">
|
<div className="my-2 ml-2 flex flex-col flex-wrap items-start gap-1.5 text-xs text-secondary-foreground">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.score")}
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">{score}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-primary-variant">
|
<span className="text-primary-variant">
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.ratio")}
|
{t("trackingDetails.lifecycleItemDesc.header.ratio")}
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium text-primary">{ratio}</span>
|
<span className="font-medium text-primary">{ratio}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-primary-variant">
|
<span className="text-primary-variant">
|
||||||
{t("trackingDetails.lifecycleItemDesc.header.area")}
|
{t("trackingDetails.lifecycleItemDesc.header.area")}{" "}
|
||||||
|
{attributeAreaPx !== undefined &&
|
||||||
|
attributeAreaPct !== undefined && (
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
({getTranslatedLabel(item.data.label)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{areaPx !== undefined && areaPct !== undefined ? (
|
{areaPx !== undefined && areaPct !== undefined ? (
|
||||||
<span className="font-medium text-primary">
|
<span className="font-medium text-primary">
|
||||||
@ -876,9 +930,25 @@ function LifecycleIconRow({
|
|||||||
<span>N/A</span>
|
<span>N/A</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{attributeAreaPx !== undefined &&
|
||||||
|
attributeAreaPct !== undefined && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-primary-variant">
|
||||||
|
{t("trackingDetails.lifecycleItemDesc.header.area")} (
|
||||||
|
{getTranslatedLabel(item.data.attribute)})
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{t("information.pixels", {
|
||||||
|
ns: "common",
|
||||||
|
area: attributeAreaPx,
|
||||||
|
})}{" "}
|
||||||
|
· {attributeAreaPct}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{item.data?.zones && item.data.zones.length > 0 && (
|
{item.data?.zones && item.data.zones.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||||
{item.data.zones.map((zone, zidx) => {
|
{item.data.zones.map((zone, zidx) => {
|
||||||
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -82,6 +82,7 @@ function MSEPlayer({
|
|||||||
[key: string]: (msg: { value: string; type: string }) => void;
|
[key: string]: (msg: { value: string; type: string }) => void;
|
||||||
}>({});
|
}>({});
|
||||||
const msRef = useRef<MediaSource | null>(null);
|
const msRef = useRef<MediaSource | null>(null);
|
||||||
|
const mseCodecRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const wsURL = useMemo(() => {
|
const wsURL = useMemo(() => {
|
||||||
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
|
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
|
||||||
@ -93,6 +94,10 @@ function MSEPlayer({
|
|||||||
console.error(
|
console.error(
|
||||||
`${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`,
|
`${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`,
|
||||||
);
|
);
|
||||||
|
if (mseCodecRef.current) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(`${camera} - MSE codec in use: ${mseCodecRef.current}`);
|
||||||
|
}
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
},
|
},
|
||||||
[camera, onError],
|
[camera, onError],
|
||||||
@ -299,6 +304,9 @@ function MSEPlayer({
|
|||||||
onmessageRef.current["mse"] = (msg) => {
|
onmessageRef.current["mse"] = (msg) => {
|
||||||
if (msg.type !== "mse") return;
|
if (msg.type !== "mse") return;
|
||||||
|
|
||||||
|
// Store the codec value for error logging
|
||||||
|
mseCodecRef.current = msg.value;
|
||||||
|
|
||||||
let sb: SourceBuffer | undefined;
|
let sb: SourceBuffer | undefined;
|
||||||
try {
|
try {
|
||||||
sb = msRef.current?.addSourceBuffer(msg.value);
|
sb = msRef.current?.addSourceBuffer(msg.value);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export type TrackingDetailsSequence = {
|
|||||||
data: {
|
data: {
|
||||||
camera: string;
|
camera: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
score: number;
|
||||||
sub_label: string;
|
sub_label: string;
|
||||||
box?: [number, number, number, number];
|
box?: [number, number, number, number];
|
||||||
region: [number, number, number, number];
|
region: [number, number, number, number];
|
||||||
|
|||||||
@ -1376,8 +1376,9 @@ function FrigateCameraFeatures({
|
|||||||
title={t("cameraSettings.title", { camera })}
|
title={t("cameraSettings.title", { camera })}
|
||||||
/>
|
/>
|
||||||
</DrawerTrigger>
|
</DrawerTrigger>
|
||||||
<DrawerContent className="rounded-2xl px-2 py-4">
|
<DrawerContent className="max-h-[75dvh] overflow-hidden rounded-2xl">
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
<div className="scrollbar-container mt-2 flex h-auto flex-col gap-2 overflow-y-auto px-2 py-4">
|
||||||
|
<>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
@ -1425,7 +1426,9 @@ function FrigateCameraFeatures({
|
|||||||
disabled={audioState == "OFF"}
|
disabled={audioState == "OFF"}
|
||||||
isChecked={transcriptionState == "ON"}
|
isChecked={transcriptionState == "ON"}
|
||||||
onCheckedChange={() =>
|
onCheckedChange={() =>
|
||||||
sendTranscription(transcriptionState == "ON" ? "OFF" : "ON")
|
sendTranscription(
|
||||||
|
transcriptionState == "ON" ? "OFF" : "ON",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -1440,7 +1443,6 @@ function FrigateCameraFeatures({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 flex flex-col gap-5">
|
<div className="mt-3 flex flex-col gap-5">
|
||||||
{!isRestreamed && (
|
{!isRestreamed && (
|
||||||
@ -1482,7 +1484,8 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isRestreamed && Object.values(camera.live.streams).length > 0 && (
|
{isRestreamed &&
|
||||||
|
Object.values(camera.live.streams).length > 0 && (
|
||||||
<div className="mt-1 p-2">
|
<div className="mt-1 p-2">
|
||||||
<div className="mb-1 text-sm">{t("stream.title")}</div>
|
<div className="mb-1 text-sm">{t("stream.title")}</div>
|
||||||
<Select
|
<Select
|
||||||
@ -1526,7 +1529,9 @@ function FrigateCameraFeatures({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{preferredLiveMode != "jsmpeg" && !debug && isRestreamed && (
|
{preferredLiveMode != "jsmpeg" &&
|
||||||
|
!debug &&
|
||||||
|
isRestreamed && (
|
||||||
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
<div className="mt-1 flex flex-row items-center gap-1 text-sm text-muted-foreground">
|
||||||
{supportsAudioOutput ? (
|
{supportsAudioOutput ? (
|
||||||
<>
|
<>
|
||||||
@ -1555,7 +1560,9 @@ function FrigateCameraFeatures({
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline"
|
className="inline"
|
||||||
>
|
>
|
||||||
{t("readTheDocumentation", { ns: "common" })}
|
{t("readTheDocumentation", {
|
||||||
|
ns: "common",
|
||||||
|
})}
|
||||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -1599,7 +1606,9 @@ function FrigateCameraFeatures({
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline"
|
className="inline"
|
||||||
>
|
>
|
||||||
{t("readTheDocumentation", { ns: "common" })}
|
{t("readTheDocumentation", {
|
||||||
|
ns: "common",
|
||||||
|
})}
|
||||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -1613,7 +1622,9 @@ function FrigateCameraFeatures({
|
|||||||
<div className="mt-2 flex flex-col items-center gap-3">
|
<div className="mt-2 flex flex-col items-center gap-3">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<IoIosWarning className="mr-1 size-8 text-danger" />
|
<IoIosWarning className="mr-1 size-8 text-danger" />
|
||||||
<p className="text-sm">{t("stream.lowBandwidth.tips")}</p>
|
<p className="text-sm">
|
||||||
|
{t("stream.lowBandwidth.tips")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className={`flex items-center gap-2.5 rounded-lg`}
|
className={`flex items-center gap-2.5 rounded-lg`}
|
||||||
@ -1651,7 +1662,8 @@ function FrigateCameraFeatures({
|
|||||||
onClick={handleEventButtonClick}
|
onClick={handleEventButtonClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-auto w-full whitespace-normal",
|
"h-auto w-full whitespace-normal",
|
||||||
isRecording && "animate-pulse bg-red-500 hover:bg-red-600",
|
isRecording &&
|
||||||
|
"animate-pulse bg-red-500 hover:bg-red-600",
|
||||||
)}
|
)}
|
||||||
disabled={debug}
|
disabled={debug}
|
||||||
>
|
>
|
||||||
@ -1700,6 +1712,8 @@ function FrigateCameraFeatures({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -784,7 +784,7 @@ export default function AuthenticationView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col">
|
<div className="flex size-full flex-col">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
|
||||||
{section === "users" && UsersSection}
|
{section === "users" && UsersSection}
|
||||||
{section === "roles" && RolesSection}
|
{section === "roles" && RolesSection}
|
||||||
{!section && (
|
{!section && (
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export default function CameraManagementView({
|
|||||||
closeButton
|
closeButton
|
||||||
/>
|
/>
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
{viewMode === "settings" ? (
|
{viewMode === "settings" ? (
|
||||||
<>
|
<>
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
|
|||||||
@ -298,7 +298,7 @@ export default function CameraReviewSettingsView({
|
|||||||
<>
|
<>
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
{t("cameraReview.title")}
|
{t("cameraReview.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
@ -244,7 +244,7 @@ export default function EnrichmentsSettingsView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
{t("enrichments.title")}
|
{t("enrichments.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
@ -211,7 +211,7 @@ export default function FrigatePlusSettingsView({
|
|||||||
<>
|
<>
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
{t("frigatePlus.title")}
|
{t("frigatePlus.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
@ -434,7 +434,7 @@ export default function MasksAndZonesView({
|
|||||||
{cameraConfig && editingPolygons && (
|
{cameraConfig && editingPolygons && (
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12">
|
||||||
{editPane == "zone" && (
|
{editPane == "zone" && (
|
||||||
<ZoneEditPane
|
<ZoneEditPane
|
||||||
polygons={editingPolygons}
|
polygons={editingPolygons}
|
||||||
|
|||||||
@ -191,7 +191,7 @@ export default function MotionTunerView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mr-3 md:mt-0 md:w-3/12">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
{t("motionDetectionTuner.title")}
|
{t("motionDetectionTuner.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
@ -331,7 +331,7 @@ export default function NotificationView({
|
|||||||
|
|
||||||
if (!("Notification" in window) || !window.isSecureContext) {
|
if (!("Notification" in window) || !window.isSecureContext) {
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div className="col-span-1">
|
<div className="col-span-1">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
@ -385,7 +385,7 @@ export default function NotificationView({
|
|||||||
<>
|
<>
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto px-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto px-2 md:order-none">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
isAdmin && "grid w-full grid-cols-1 gap-4 md:grid-cols-2",
|
isAdmin && "grid w-full grid-cols-1 gap-4 md:grid-cols-2",
|
||||||
|
|||||||
@ -164,7 +164,7 @@ export default function ObjectSettingsView({
|
|||||||
return (
|
return (
|
||||||
<div className="mt-1 flex size-full flex-col pb-2 md:flex-row">
|
<div className="mt-1 flex size-full flex-col pb-2 md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0 md:w-3/12">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
{t("debug.title")}
|
{t("debug.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
@ -434,7 +434,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
|||||||
{t("debug.objectShapeFilterDrawing.area")}
|
{t("debug.objectShapeFilterDrawing.area")}
|
||||||
</p>
|
</p>
|
||||||
{obj.area ? (
|
{obj.area ? (
|
||||||
<>
|
<div className="text-end">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
px: {obj.area.toString()}
|
px: {obj.area.toString()}
|
||||||
</div>
|
</div>
|
||||||
@ -448,7 +448,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
|
|||||||
.toFixed(4)
|
.toFixed(4)
|
||||||
.toString()}
|
.toString()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -440,7 +440,7 @@ export default function TriggerView({
|
|||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
|
||||||
{!isSemanticSearchEnabled ? (
|
{!isSemanticSearchEnabled ? (
|
||||||
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
<div className="mb-5 flex flex-row items-center justify-between gap-2">
|
||||||
<div className="flex flex-col items-start">
|
<div className="flex flex-col items-start">
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export default function UiSettingsView() {
|
|||||||
<>
|
<>
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
<Heading as="h4" className="mb-2">
|
<Heading as="h4" className="mb-2">
|
||||||
{t("general.title")}
|
{t("general.title")}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user