Files
video/tests/tools/test_stock_source_adapters.py
calesthio 54155f132f docmontage: corpus builder hardening from P1 + P2 audit observations
All of these changes came out of running the P1 and P2 documentary-montage
audits end-to-end and watching specific things break. Grouping them into
one commit because they share a theme: making the corpus builder and its
stock source adapters robust enough that a real brief can produce a
real corpus without special-casing.

corpus_builder.py + new clip_cache.py + test_clip_cache.py
  Phase 1 of the shared-corpus architecture. Adds
  ~/.openmontage/clips_cache/ — a process-safe, LRU-evicted cache of
  downloaded clip files keyed by clip_id. Before each candidate download,
  corpus_builder asks the cache whether the bytes already exist on disk
  from a previous run; on a hit it hard-links (or copies on cross-drive)
  the blob into the caller's corpus dir and skips the network fetch. On
  a miss it downloads as usual and ingests the fresh file. Motivation:
  re-running the P1 audit after every tool fix was re-downloading gigs
  of archive.org footage that had already been fetched in the prior run.
  Cache faults never block the pipeline — they degrade gracefully to
  normal downloads. The cache bubbles counters into the corpus_builder
  return payload so the production report can show hit/miss/bytes-saved.
  Default 20 GB cap, overridable via OPENMONTAGE_CACHE_MAX_GB.
  Full test coverage: try_link, ingest, stats, LRU eviction, manifest
  persistence, lock behavior. 23 tests, tmp_path-scoped.

stock_sources/archive_org.py
  1. Three-strategy query cascade (phrase_prox_10 → distinctive_and →
     distinctive_or). Motivation: natural-language documentary queries
     against archive.org Solr were zeroing out — "1950s family watching
     television" returned 0 hits because Solr's default multi-term AND
     over-intersects. Walks strict to loose and returns the first
     non-empty strategy. Stop words, source hints ("prelinger",
     "archive", "footage"), and year tokens ("1950s") are excluded from
     the distinctive-token picks since they don't correlate with
     Prelinger title tokens.
  2. 150 MB per-rendition size cap. archive.org routinely hosts
     multi-hundred-megabyte h.264 masters and one 2 GB Prelinger item
     poisons corpus build wall-time and disk. Within a format bucket
     we now pick the largest rendition under the cap; if nothing fits
     we fall through to the next format rather than dropping the item.
  3. 180 s default max-duration ceiling when the caller hasn't set one
     — archive.org is the only source that routinely hosts feature-
     length material and a naive fan-out pulls them into corpora that
     only ever want a few seconds per clip.

stock_sources/wikimedia.py
  Parallel 3-strategy cascade (full → top2_or → single_best). Motivation:
  Commons CirrusSearch also defaults to AND across multi-word queries;
  our first P2 diagnostic pass returned 0 video results for 10/10
  queries. Same stop-word / source-hint / year-token stripping as
  archive_org so the two adapters stay symmetric.

test_stock_source_adapters.py
  Rewrote the wikimedia query-builder tests against the new cascade
  API. Added coverage for multi-word fallback + source-hint/year
  stripping.

video_compose.py
  Two small fixes for the Remotion renderer on Windows:
  1. Resolve output_path to absolute before invoking the CLI so the
     binary can write wherever the caller asked regardless of cwd.
  2. Pass cwd=composer_dir to run_command so npx can find the local
     Remotion binary under node_modules/.bin. Without this, Windows
     npx returns "could not determine executable to run" because it
     resolves .bin relative to the process cwd rather than the
     script's parent.
2026-04-11 00:46:06 -07:00

73 lines
2.9 KiB
Python

from tools.video.stock_sources import all_sources
from tools.video.stock_sources.unsplash import _build_download_url, _orientation_for_unsplash
from tools.video.stock_sources.wikimedia import (
_build_search_queries,
_kind_from_mime,
_meta_value,
)
def test_stock_source_autodiscovery_includes_new_sources():
names = {source.name for source in all_sources()}
assert "wikimedia" in names
assert "unsplash" in names
def test_wikimedia_search_query_respects_kind():
# The cascade's first ("full") query should always carry the
# filetype filter for video/image kinds. "any" drops the prefix.
video_cascade = _build_search_queries("rain city", "video")
assert video_cascade[0][0] == "full"
assert video_cascade[0][1].startswith("filetype:video")
image_cascade = _build_search_queries("rain city", "image")
assert image_cascade[0][0] == "full"
assert image_cascade[0][1].startswith("filetype:image")
any_cascade = _build_search_queries("rain city", "any")
assert any_cascade[0][0] == "full"
assert any_cascade[0][1] == "rain city"
def test_wikimedia_cascade_falls_back_on_multi_word():
# Multi-word query should produce a 3-stage cascade: full, top2_or,
# single_best. Tokens are picked by length, so "television" beats
# "family" and "watching".
cascade = _build_search_queries(
"1950s family watching television", "video"
)
labels = [label for label, _ in cascade]
assert labels == ["full", "top2_or", "single_best"]
assert cascade[1][1] == "filetype:video television watching"
assert cascade[2][1] == "filetype:video television"
def test_wikimedia_cascade_strips_source_hints_and_years():
# "prelinger" is a source hint (redundant on Commons) and "1955" is
# a year — both are excluded from distinctive-token picks.
cascade = _build_search_queries(
"Prelinger 1955 housewife kitchen", "video"
)
# Full query keeps the source hint + year (first attempt is strict).
assert cascade[0][1] == "filetype:video Prelinger 1955 housewife kitchen"
# Distinctive picks do NOT include prelinger or 1955.
joined = " ".join(sq for _, sq in cascade[1:])
assert "housewife" in joined
assert "kitchen" in joined
assert "prelinger" not in joined.lower()
assert "1955" not in joined
def test_wikimedia_kind_and_metadata_helpers():
assert _kind_from_mime("video/webm", "File:foo.webm") == "video"
assert _kind_from_mime("image/jpeg", "File:foo.jpg") == "image"
assert _meta_value({"Artist": {"value": "<a href='/wiki/User:Test'>Test User</a>"}}, "Artist") == "Test User"
def test_unsplash_helpers_preserve_query_params():
assert _orientation_for_unsplash("square") == "squarish"
url = _build_download_url("https://images.unsplash.com/photo-123?ixid=abc", 1920)
assert "ixid=abc" in url
assert "w=1920" in url
assert "fm=jpg" in url