feat: frigate-notify v2 (modular app, CLIP recognition) + clip-service
Splits the single main.py into config/database/models/routes/vision/recognition/ ollama_client/cleanup modules, adds a web UI, and introduces clip-service for CLIP (ViT-L-14) embeddings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
"""Known objects API endpoints for frigate-notify v2."""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
|
||||
from database import get_connection
|
||||
from models import KnownObjectResponse, KnownObjectUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/known-objects", tags=["known-objects"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_known_objects(
|
||||
type: Optional[str] = Query(default=None, pattern="^(person|vehicle|pet)$"),
|
||||
):
|
||||
"""List all known objects with embedding_count. Optional type filter."""
|
||||
conn = get_connection()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
ko.id, ko.name, ko.type, ko.action,
|
||||
ko.created_at, ko.updated_at,
|
||||
COUNT(e.id) AS embedding_count
|
||||
FROM known_objects ko
|
||||
LEFT JOIN embeddings e ON ko.id = e.known_object_id
|
||||
"""
|
||||
params: list = []
|
||||
|
||||
if type:
|
||||
query += " WHERE ko.type = ?"
|
||||
params.append(type)
|
||||
|
||||
query += " GROUP BY ko.id ORDER BY ko.name"
|
||||
|
||||
cursor = conn.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"type": row["type"],
|
||||
"action": row["action"],
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
"embedding_count": row["embedding_count"],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/{object_id}")
|
||||
async def get_known_object(object_id: int):
|
||||
"""Get a single known object with list of its embeddings (id, created_at only)."""
|
||||
conn = get_connection()
|
||||
|
||||
cursor = conn.execute(
|
||||
"SELECT id, name, type, action, created_at, updated_at FROM known_objects WHERE id = ?",
|
||||
(object_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Known object not found")
|
||||
|
||||
# Get embeddings list
|
||||
emb_cursor = conn.execute(
|
||||
"SELECT id, created_at FROM embeddings WHERE known_object_id = ? ORDER BY created_at",
|
||||
(object_id,),
|
||||
)
|
||||
embeddings = [
|
||||
{"id": e["id"], "created_at": e["created_at"]} for e in emb_cursor.fetchall()
|
||||
]
|
||||
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"type": row["type"],
|
||||
"action": row["action"],
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
"embeddings": embeddings,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{object_id}/thumbnails/{embedding_id}")
|
||||
async def get_embedding_thumbnail(object_id: int, embedding_id: int):
|
||||
"""Return raw JPEG thumbnail from embeddings table."""
|
||||
conn = get_connection()
|
||||
|
||||
cursor = conn.execute(
|
||||
"SELECT thumbnail FROM embeddings WHERE id = ? AND known_object_id = ?",
|
||||
(embedding_id, object_id),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Embedding not found")
|
||||
if row["thumbnail"] is None:
|
||||
raise HTTPException(status_code=404, detail="No thumbnail available")
|
||||
|
||||
return Response(content=row["thumbnail"], media_type="image/jpeg")
|
||||
|
||||
|
||||
@router.put("/{object_id}")
|
||||
async def update_known_object(object_id: int, body: KnownObjectUpdate):
|
||||
"""Update name/type/action for a known object. Sets updated_at."""
|
||||
conn = get_connection()
|
||||
|
||||
cursor = conn.execute(
|
||||
"SELECT id FROM known_objects WHERE id = ?",
|
||||
(object_id,),
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Known object not found")
|
||||
|
||||
updates = []
|
||||
params: list = []
|
||||
|
||||
if body.name is not None:
|
||||
updates.append("name = ?")
|
||||
params.append(body.name)
|
||||
if body.type is not None:
|
||||
updates.append("type = ?")
|
||||
params.append(body.type)
|
||||
if body.action is not None:
|
||||
updates.append("action = ?")
|
||||
params.append(body.action)
|
||||
|
||||
if not updates:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
|
||||
updates.append("updated_at = CURRENT_TIMESTAMP")
|
||||
params.append(object_id)
|
||||
|
||||
conn.execute(
|
||||
f"UPDATE known_objects SET {', '.join(updates)} WHERE id = ?",
|
||||
params,
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
# Return the updated object
|
||||
cursor = conn.execute(
|
||||
"SELECT id, name, type, action, created_at, updated_at FROM known_objects WHERE id = ?",
|
||||
(object_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
return KnownObjectResponse(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
type=row["type"],
|
||||
action=row["action"],
|
||||
created_at=row["created_at"],
|
||||
updated_at=row["updated_at"],
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{object_id}")
|
||||
async def delete_known_object(object_id: int):
|
||||
"""Delete a known object. Embeddings cascade delete automatically."""
|
||||
conn = get_connection()
|
||||
|
||||
cursor = conn.execute(
|
||||
"SELECT id FROM known_objects WHERE id = ?",
|
||||
(object_id,),
|
||||
)
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Known object not found")
|
||||
|
||||
conn.execute("DELETE FROM known_objects WHERE id = ?", (object_id,))
|
||||
conn.commit()
|
||||
|
||||
return {"status": "ok"}
|
||||
Reference in New Issue
Block a user