face-intel / providers /reverse /serpapi.py
Marwan
Restructure + add reverse face search (PimEyes-style)
f5eeb1c
Raw
History Blame Contribute Delete
3.4 kB
"""
SerpAPI Google Reverse Image Search provider.
Uses cores.search.http for the shared session — no duplicated
requests-session code. All hashing delegated to cores.vision.
"""
from __future__ import annotations
import io
from typing import Any
import cv2
import numpy as np
from config.settings import Settings, settings as _default_settings
from cores.search import shared_session
from pipeline.feature_extraction import PipelineOutput
from providers.base import BaseProvider, ProviderCapability
class SerpAPIProvider(BaseProvider):
name = "serpapi"
capability = ProviderCapability.REVERSE_SEARCH
UPLOAD_URL = "https://assets.serpapi.com/upload"
SEARCH_URL = "https://serpapi.com/search"
def __init__(self, settings: Settings | None = None) -> None:
super().__init__(settings=settings or _default_settings)
self._api_key = self._settings.serpapi_key
self._session = shared_session()
def is_available(self) -> bool:
return bool(self._api_key)
def _run(self, pipeline_output: PipelineOutput) -> tuple[dict, dict]:
if not self._api_key:
raise RuntimeError("SerpAPI key not configured")
img: np.ndarray = pipeline_output.image
ok, buffer = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90])
if not ok:
raise RuntimeError("Could not encode image for SerpAPI upload")
# Step 1: upload
upload_resp = self._session.post(
self.UPLOAD_URL,
files={"file": ("query.jpg", io.BytesIO(buffer.tobytes()), "image/jpeg")},
data={"serp_api_key": self._api_key},
timeout=60,
)
upload_resp.raise_for_status()
uploaded_url = upload_resp.text.strip().strip('"')
# Step 2: search
params = {
"engine": "google_reverse_image",
"image_url": uploaded_url,
"api_key": self._api_key,
}
search_resp = self._session.get(self.SEARCH_URL, params=params, timeout=60)
search_resp.raise_for_status()
data: dict[str, Any] = search_resp.json()
# Step 3: parse
max_results = self._settings.reverse_search_max_results
results: list[dict] = []
for match in data.get("image_results", [])[:max_results]:
results.append({
"image_url": match.get("image", ""),
"source_page": match.get("link", ""),
"title": match.get("title", ""),
"snippet": match.get("snippet", ""),
"thumbnail": match.get("thumbnail", ""),
})
for match in data.get("inline_images", [])[:max_results]:
results.append({
"image_url": match.get("image", ""),
"source_page": match.get("link", ""),
"title": match.get("title", ""),
"snippet": match.get("snippet", ""),
"thumbnail": match.get("thumbnail", ""),
})
raw = {
"uploaded_image_url": uploaded_url,
"total_results": len(results),
"search_parameters": params,
"search_metadata": data.get("search_metadata", {}),
}
normalized = {
"results": results,
"total": len(results),
"uploaded_image_url": uploaded_url,
}
return raw, normalized