From f220e4d3f37a1e2bf44b6080d13a8533e7e56b8b Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Tue, 19 Dec 2023 14:57:46 -0500 Subject: [PATCH] add ability to update description in frontend --- docs/docs/integrations/api.md | 16 ++++++- frigate/http.py | 47 +++++++++++++++++++- web-old/src/components/TextArea.jsx | 66 +++++++++++++++++++++++++++++ web-old/src/routes/Events.jsx | 26 +++++++++++- 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 web-old/src/components/TextArea.jsx diff --git a/docs/docs/integrations/api.md b/docs/docs/integrations/api.md index 20877bb6f..64f427b4b 100644 --- a/docs/docs/integrations/api.md +++ b/docs/docs/integrations/api.md @@ -174,6 +174,8 @@ Events from the database. Accepts the following query string parameters: | `is_submitted` | int | Filter events that are submitted to Frigate+ (0 or 1) | | `min_length` | float | Minimum length of the event | | `max_length` | float | Maximum length of the event | +| `search` | str | Search query for semantic search | +| `like` | str | Event ID for thumbnail similarity search | ### `GET /api/timeline` @@ -225,7 +227,17 @@ Sub labels must be 100 characters or shorter. ```json { "subLabel": "some_string", - "subLabelScore": 0.79, + "subLabelScore": 0.79 +} +``` + +### `POST /api/events//description` + +Set a description for an event to add details about what is occurring. Used during semantic search. + +```json +{ + "description": "This is a black and white dog walking on the sidewalk." } ``` @@ -298,7 +310,7 @@ It is also possible to export this recording as a timelapse. ```json { - "playback": "realtime", // playback factor: realtime or timelapse_25x + "playback": "realtime" // playback factor: realtime or timelapse_25x } ``` diff --git a/frigate/http.py b/frigate/http.py index d063c920b..443243290 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -46,7 +46,7 @@ from frigate.const import ( MAX_SEGMENT_DURATION, RECORD_DIR, ) -from frigate.embeddings import Embeddings +from frigate.embeddings import Embeddings, get_metadata from frigate.events.external import ExternalEventProcessor from frigate.models import Event, Previews, Recordings, Regions, Timeline from frigate.object_processing import TrackedObject @@ -449,6 +449,51 @@ def set_sub_label(id): ) +@bp.route("/events//description", methods=("POST",)) +def set_description(id): + try: + event: Event = Event.get(Event.id == id) + except DoesNotExist: + return make_response( + jsonify({"success": False, "message": "Event " + id + " not found"}), 404 + ) + + json: dict[str, any] = request.get_json(silent=True) or {} + new_description = json.get("description") + + if new_description is None or len(new_description) == 0: + return make_response( + jsonify( + { + "success": False, + "message": "description cannot be empty", + } + ), + 400, + ) + + event.data["description"] = new_description + event.save() + + # If semantic search is enabled, update the index + if current_app.embeddings is not None: + current_app.embeddings.description.upsert( + documents=[new_description], + metadatas=[get_metadata(event)], + ids=[id], + ) + + return make_response( + jsonify( + { + "success": True, + "message": "Event " + id + " description set to " + new_description, + } + ), + 200, + ) + + @bp.route("/labels") def get_labels(): camera = request.args.get("camera", type=str, default="") diff --git a/web-old/src/components/TextArea.jsx b/web-old/src/components/TextArea.jsx new file mode 100644 index 000000000..22e04d375 --- /dev/null +++ b/web-old/src/components/TextArea.jsx @@ -0,0 +1,66 @@ +import { h } from 'preact'; +import { useCallback, useEffect, useState } from 'preact/hooks'; + +export default function TextArea({ + helpText, + keyboardType = 'text', + inputRef, + onBlur, + onChangeText, + onFocus, + readonly, + value: propValue = '', + ...props +}) { + const [isFocused, setFocused] = useState(false); + const [value, setValue] = useState(propValue); + + const handleFocus = useCallback( + (event) => { + setFocused(true); + onFocus && onFocus(event); + }, + [onFocus] + ); + + const handleBlur = useCallback( + (event) => { + setFocused(false); + onBlur && onBlur(event); + }, + [onBlur] + ); + + const handleChange = useCallback( + (event) => { + const { value } = event.target; + setValue(value); + onChangeText && onChangeText(value); + }, + [onChangeText, setValue] + ); + + useEffect(() => { + if (propValue !== value) { + setValue(propValue); + } + // DO NOT include `value` + }, [propValue, setValue]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+ +
+ ); +} diff --git a/web-old/src/routes/Events.jsx b/web-old/src/routes/Events.jsx index 8462188fe..7cabe2ec3 100644 --- a/web-old/src/routes/Events.jsx +++ b/web-old/src/routes/Events.jsx @@ -28,6 +28,7 @@ import Dialog from '../components/Dialog'; import MultiSelect from '../components/MultiSelect'; import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil'; import TextField from '../components/TextField'; +import TextArea from '../components/TextArea'; import TimeAgo from '../components/TimeAgo'; import Timepicker from '../components/TimePicker'; import TimelineSummary from '../components/TimelineSummary'; @@ -259,6 +260,15 @@ export default function Events({ path, ...props }) { setState({ ...state, showDownloadMenu: true }); }; + let descriptionTimeout; + const onUpdateDescription = async (event, description) => { + clearTimeout(descriptionTimeout); + descriptionTimeout = setTimeout(async () => { + event.data.description = description; + await axios.post(`events/${event.id}/description`, { description }); + }, 500); + }; + const showSimilarEvents = (event_id, e) => { if (e) { e.stopPropagation(); @@ -727,6 +737,7 @@ export default function Events({ path, ...props }) { }); }} onSave={onSave} + onUpdateDescription={onUpdateDescription} showSimilarEvents={showSimilarEvents} showSubmitToPlus={showSubmitToPlus} /> @@ -768,6 +779,7 @@ export default function Events({ path, ...props }) { }); }} onSave={onSave} + onUpdateDescription={onUpdateDescription} showSimilarEvents={showSimilarEvents} showSubmitToPlus={showSubmitToPlus} /> @@ -801,6 +813,7 @@ function Event({ onDownloadClick, onReady, onSave, + onUpdateDescription, showSimilarEvents, showSubmitToPlus, }) { @@ -837,7 +850,18 @@ function Event({ {event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null} {event?.data?.description ? ( -
{event.data.description}
+
+ {viewEvent === event.id ? ( +