diff --git a/README.md b/README.md index a8e611c..6034174 100644 --- a/README.md +++ b/README.md @@ -85,23 +85,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 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 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 curl -X POST http://127.0.0.1:8765/api/highlights \ -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 -midnight, so `34000` means `09:26:40`. This makes it straightforward for an -AI to append detections, labels, or review segments programmatically. +The API stores the entry without changing any video. `start` and `end` may also +be used as seconds from midnight, and `duration` may be used instead of an end +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 diff --git a/server.py b/server.py index 06023b5..070406b 100644 --- a/server.py +++ b/server.py @@ -13,6 +13,7 @@ from urllib.parse import parse_qs, urlparse DATE_RE = re.compile(r"^\d{8}$") 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 @@ -43,6 +44,25 @@ def scan_recordings(root): 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): protocol_version = "HTTP/1.1" @@ -75,17 +95,21 @@ class PlayerHandler(BaseHTTPRequestHandler): try: length = int(self.headers.get("Content-Length", "0")) item = json.loads(self.rfile.read(length)) - date = str(item["date"]) - start = float(item["start"]) - end = float(item["end"]) - if not DATE_RE.fullmatch(date) or not (0 <= start < end <= 86400): + date = normalize_date(item["date"]) + start = parse_clock(item.get("start", item.get("start_time"))) + end_value = item.get("end", item.get("end_time")) + 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 highlight = { "id": max((entry["id"] for entry in self.server.highlights), default=0) + 1, "date": date, "start": start, "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): return self.send_error(400, "Invalid highlight")