add ability to update description in frontend

This commit is contained in:
Jason Hunter 2023-12-19 14:57:46 -05:00
parent 99cdf10e1c
commit f220e4d3f3
4 changed files with 151 additions and 4 deletions

View File

@ -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) | | `is_submitted` | int | Filter events that are submitted to Frigate+ (0 or 1) |
| `min_length` | float | Minimum length of the event | | `min_length` | float | Minimum length of the event |
| `max_length` | float | Maximum 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` ### `GET /api/timeline`
@ -225,7 +227,17 @@ Sub labels must be 100 characters or shorter.
```json ```json
{ {
"subLabel": "some_string", "subLabel": "some_string",
"subLabelScore": 0.79, "subLabelScore": 0.79
}
```
### `POST /api/events/<id>/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 ```json
{ {
"playback": "realtime", // playback factor: realtime or timelapse_25x "playback": "realtime" // playback factor: realtime or timelapse_25x
} }
``` ```

View File

@ -46,7 +46,7 @@ from frigate.const import (
MAX_SEGMENT_DURATION, MAX_SEGMENT_DURATION,
RECORD_DIR, RECORD_DIR,
) )
from frigate.embeddings import Embeddings from frigate.embeddings import Embeddings, get_metadata
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.models import Event, Previews, Recordings, Regions, Timeline from frigate.models import Event, Previews, Recordings, Regions, Timeline
from frigate.object_processing import TrackedObject from frigate.object_processing import TrackedObject
@ -449,6 +449,51 @@ def set_sub_label(id):
) )
@bp.route("/events/<id>/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") @bp.route("/labels")
def get_labels(): def get_labels():
camera = request.args.get("camera", type=str, default="") camera = request.args.get("camera", type=str, default="")

View File

@ -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 (
<div className="w-full">
<textarea
className="block p-2.5 w-full h-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
// className="h-6 mt-6 w-full bg-transparent border-0 focus:outline-none focus:ring-0"
onBlur={handleBlur}
onFocus={handleFocus}
onInput={handleChange}
readOnly={readonly}
tabIndex="0"
{...props}
>
{value}
</textarea>
</div>
);
}

View File

@ -28,6 +28,7 @@ import Dialog from '../components/Dialog';
import MultiSelect from '../components/MultiSelect'; import MultiSelect from '../components/MultiSelect';
import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil'; import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil';
import TextField from '../components/TextField'; import TextField from '../components/TextField';
import TextArea from '../components/TextArea';
import TimeAgo from '../components/TimeAgo'; import TimeAgo from '../components/TimeAgo';
import Timepicker from '../components/TimePicker'; import Timepicker from '../components/TimePicker';
import TimelineSummary from '../components/TimelineSummary'; import TimelineSummary from '../components/TimelineSummary';
@ -259,6 +260,15 @@ export default function Events({ path, ...props }) {
setState({ ...state, showDownloadMenu: true }); 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) => { const showSimilarEvents = (event_id, e) => {
if (e) { if (e) {
e.stopPropagation(); e.stopPropagation();
@ -727,6 +737,7 @@ export default function Events({ path, ...props }) {
}); });
}} }}
onSave={onSave} onSave={onSave}
onUpdateDescription={onUpdateDescription}
showSimilarEvents={showSimilarEvents} showSimilarEvents={showSimilarEvents}
showSubmitToPlus={showSubmitToPlus} showSubmitToPlus={showSubmitToPlus}
/> />
@ -768,6 +779,7 @@ export default function Events({ path, ...props }) {
}); });
}} }}
onSave={onSave} onSave={onSave}
onUpdateDescription={onUpdateDescription}
showSimilarEvents={showSimilarEvents} showSimilarEvents={showSimilarEvents}
showSubmitToPlus={showSubmitToPlus} showSubmitToPlus={showSubmitToPlus}
/> />
@ -801,6 +813,7 @@ function Event({
onDownloadClick, onDownloadClick,
onReady, onReady,
onSave, onSave,
onUpdateDescription,
showSimilarEvents, showSimilarEvents,
showSubmitToPlus, showSubmitToPlus,
}) { }) {
@ -837,7 +850,18 @@ function Event({
{event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null} {event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null}
</div> </div>
{event?.data?.description ? ( {event?.data?.description ? (
<div className="text-sm flex flex-col grow pb-2">{event.data.description}</div> <div className="flex flex-col pb-2">
{viewEvent === event.id ? (
<TextArea
label=""
value={event.data.description}
onChangeText={(value) => onUpdateDescription(event, value)}
onClick={(e) => e.stopPropagation()}
/>
) : (
<div class="text-sm line-clamp-2">{event.data.description}</div>
)}
</div>
) : null} ) : null}
<div className="text-sm flex"> <div className="text-sm flex">
<Clock className="h-5 w-5 mr-2 inline" /> <Clock className="h-5 w-5 mr-2 inline" />