File size: 6,156 Bytes
f5eeb1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""Face routes — detect / recognize / intelligence / index endpoints."""

from __future__ import annotations

from typing import List, Optional

from fastapi import APIRouter, Depends, File, Form, UploadFile
from pydantic import BaseModel

from api.deps import (
    get_detection_service,
    get_recognition_service,
    get_face_intelligence_service,
    get_face_index_service,
)
from models.jobs import JobKind, JobRequest
from services.detection_service import DetectionService
from services.recognition_service import RecognitionService
from services.face_intelligence_service import FaceIntelligenceService
from services.face_index_service import FaceIndexService

router = APIRouter()


class FaceRequest(BaseModel):
    image_url: Optional[str] = None
    image_base64: Optional[str] = None
    providers: List[str] = []


@router.post("/detect")
async def detect_faces(req: FaceRequest, svc: DetectionService = Depends(get_detection_service)):
    job_req = JobRequest(
        kind=JobKind.DETECTION,
        image_url=req.image_url,
        image_base64=req.image_base64,
        providers=req.providers,
    )
    return await svc.detect(job_req)


@router.post("/recognize")
async def recognize_faces(req: FaceRequest, svc: RecognitionService = Depends(get_recognition_service)):
    job_req = JobRequest(
        kind=JobKind.RECOGNITION,
        image_url=req.image_url,
        image_base64=req.image_base64,
        providers=req.providers,
    )
    return await svc.recognize(job_req)


@router.post("/intelligence")
async def face_intelligence(
    req: FaceRequest,
    svc: FaceIntelligenceService = Depends(get_face_intelligence_service),
):
    """Run face intelligence: quality, blur, pose, best-face, clustering."""
    job_req = JobRequest(
        kind=JobKind.DETECTION,
        image_url=req.image_url,
        image_base64=req.image_base64,
        providers=req.providers,
    )
    return await svc.analyze(job_req)


@router.get("/gallery")
async def list_gallery(svc: RecognitionService = Depends(get_recognition_service)):
    return {"persons": svc.list_known_persons()}


@router.delete("/gallery/{name}")
async def remove_from_gallery(name: str, svc: RecognitionService = Depends(get_recognition_service)):
    return svc.remove_known_person(name)


# --------------------------------------------------------------------------- #
# Reverse Face Search (PimEyes-style indexed search)
# --------------------------------------------------------------------------- #

class EnrollURLRequest(BaseModel):
    image_url: str
    name: Optional[str] = None
    source_url: Optional[str] = None
    metadata: Optional[dict] = None


class SearchURLRequest(BaseModel):
    image_url: str
    top_k: Optional[int] = None
    threshold: Optional[float] = None


@router.post("/enroll")
async def enroll_face(
    image: UploadFile = File(...),
    name: Optional[str] = Form(None),
    source_url: Optional[str] = Form(None),
    metadata: Optional[str] = Form(None),
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """
    Enroll a face into the searchable reverse-search index.

    Upload an image (multipart/form-data) with optional name, source_url,
    and metadata (JSON string).  The largest face in the image will be
    detected, embedded (512-d ArcFace), and added to the index.
    """
    import json
    image_bytes = await image.read()
    md = json.loads(metadata) if metadata else None
    return await svc.enroll(
        image_bytes=image_bytes,
        name=name,
        source_url=source_url,
        metadata=md,
    )


@router.post("/enroll/url")
async def enroll_face_url(
    req: EnrollURLRequest,
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """Enroll a face from a URL (alternative to multipart upload)."""
    return await svc.enroll(
        image_url=req.image_url,
        name=req.name,
        source_url=req.source_url,
        metadata=req.metadata,
    )


@router.post("/search")
async def search_faces(
    image: UploadFile = File(...),
    top_k: Optional[int] = Form(None),
    threshold: Optional[float] = Form(None),
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """
    Search the index for faces matching the uploaded image.

    Returns up to `top_k` matches with similarity >= `threshold`.
    Each match includes the face_id, name, source_url, metadata,
    and similarity score (0-1, higher is better).
    """
    image_bytes = await image.read()
    return await svc.search(
        image_bytes=image_bytes,
        top_k=top_k,
        threshold=threshold,
    )


@router.post("/search/url")
async def search_faces_url(
    req: SearchURLRequest,
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """Search by image URL (alternative to multipart upload)."""
    return await svc.search(
        image_url=req.image_url,
        top_k=req.top_k,
        threshold=req.threshold,
    )


@router.get("/list")
async def list_enrolled_faces(
    limit: int = 50,
    offset: int = 0,
    name: Optional[str] = None,
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """Paginated list of enrolled faces in the index."""
    return {"faces": svc.list(limit=limit, offset=offset, name=name)}


@router.get("/{face_id}")
async def get_face(
    face_id: str,
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """Get details of a specific enrolled face."""
    face = svc.get(face_id)
    if face is None:
        return {"success": False, "error": "Face not found", "face_id": face_id}
    return {"success": True, "face": face}


@router.delete("/{face_id}")
async def delete_face(
    face_id: str,
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """Remove a face from the index."""
    deleted = svc.delete(face_id)
    return {"success": deleted, "face_id": face_id,
            "message": "Deleted" if deleted else "Not found"}


@router.get("/stats")
async def face_index_stats(
    svc: FaceIndexService = Depends(get_face_index_service),
):
    """Index statistics: total count, recent enrollments, etc."""
    return svc.stats()