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) |
| `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/<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
{
"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,
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/<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")
def get_labels():
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 { 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}
</div>
{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}
<div className="text-sm flex">
<Clock className="h-5 w-5 mr-2 inline" />