| 1 | """Openverse provider. |
| 2 | |
| 3 | Zero-config (no API key required). Indexes openly licensed images across |
| 4 | Wikimedia, Flickr, museums, and other sources. |
| 5 | |
| 6 | API docs: https://api.openverse.org/v1/ |
| 7 | """ |
| 8 | |
| 9 | from __future__ import annotations |
| 10 | |
| 11 | import sys |
| 12 | from pathlib import Path |
| 13 | |
| 14 | _SCRIPTS_DIR = Path(__file__).resolve().parents[1] |
| 15 | if str(_SCRIPTS_DIR) not in sys.path: |
| 16 | sys.path.insert(0, str(_SCRIPTS_DIR)) |
| 17 | |
| 18 | from console_encoding import configure_utf8_stdio # noqa: E402 |
| 19 | |
| 20 | configure_utf8_stdio() |
| 21 | |
| 22 | if __name__ == "__main__": |
| 23 | print(__doc__) |
| 24 | print("Use via: python3 skills/ppt-master/scripts/image_search.py ...") |
| 25 | raise SystemExit(0 if any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]) else 1) |
| 26 | |
| 27 | import requests |
| 28 | |
| 29 | from image_sources.provider_common import ( |
| 30 | AssetCandidate, |
| 31 | ImageSearchRequest, |
| 32 | USER_AGENT, |
| 33 | build_query_progression, |
| 34 | classify_license, |
| 35 | normalize_license_name, |
| 36 | ) |
| 37 | |
| 38 | |
| 39 | API_URL = "https://api.openverse.org/v1/images/" |
| 40 | DEFAULT_PAGE_SIZE = 20 |
| 41 | DEFAULT_TIMEOUT = 30 |
| 42 | |
| 43 | # Map our orientation vocabulary to Openverse's ``aspect_ratio`` parameter. |
| 44 | _ASPECT_MAP = {"landscape": "wide", "portrait": "tall", "square": "square"} |
| 45 | |
| 46 | # Openverse license param values. ``cc0,pdm`` covers our "no-attribution" tier; |
| 47 | # adding ``by,by-sa`` opens the "attribution-required" tier. |
| 48 | _LICENSE_PARAM = { |
| 49 | "no-attribution-only": "cc0,pdm", |
| 50 | "all": "by,by-sa,cc0,pdm", |
| 51 | } |
| 52 | |
| 53 | |
| 54 | def parse_results(payload: dict) -> list[AssetCandidate]: |
| 55 | """Translate an Openverse search payload into a list of candidates.""" |
| 56 | candidates: list[AssetCandidate] = [] |
| 57 | for item in payload.get("results", []) or []: |
| 58 | license_name = (item.get("license") or "").strip() |
| 59 | license_url = (item.get("license_url") or "").strip() |
| 60 | tier = classify_license(license_name, license_url, provider="openverse") |
| 61 | if not tier: |
| 62 | continue |
| 63 | |
| 64 | download_url = (item.get("url") or item.get("thumbnail") or "").strip() |
| 65 | if not download_url: |
| 66 | continue |
| 67 | |
| 68 | candidates.append( |
| 69 | AssetCandidate( |
| 70 | provider="openverse", |
| 71 | title=(item.get("title") or "").strip() or "Untitled", |
| 72 | asset_id=str(item.get("id") or ""), |
| 73 | source_page_url=( |
| 74 | item.get("foreign_landing_url") or item.get("detail_url") or "" |
| 75 | ).strip(), |
| 76 | license_name=normalize_license_name(license_name), |
| 77 | license_url=license_url, |
| 78 | license_tier=tier, |
| 79 | width=int(item.get("width") or 0), |
| 80 | height=int(item.get("height") or 0), |
| 81 | download_url=download_url, |
| 82 | author=(item.get("creator") or "").strip(), |
| 83 | raw=item, |
| 84 | ) |
| 85 | ) |
| 86 | return candidates |
| 87 | |
| 88 | |
| 89 | def search( |
| 90 | request: ImageSearchRequest, |
| 91 | *, |
| 92 | license_tier_filter: str = "no-attribution-only", |
| 93 | page_size: int = DEFAULT_PAGE_SIZE, |
| 94 | timeout: int = DEFAULT_TIMEOUT, |
| 95 | ) -> list[AssetCandidate]: |
| 96 | """Search Openverse for candidates matching ``request``. |
| 97 | |
| 98 | ``license_tier_filter`` is one of ``"no-attribution-only"`` or ``"all"``. |
| 99 | Returns the candidates from the first non-empty query in the |
| 100 | progression — caller is responsible for picking the best one via |
| 101 | ``score_candidate``. |
| 102 | """ |
| 103 | if license_tier_filter not in _LICENSE_PARAM: |
| 104 | raise ValueError(f"unsupported license_tier_filter: {license_tier_filter!r}") |
| 105 | |
| 106 | orientation = (request.orientation or "").strip().lower() |
| 107 | |
| 108 | for query in build_query_progression(request.query): |
| 109 | params: dict[str, str | int] = { |
| 110 | "q": query, |
| 111 | "page_size": page_size, |
| 112 | "license": _LICENSE_PARAM[license_tier_filter], |
| 113 | "size": "large", |
| 114 | } |
| 115 | if orientation in _ASPECT_MAP: |
| 116 | params["aspect_ratio"] = _ASPECT_MAP[orientation] |
| 117 | |
| 118 | response = requests.get( |
| 119 | API_URL, |
| 120 | params=params, |
| 121 | headers={"User-Agent": USER_AGENT, "Accept": "application/json"}, |
| 122 | timeout=timeout, |
| 123 | ) |
| 124 | response.raise_for_status() |
| 125 | candidates = parse_results(response.json()) |
| 126 | if candidates: |
| 127 | return candidates |
| 128 | |
| 129 | return [] |
| 130 |