Simplify highlight API for automation
This commit is contained in:
@@ -65,23 +65,25 @@ To create a highlight in the player, play the recording, press **Mark start**,
|
|||||||
move to the end of the interesting part, press **Mark end**, enter a name, and
|
move to the end of the interesting part, press **Mark end**, enter a name, and
|
||||||
press **Add highlight**. Highlights are saved in `media-mp4/highlights.json`.
|
press **Add highlight**. Highlights are saved in `media-mp4/highlights.json`.
|
||||||
|
|
||||||
The highlight API is intentionally simple. An AI or script can list highlights:
|
The highlight API is intentionally simple. An AI agent can list highlights:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl http://127.0.0.1:8765/api/highlights
|
curl http://127.0.0.1:8765/api/highlights
|
||||||
```
|
```
|
||||||
|
|
||||||
It can append a highlight with a date, seconds from midnight, and a label:
|
It can append one using normal date and clock values:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -X POST http://127.0.0.1:8765/api/highlights \
|
curl -X POST http://127.0.0.1:8765/api/highlights \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"date":"20260827","start":34000,"end":34120,"label":"Person at entrance"}'
|
-d '{"date":"2026-08-27","start_time":"09:26:40","end_time":"09:28:40","title":"Person at entrance"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
The API stores the entry without changing any video. Times are seconds from
|
The API stores the entry without changing any video. `start` and `end` may also
|
||||||
midnight, so `34000` means `09:26:40`. This makes it straightforward for an
|
be used as seconds from midnight, and `duration` may be used instead of an end
|
||||||
AI to append detections, labels, or review segments programmatically.
|
time. For example, `{"start":"09:26:40","duration":120}` creates a
|
||||||
|
two-minute highlight. This makes it straightforward for an AI to append
|
||||||
|
detections, labels, or review segments programmatically.
|
||||||
|
|
||||||
## Custom Folders And Ports
|
## Custom Folders And Ports
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from urllib.parse import parse_qs, urlparse
|
|||||||
|
|
||||||
DATE_RE = re.compile(r"^\d{8}$")
|
DATE_RE = re.compile(r"^\d{8}$")
|
||||||
VIDEO_RE = re.compile(r"^(\d{2})\.(\d{2})\.(\d{2})\.(avi|mp4)$", re.IGNORECASE)
|
VIDEO_RE = re.compile(r"^(\d{2})\.(\d{2})\.(\d{2})\.(avi|mp4)$", re.IGNORECASE)
|
||||||
|
TIME_RE = re.compile(r"^(\d{1,2}):(\d{2})(?::(\d{2}))?$")
|
||||||
APP_DIR = Path(__file__).resolve().parent
|
APP_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +43,25 @@ def scan_recordings(root):
|
|||||||
return recordings
|
return recordings
|
||||||
|
|
||||||
|
|
||||||
|
def parse_clock(value):
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
match = TIME_RE.fullmatch(str(value).strip())
|
||||||
|
if not match:
|
||||||
|
raise ValueError
|
||||||
|
hour, minute, second = int(match.group(1)), int(match.group(2)), int(match.group(3) or 0)
|
||||||
|
if hour > 23 or minute > 59 or second > 59:
|
||||||
|
raise ValueError
|
||||||
|
return hour * 3600 + minute * 60 + second
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_date(value):
|
||||||
|
date = str(value).replace("-", "")
|
||||||
|
if not DATE_RE.fullmatch(date):
|
||||||
|
raise ValueError
|
||||||
|
return date
|
||||||
|
|
||||||
|
|
||||||
class PlayerHandler(BaseHTTPRequestHandler):
|
class PlayerHandler(BaseHTTPRequestHandler):
|
||||||
protocol_version = "HTTP/1.1"
|
protocol_version = "HTTP/1.1"
|
||||||
|
|
||||||
@@ -74,17 +94,21 @@ class PlayerHandler(BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
length = int(self.headers.get("Content-Length", "0"))
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
item = json.loads(self.rfile.read(length))
|
item = json.loads(self.rfile.read(length))
|
||||||
date = str(item["date"])
|
date = normalize_date(item["date"])
|
||||||
start = float(item["start"])
|
start = parse_clock(item.get("start", item.get("start_time")))
|
||||||
end = float(item["end"])
|
end_value = item.get("end", item.get("end_time"))
|
||||||
if not DATE_RE.fullmatch(date) or not (0 <= start < end <= 86400):
|
if end_value is None and item.get("duration") is not None:
|
||||||
|
end = start + parse_clock(item["duration"])
|
||||||
|
else:
|
||||||
|
end = parse_clock(end_value)
|
||||||
|
if not (0 <= start < end <= 86400):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
highlight = {
|
highlight = {
|
||||||
"id": max((entry["id"] for entry in self.server.highlights), default=0) + 1,
|
"id": max((entry["id"] for entry in self.server.highlights), default=0) + 1,
|
||||||
"date": date,
|
"date": date,
|
||||||
"start": start,
|
"start": start,
|
||||||
"end": end,
|
"end": end,
|
||||||
"label": str(item.get("label", "Untitled highlight"))[:120],
|
"label": str(item.get("label", item.get("title", "Untitled highlight")))[:120],
|
||||||
}
|
}
|
||||||
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||||
return self.send_error(400, "Invalid highlight")
|
return self.send_error(400, "Invalid highlight")
|
||||||
|
|||||||
Reference in New Issue
Block a user