"""Pexels provider. Requires ``PEXELS_API_KEY`` in the environment. Pexels's site-wide license allows commercial use without attribution, so all returned candidates are classified as ``no-attribution``. API docs: https://www.pexels.com/api/documentation/ """ from __future__ import annotations import sys from pathlib import Path _SCRIPTS_DIR = Path(__file__).resolve().parents[1] if str(_SCRIPTS_DIR) not in sys.path: sys.path.insert(0, str(_SCRIPTS_DIR)) from console_encoding import configure_utf8_stdio # noqa: E402 configure_utf8_stdio() if __name__ == "__main__": print(__doc__) print("Use via: python3 skills/ppt-master/scripts/image_search.py ...") raise SystemExit(0 if any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]) else 1) import os import requests from image_sources.provider_common import ( AssetCandidate, ImageSearchRequest, LICENSE_TIER_NO_ATTRIBUTION, USER_AGENT, build_query_progression, normalize_license_name, ) API_URL = "https://api.pexels.com/v1/search" DEFAULT_PAGE_SIZE = 20 DEFAULT_TIMEOUT = 30 _ORIENTATION_MAP = { "landscape": "landscape", "portrait": "portrait", "square": "square", } def _require_api_key() -> str: key = (os.environ.get("PEXELS_API_KEY") or "").strip() if not key: raise RuntimeError( "PEXELS_API_KEY is not set. Add it to your environment or .env file. " "Get one at https://www.pexels.com/api/" ) return key def parse_results(payload: dict) -> list[AssetCandidate]: """Translate a Pexels response into candidates.""" candidates: list[AssetCandidate] = [] for item in payload.get("photos", []) or []: src = item.get("src") or {} download_url = (src.get("original") or src.get("large2x") or src.get("large") or "").strip() if not download_url: continue candidates.append( AssetCandidate( provider="pexels", title=(item.get("alt") or "").strip() or "Pexels photo", asset_id=str(item.get("id") or ""), source_page_url=(item.get("url") or "").strip(), license_name=normalize_license_name("Pexels License"), license_url="https://www.pexels.com/license/", license_tier=LICENSE_TIER_NO_ATTRIBUTION, width=int(item.get("width") or 0), height=int(item.get("height") or 0), download_url=download_url, author=(item.get("photographer") or "").strip(), raw=item, ) ) return candidates def search( request: ImageSearchRequest, *, license_tier_filter: str = "no-attribution-only", page_size: int = DEFAULT_PAGE_SIZE, timeout: int = DEFAULT_TIMEOUT, ) -> list[AssetCandidate]: """Search Pexels for candidates. Pexels images are uniformly ``no-attribution``, so the ``"all"`` filter behaves identically to ``"no-attribution-only"``. Both are accepted to keep the dispatcher simple. """ if license_tier_filter not in {"no-attribution-only", "all"}: raise ValueError(f"unsupported license_tier_filter: {license_tier_filter!r}") api_key = _require_api_key() orientation = (request.orientation or "").strip().lower() for query in build_query_progression(request.query): params: dict[str, str | int] = { "query": query, "per_page": page_size, "size": "large", } if orientation in _ORIENTATION_MAP: params["orientation"] = _ORIENTATION_MAP[orientation] response = requests.get( API_URL, params=params, headers={ "Authorization": api_key, "User-Agent": USER_AGENT, "Accept": "application/json", }, timeout=timeout, ) response.raise_for_status() candidates = parse_results(response.json()) if candidates: return candidates return []