Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion skills/pipelines/documentary-montage/asset-director.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ clips that fill each slot. There are two paths:

1. **Build the corpus** — fan the scene director's queries out across
all available stock sources (Pexels, Pixabay Video, Coverr, Mixkit,
Archive.org, NARA, Library of Congress, Pond5 PD, Videvo, NASA, ESA,
Archive.org, NARA, Library of Congress, Pond5 PD when
`POND5_API_KEY` is configured, Videvo, NASA, ESA,
JAXA, NOAA, Dareful, Wikimedia, Unsplash) and download/embed the candidates.
2. **Pick per slot** — run CLIP retrieval against the corpus with each
slot description and choose one winner per slot.
Expand Down
2 changes: 1 addition & 1 deletion skills/pipelines/documentary-montage/scene-director.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ based on what footage lives where:
| `archive_org` | Prelinger home movies, mid-century educational film, 1940s-1980s texture | Vintage, wry, dreamlike, anything nostalgic |
| `nara` | U.S. National Archives — WWII, Cold War, Apollo, civil rights, presidential | Historical American documentary, military, government, space race |
| `loc` | Library of Congress — early cinema, newsreels, cultural recordings | Pre-1928 public domain footage, American history, folk traditions |
| `pond5_pd` | Pond5 Public Domain — WWI/WWII, early cinema, historical speeches | Archival/vintage footage, Méliès, Edison, newsreels |
| `pond5_pd` | Pond5 Public Domain API (requires `POND5_API_KEY`) — WWI/WWII, early cinema, historical speeches | Archival/vintage footage, Méliès, Edison, newsreels |
| `videvo` | 90K+ free clips, nature, aerial, city, abstract, time-lapses | Large free library, complements Pexels with different contributors |
| `nasa` | Earth-from-orbit, astronomy, flight, scale imagery | Reverent, anything about scale, space, planet, flight |
| `esa` | European space missions, Hubble/Webb imagery, Earth observation | European space content, complements NASA for non-U.S. missions |
Expand Down
40 changes: 39 additions & 1 deletion tests/tools/test_stock_source_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ def test_unsplash_helpers_preserve_query_params():
_STILL_SWALLOWS_TRANSPORT_ERRORS = {
"archive_org": "#511 follow-up: query cascade continues past a failed strategy",
"wikimedia": "#511 follow-up: query cascade continues past a failed strategy",
"pond5_pd": "#511 follow-up: API failure falls through to the web fallback",
}


Expand Down Expand Up @@ -139,6 +138,11 @@ def __exit__(self, *_exc):
return False


class _StatusErrorResponse:
def raise_for_status(self):
raise TransportError("simulated HTTP 503")


class _EmptySoup:
"""Stand-in for `BeautifulSoup`: parses anything, finds nothing."""

Expand Down Expand Up @@ -183,6 +187,40 @@ def _adapter_names():
return [source.name for source in all_sources()]


def test_pond5_availability_requires_credentials(monkeypatch):
source = get_source("pond5_pd")

monkeypatch.delenv("POND5_API_KEY", raising=False)
assert source.is_available() is False

monkeypatch.setenv("POND5_API_KEY", "test-pond5")
assert source.is_available() is True


def test_pond5_search_sends_configured_credentials(monkeypatch):
captured = {}

def fake_get(*_args, **kwargs):
captured.update(kwargs)
return _EmptyOkResponse()

_install_fake_transport(monkeypatch, fake_get)
get_source("pond5_pd").search(
"ocean waves", SearchFilters(kind="video", per_page=5)
)

assert captured["headers"]["Authorization"] == "Bearer test-pond5"


def test_pond5_search_propagates_status_errors(monkeypatch):
_install_fake_transport(monkeypatch, lambda *_a, **_k: _StatusErrorResponse())

with pytest.raises(TransportError, match="HTTP 503"):
get_source("pond5_pd").search(
"ocean waves", SearchFilters(kind="video", per_page=5)
)


def _transport_error_params():
params = []
for name in _adapter_names():
Expand Down
48 changes: 14 additions & 34 deletions tools/video/stock_sources/pond5_pd.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@
historical and archival material: WWI/WWII, early cinema, space
launches, historical speeches, Olympic footage.

All public domain items are CC0-equivalent — free for any use, no
attribution required (though appreciated).

The adapter accesses Pond5's free public domain search which does not
require an API key. For the full commercial API, a partnership agreement
is needed, but the public domain subset is openly browsable.
The Pond5 search API requires an API key. This adapter does not scrape
the public website as an unauthenticated fallback.

What Pond5 Public Domain is good for
-------------------------------------
Expand Down Expand Up @@ -46,14 +42,13 @@ class Pond5PublicDomainSource:
display_name = "Pond5 Public Domain"
provider = "pond5"
priority = 38
install_instructions = (
"Pond5 Public Domain works without an API key for basic search. "
"Set POND5_API_KEY in .env for higher rate limits and full API access."
)
install_instructions = "Set POND5_API_KEY in .env to enable Pond5 search."
supports = {"video": True, "image": True}

def is_available(self) -> bool:
return True
import os

return bool(os.environ.get("POND5_API_KEY"))

def search(self, query: str, filters: SearchFilters) -> list[Candidate]:
import requests
Expand Down Expand Up @@ -81,18 +76,14 @@ def search(self, query: str, filters: SearchFilters) -> list[Candidate]:
if api_key:
headers["Authorization"] = f"Bearer {api_key}"

try:
r = requests.get(
_SEARCH_URL,
headers=headers,
params=params,
timeout=30,
)
r.raise_for_status()
data = r.json()
except Exception as e:
_log.warning("Pond5 PD search failed (API), trying web fallback: %s", e)
return self._search_web_fallback(query, kind, filters)
r = requests.get(
_SEARCH_URL,
headers=headers,
params=params,
timeout=30,
)
r.raise_for_status()
data = r.json()

results = data.get("results", []) or data.get("items", []) or []
return self._parse_results(results, kind, filters)
Expand Down Expand Up @@ -160,17 +151,6 @@ def _parse_results(
)
return out

def _search_web_fallback(
self, query: str, kind: str, filters: SearchFilters
) -> list[Candidate]:
"""Fallback: parse Pond5 free page HTML for public domain clips.

Used when the API endpoint is unavailable or returns errors.
Returns empty list if HTML parsing fails — does not raise.
"""
_log.info("Pond5 PD: web fallback not implemented, returning empty")
return []

def download(self, candidate: Candidate, out_path: Path) -> Path:
import requests

Expand Down
Loading