130 lines
4.1 KiB
Python
130 lines
4.1 KiB
Python
"""Openverse provider.
|
|
|
|
Zero-config (no API key required). Indexes openly licensed images across
|
|
Wikimedia, Flickr, museums, and other sources.
|
|
|
|
API docs: https://api.openverse.org/v1/
|
|
"""
|
|
|
|
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 requests
|
|
|
|
from image_sources.provider_common import (
|
|
AssetCandidate,
|
|
ImageSearchRequest,
|
|
USER_AGENT,
|
|
build_query_progression,
|
|
classify_license,
|
|
normalize_license_name,
|
|
)
|
|
|
|
|
|
API_URL = "https://api.openverse.org/v1/images/"
|
|
DEFAULT_PAGE_SIZE = 20
|
|
DEFAULT_TIMEOUT = 30
|
|
|
|
# Map our orientation vocabulary to Openverse's ``aspect_ratio`` parameter.
|
|
_ASPECT_MAP = {"landscape": "wide", "portrait": "tall", "square": "square"}
|
|
|
|
# Openverse license param values. ``cc0,pdm`` covers our "no-attribution" tier;
|
|
# adding ``by,by-sa`` opens the "attribution-required" tier.
|
|
_LICENSE_PARAM = {
|
|
"no-attribution-only": "cc0,pdm",
|
|
"all": "by,by-sa,cc0,pdm",
|
|
}
|
|
|
|
|
|
def parse_results(payload: dict) -> list[AssetCandidate]:
|
|
"""Translate an Openverse search payload into a list of candidates."""
|
|
candidates: list[AssetCandidate] = []
|
|
for item in payload.get("results", []) or []:
|
|
license_name = (item.get("license") or "").strip()
|
|
license_url = (item.get("license_url") or "").strip()
|
|
tier = classify_license(license_name, license_url, provider="openverse")
|
|
if not tier:
|
|
continue
|
|
|
|
download_url = (item.get("url") or item.get("thumbnail") or "").strip()
|
|
if not download_url:
|
|
continue
|
|
|
|
candidates.append(
|
|
AssetCandidate(
|
|
provider="openverse",
|
|
title=(item.get("title") or "").strip() or "Untitled",
|
|
asset_id=str(item.get("id") or ""),
|
|
source_page_url=(
|
|
item.get("foreign_landing_url") or item.get("detail_url") or ""
|
|
).strip(),
|
|
license_name=normalize_license_name(license_name),
|
|
license_url=license_url,
|
|
license_tier=tier,
|
|
width=int(item.get("width") or 0),
|
|
height=int(item.get("height") or 0),
|
|
download_url=download_url,
|
|
author=(item.get("creator") 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 Openverse for candidates matching ``request``.
|
|
|
|
``license_tier_filter`` is one of ``"no-attribution-only"`` or ``"all"``.
|
|
Returns the candidates from the first non-empty query in the
|
|
progression — caller is responsible for picking the best one via
|
|
``score_candidate``.
|
|
"""
|
|
if license_tier_filter not in _LICENSE_PARAM:
|
|
raise ValueError(f"unsupported license_tier_filter: {license_tier_filter!r}")
|
|
|
|
orientation = (request.orientation or "").strip().lower()
|
|
|
|
for query in build_query_progression(request.query):
|
|
params: dict[str, str | int] = {
|
|
"q": query,
|
|
"page_size": page_size,
|
|
"license": _LICENSE_PARAM[license_tier_filter],
|
|
"size": "large",
|
|
}
|
|
if orientation in _ASPECT_MAP:
|
|
params["aspect_ratio"] = _ASPECT_MAP[orientation]
|
|
|
|
response = requests.get(
|
|
API_URL,
|
|
params=params,
|
|
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
|
|
timeout=timeout,
|
|
)
|
|
response.raise_for_status()
|
|
candidates = parse_results(response.json())
|
|
if candidates:
|
|
return candidates
|
|
|
|
return []
|