illustration
illustration — find existing images to illustrate narrated video.
A façade + (future) agentic layer for cross-modal text-to-image retrieval: given narration text, retrieve fitting images from stock / open-media corpora. It is not an image generator.
Quick start (no API key needed — Openverse is the default source):
>>> import illustration
>>> hits = illustration.search("a stormy harbour at dusk", n=10)
>>> hits[0].url, hits[0].license, hits[0].cacheable
('https://...', 'by-sa', True)
The first argument is the query; everything else is keyword. search returns
a list of ImageResult (the normalized,
license-carrying result schema). Results are cached (SHA-256 content-addressed),
so an identical second call is free.
Adding a provider is open-closed — subclass
RetrievalSource and
register_source() it; the façade is untouched.
See misc/docs/design/illustration_design.md for the full design.
- class illustration.BeatSelection(*, beat_index: int, chosen: ImageResult | None, relevance: float = 0.0, coherence: float = 0.0, redundancy: float = 0.0, forced_duplicate: bool = False, n_candidates: int = 0)[source]
The chosen image (and why) for one beat in a sequence.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.Budget(max_iter: int = 3, max_search_calls: int = 8, max_caption_calls: int = 12, max_judge_calls: int = 8, accept_threshold: float = 0.62, correct_min_score: float | None = None, select_max_k: int = 3, select_rel: float = 0.9, min_score: float | None = None, max_cost: float | None = None, cost_estimator: Callable[[str, dict], float] | None = None)[source]
Hard loop bounds — the safety net, enforced in controller code (R2).
Counts and iterations are the deterministic, provider-independent caps. The optional
cost_estimator/max_costoverlay lets a caller add a money ceiling on top; with no estimator the call/iteration caps alone bound spend.- accept_threshold: float = 0.62
Rubric
overall(0-1) at/above which a judged candidate is accepted.
- correct_min_score: float | None = None
Optional absolute relevance floor below which a Correct-graded dominant candidate is escalated to the VLM judge instead of auto-accepted.
None(default) keeps the cheap fast-path; set it (on the reranker’s score scale) when a dominant-but-weak top must still be rubric-verified.
- cost_estimator: Callable[[str, dict], float] | None = None
(call_type, info) -> costestimate;call_typein {“search”, “caption”, “judge”}.
- max_cost: float | None = None
Optional money ceiling overlay. The count/iteration caps above are the strict hard bound;
max_costis a soft ceiling — the run halts once accruedest_costreaches it, so the one call already decided upon may push the total slightly pastmax_cost(effective bound ≈max_cost+ one call’s estimate).
- max_search_calls: int = 8
Cap on individual
(query, source)search requests across the whole run.
- min_score: float | None = None
Optional absolute relevance floor for
ir.selectabstention.None= relative-only (the loop’s quality bar lives in the rubric, which is 0-1).
- select_max_k: int = 3
ir.selecttuning for the grade (conservative selector).
- class illustration.Candidate(*, result: ImageResult, score: float | None = None, caption: str | None = None, rubric: RubricScore | None = None, rationale: str | None = None)[source]
One scored (and optionally inspected) candidate.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- property quality: float
the rubric overall if judged, else 0.
- Type:
A 0-1 comparable quality
- exception illustration.CurateDependencyError(missing: list[str] | None = None, *, extra: str = 'curate', purpose: str = 'agentic curation')[source]
An optional Layer-2 (agentic curation) dependency is not installed.
The message names the missing packages and the extra that provides them, so the failure is actionable (e.g.
pip install 'illustration[curate]').
- class illustration.CurationResult(*, beat: str, best: Candidate | None, accepted: bool, grade: str, reason: str, candidates: list[Candidate] = <factory>, trace: list[IterationRecord] = <factory>, spend: dict = <factory>)[source]
The outcome of curating one beat.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- exception illustration.IllustrationError[source]
Base class for every error raised by
illustration.
- class illustration.ImageResult(*, provider: str, id: str, url: str, thumbnail_url: str | None = None, width: int | None = None, height: int | None = None, title: str | None = None, description: str | None = None, tags: list[str] = <factory>, license: str | None = None, license_url: str | None = None, attribution: str | None = None, source_page_url: str | None = None, author: str | None = None, author_url: str | None = None, cacheable: bool = False, avg_color: str | None = None, query: str | None = None, score: float | None = None, raw: dict[str, ~typing.Any]=<factory>)[source]
One normalized image hit from any provider.
The first eight fields plus
cacheableare the cross-provider minimum the design guarantees; the rest are populated when a provider supplies them.- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.InspectReport(*, provider: str, id: str, mode: str, caption: str | None = None, rubric: RubricScore | None = None, rationale: str | None = None)[source]
A VLM inspection of one candidate (caption or judge mode; R2 INSPECT).
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.IterationRecord(*, iteration: int, queries: list[str], n_candidates: int, n_passed: int, grade: str, action: str, best_score: float | None = None, search_calls: int = 0, caption_calls: int = 0, judge_calls: int = 0, notes: str = '')[source]
One iteration of the loop — the run-log R2 requires for observability.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- exception illustration.MissingCredentialError(provider: str, *, env_var: str | None = None, console_url: str | None = None)[source]
A source needs an API key that could not be resolved.
The message tells the user exactly what to do; key values are never logged.
- class illustration.PrefilterReport(*, provider: str, id: str, passed: bool, reasons: list[str] = <factory>, values: dict[str, float]=<factory>)[source]
Per-candidate pre-filter verdict (the R2 PREFILTER stage record).
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.PrefilterResult(passed: list[ImageResult], reports: list[PrefilterReport])[source]
The outcome of pre-filtering a candidate set.
- property dropped: list[PrefilterReport]
Reports for the candidates that failed at least one check.
- exception illustration.ProviderError(provider: str, message: str, *, status: int | None = None)[source]
A provider’s HTTP API returned an error or an unusable response.
- exception illustration.RateLimitError(provider: str, message: str, *, status: int | None = None)[source]
A provider returned HTTP 429 (rate limit exceeded).
- exception illustration.RerankDependencyError(missing: list[str] | None = None)[source]
The optional local-rerank dependencies are not installed.
The message names the missing packages and the extra that provides them.
- class illustration.RetrievalSource(*, session: Any = None)[source]
Abstract base for a pure image-search provider.
Subclasses set the class attributes below and implement
_items()and_normalize()(and_auth_headers()if the provider needs a key).search()andraw_search()are template methods — do not override them: they enforce credential checks, canonical→native translation, pagination (capped byMAX_PAGES), and per-item normalization that skips rather than fails on a malformed item. Override a hook, not the template, so a provider can never silently lose those guarantees.- endpoint: str = ''
Search endpoint URL. Required.
- fixed_params: Mapping[str, Any] = mappingproxy({})
Constant native params sent on every request (e.g. an API mode/format).
- info: SourceInfo = SourceInfo(name='', description='', requires_key=False, homepage=None, default_cacheable=True, license_note='', rate_limit='', tags=())
Static metadata (a per-instance one is synthesized in __init__ if unset).
- max_per_page: int = 20
Hard cap on results per page this provider allows.
- min_per_page: int = 1
Floor on results per page this provider allows. Default 1 (no floor); raise it for a provider that rejects a small page (Pixabay’s documented minimum is 3), so
search(q, n=1)asks for a page the API accepts and the extra rows are trimmed by thenslice rather than 400-ing.
- name: str = ''
Registry key, e.g.
"openverse". Required.
- page_param: str = 'page'
Native name of the page-number parameter.
- param_map: Mapping[str, Any] = mappingproxy({})
Canonical→native parameter spec (see
illustration.translation). Immutable empty default so subclasses never share one mutable dict.
- per_page_param: str = 'page_size'
Native name of the results-per-page parameter.
- query_param: str = 'q'
Native name of the free-text query parameter.
- raw_search(*, api_key: str | None = None, **native_params: Any) dict[source]
Hit the endpoint with zero translation — the deepest escape hatch.
native_paramsare passed through verbatim as the provider’s own query parameters; the raw decoded JSON response is returned.
- search(query: str, *, n: int = 10, api_key: str | None = None, native_params: Mapping[str, Any] | None = None, **canonical: Any) list[ImageResult][source]
Search
queryand return up tonnormalizedImageResult.canonicalare façade-canonical filters (see the façadesearchand the design doc §2); each is translated to the provider’s native param viaparam_map, degrading gracefully where unsupported.native_paramsare raw provider-native params (the escape hatch) merged last, overriding translated ones.
- class illustration.RubricScore(*, subject: float = 0.0, action: float = 0.0, setting: float = 0.0, mood: float = 0.0, style: float = 0.0, quality: float = 0.0, overall: float = 0.0, rationale: str = '', parsed: bool = True)[source]
A pointwise VLM judgement of one candidate (R2 §4 rubric dimensions).
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- recompute_overall() RubricScore[source]
Set
overallto the mean of the six rubric dimensions.
- class illustration.SearchCache(store: MutableMapping[str, Any] | None = None)[source]
A thin read/write facade over an injectable
MutableMappingstore.Values are stored as a small JSON envelope
{schema, source, query, stored_at, results: [ImageResult.model_dump(), ...]}.- get(source: str, query: str, params: Mapping[str, Any]) list[ImageResult] | None[source]
Return cached results for the key, or
Noneon a miss.
- put(source: str, query: str, params: Mapping[str, Any], results: Iterable[ImageResult]) str[source]
Store
resultsunder the key; return the key.
- class illustration.SelectionBody(*, beat: str, beat_index: int, source: str, selected: _CandidateRef | None = None, candidates: list[_CandidateRef] = <factory>, forced_duplicate: bool = False, reason: str | None = None)[source]
The typed body of an illustration selection annotation (lacing body schema).
- model_config = {'extra': 'forbid'}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.SequenceResult(*, beats: list[str], selection: SequenceSelection)[source]
The result of curating a whole sequence of beats.
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.SequenceSelection(*, selections: list[BeatSelection] = <factory>, objective: float = 0.0, notes: list[str] = <factory>)[source]
One image chosen per beat, optimized for the cross-shot objective.
- property chosen: list[ImageResult | None]
The chosen image per beat, in order (
Nonewhere a beat was empty).
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class illustration.SourceInfo(name: str, description: str = '', requires_key: bool = False, homepage: str | None = None, default_cacheable: bool = True, license_note: str = '', rate_limit: str = '', tags: tuple[str, ...]=<factory>)[source]
Static, human-facing metadata about a source (for discovery + the gate).
- class illustration.SourcesView[source]
A live
Mappingover the registry with dict- and attribute-access.Attribute access (
sources.openverse) is a convenience; dict access (sources["openverse"]) is the canonical form and the only one that works for a source whose name collides with aMappingmethod (get,keys,values, …).
- exception illustration.UnknownSourceError(name: str, known: list[str] | None = None)[source]
A source name was requested that is not in the registry.
- illustration.blur_check(*, min_variance: float = 100.0) Callable[[ImageResult, Callable[[], Any]], CheckOutcome][source]
Reject blurry images via variance of the Laplacian (R2 §3; OpenCV-free).
The 3x3 Laplacian is applied with NumPy (no OpenCV) on the grayscale image; a low response variance means few sharp edges, i.e. blur.
- illustration.brightness_check(*, min_brightness: float = 0.1, max_brightness: float = 0.95) Callable[[ImageResult, Callable[[], Any]], CheckOutcome][source]
Reject near-black / blown-out images. Mean luminance, normalized to [0, 1].
- illustration.cache_dir(*, ensure: bool = False) Path[source]
Directory for regenerable caches (the default search-result store).
- illustration.check_requirements(provider: str, *, api_key: str | None = None) str | None[source]
Return the key for
provider, raising if a required key is missing.A provider with no entry in
PROVIDER_ENV_VARSneeds no key and returnsNone. Otherwise a missing key raisesMissingCredentialError.
- illustration.check_rerank_requirements() None[source]
Raise
RerankDependencyErrorif the rerank extra isn’t installed.
- illustration.curate(beat: str, *, sources: str | list[str] | None = None, n: int = 12, budget: Budget | None = None, expander: Callable[[str], Sequence[str]] | None = None, refiner: Callable[[str, str], str] | None = None, grader: Callable[[Sequence[ImageResult], Any], Grade] | None = None, describe: Callable[[Any, str], str] | None = None, scorer: Callable | None = None, checks: Sequence[Callable[[ImageResult, Callable[[], Any]], CheckOutcome]] | None = None, model: str | None = None, search_fn: Callable | None = None, fetch: Callable | None = None) CurationResult[source]
Curate the single best image for a narration
beatvia the CRAG loop.- Parameters:
beat – The narration beat / scene description to illustrate.
sources – Source name(s) to search, or
Nonefor the default set.n – Candidates requested per source per query (recall width).
budget – Hard loop bounds (defaults to
Budget).refiner (expander /) – Query generation / refinement seams (default:
aix).grader –
(results, selection) -> Grade(defaultscore_grade()).describe – VLM
(image, prompt) -> textseam (default:aix).scorer – SigLIP-style
(beat, results) -> scoresreranker.Noneuses the local SigLIP scorer when its deps are present, else falls back to the rank-fused order.checks – Classical-CV pre-filter checks (default: dependency-aware set).
model – LLM model id passed to the default expander / refiner / describe.
search_fn –
(query, *, source, n, ...) -> [ImageResult](default:illustration.search()). Inject a stub to test offline.fetch – Image fetch override for the pre-filters (test double).
- Returns:
A
CurationResult— the accepted (or best-so-far) candidate, the full candidate set, the per-iteration trace, and the spend accounting.
- illustration.curate_sequence(beats: Sequence[str], *, sources: str | list[str] | None = None, n: int = 12, per_beat: Callable[[str], Sequence[ImageResult]] | None = None, **select_kwargs: Any) SequenceResult[source]
Curate a whole sequence: gather a candidate pool per beat, then select.
per_beatproduces the candidate pool for one beat — a sequence ofImageResult(default: a recall + SigLIP rerank viaillustration.search(), so pools carry relevance.score). For the full per-beat CRAG loop, unwrap the loop’sCandidateenvelopes:per_beat=lambda b: [c.result for c in illustration.curate(b).candidates]. Remaining keyword args pass through toselect_sequence().
- illustration.default_search_store() MutableMapping[str, Any][source]
A
JsonFilesstore under<cache_dir>/search(created on demand).
- illustration.default_sources() list[str][source]
The default source set (config
DFLT_SOURCES), filtered to registered.Falls back to all registered sources if none of the configured defaults are present, so the façade always has something to query.
- illustration.expand_query(beat: str, *, n: int = 3, expander: Callable[[str], Sequence[str]] | None = None, model: str | None = None, include_verbatim: bool = True) list[str][source]
Expand a narration
beatinto a deduped list of image-search queries.The verbatim beat is included first by default (so the literal phrasing is never lost), followed by the expander’s suggestions.
expanderdefaults to anaix.prompt_func()-backed generator (built lazily); inject abeat -> [query, ...]callable to override it or to test offline.
- illustration.export_otio(store: Any, target: str | None = None) bytes | None[source]
Export the annotation store to OpenTimelineIO (needs
lacing[otio]).Thin passthrough to lacing’s OTIO adapter, so selections can flow into video tools. Returns the bytes when
targetis None, else writes the file.
- illustration.get_source(name: str) RetrievalSource[source]
Return the registered source named
name(raises if unknown).
- illustration.hamming_distance(a: int, b: int) int[source]
Number of differing bits between two perceptual hashes.
- illustration.inspect_candidate(query: str, result: ImageResult, *, mode: str = 'caption', describe: Callable[[Any, str], str] | None = None, model: str | None = None, max_tokens: int | None = None) InspectReport[source]
Inspect one candidate: a cheap caption (default) or a full rubric judge.
mode="caption"(the Correct-grade path) produces a one-sentence caption from the thumbnail;mode="judge"(the Ambiguous path) delegates tojudge_candidate().describedefaults to a lazyaix.describe_image()bound tomodeland capped atmax_tokens(defaultDFLT_CAPTION_MAX_TOKENS).
- illustration.judge_candidate(query: str, result: ImageResult, *, describe: Callable[[Any, str], str] | None = None, model: str | None = None, max_tokens: int | None = None) InspectReport[source]
Score one candidate against
querywith a pointwise VLM rubric.Uses the full-resolution image for accuracy.
describedefaults to a lazyaix.describe_image()capped atmax_tokens(defaultDFLT_JUDGE_MAX_TOKENS). The reply is parsed into aRubricScore(overall= mean of the six dimensions); an unparseable reply yields a neutral score flaggedparsed=Falseso the loop can treat it as ambiguous rather than wrongly accept or reject.
- illustration.license_allowlist(results: Iterable[ImageResult], *, allow: Iterable[str] | None = None) list[ImageResult][source]
Keep only results whose license is on the allowlist (R3’s license gate).
The mandatory per-file license-verification gate for commercial-adjacent use: aggregators disclaim license accuracy, so callers should gate on a known-good set. Both sides are run through
illustration.licensing.normalize_license()first, so a provider’s own spelling (cc-by-sa-4.0from Commons,Pixabay License) matches the canonical code without the allowlist having to enumerate every dialect — and without ever dropping annc/ndrestriction. Results with nolicenseare dropped (unknown == not allowed).>>> a = ImageResult(provider="p", id="1", url="u", license="cc0") >>> b = ImageResult(provider="p", id="2", url="u", license="by-nc") >>> c = ImageResult(provider="p", id="3", url="u", license=None) >>> [r.id for r in license_allowlist([a, b, c])] ['1'] >>> [r.id for r in license_allowlist([a, b, c], allow={"by-nc"})] ['2']
Provider dialects pass the same gate:
>>> w = ImageResult(provider="wikimedia", id="4", url="u", license="cc-by-sa-4.0") >>> p = ImageResult(provider="pixabay", id="5", url="u", license="Pixabay License") >>> nd = ImageResult(provider="wikimedia", id="6", url="u", license="cc-by-nd-4.0") >>> [r.id for r in license_allowlist([w, p, nd])] ['4', '5']
- illustration.make_param_translator(param_map: Mapping[str, Any], *, on_unsupported: str = 'ignore', source_name: str = '') Callable[[Mapping[str, Any]], Tuple[dict, list]][source]
Build a translator from a
param_map(see module docstring).on_unsupportedgoverns what happens when a canonical param has no native equivalent:'ignore'(drop silently, the graceful default),'warn'(drop +warnings.warn()), or'raise'(raiseValueError). Parameters whose value isNoneare skipped entirely (an unset filter).
- illustration.make_phash_hasher(*, field: str = 'thumbnail_url', fetch: Callable | None = None) Callable[[ImageResult], int | None][source]
A pHash hasher that fetches each result’s image once (cached for the pass).
Returns
result -> int | None(None when the image can’t be fetched or Pillow/NumPy aren’t installed), suitable asselect_sequence()’shasherseam. The fetch is content-deduped within the pass.
- illustration.make_siglip_scorer(*, model: str = 'google/siglip2-base-patch16-224', cache: MutableMapping[str, Any] | None = None, device: str | None = None, image_field: str = 'thumbnail_url') SiglipScorer[source]
Build a SigLIP
SiglipScorer(raises if the extra is missing).
- illustration.normalize_license(value: str | None) str | None[source]
Fold a provider’s licence spelling onto one canonical, comparable code.
Lower-cases, unifies separators to
-, strips a trailing version and a leadingcc-, then appliesLICENSE_ALIASES. ReturnsNoneforNone/blank — an absent licence is never a code.>>> normalize_license("by-sa"), normalize_license("cc0"), normalize_license("CC0 1.0") ('by-sa', 'cc0', 'cc0') >>> normalize_license("Pexels License"), normalize_license(" BY ") ('pexels-license', 'by') >>> normalize_license("public domain"), normalize_license("cc-by-3.0") ('pdm', 'by') >>> normalize_license("cc-0") # the version strip would otherwise eat "-0" 'cc0' >>> normalize_license("") is None True
- illustration.nsfw_check(*, max_prob: float = 0.5, classifier: Callable[[Any], float] | None = None) Callable[[ImageResult, Callable[[], Any]], CheckOutcome][source]
Hard-drop unsafe images (R2: NSFW is a non-negotiable drop).
classifierisimage -> nsfw_probability; it defaults to the Apache-2.0Falconsai/nsfw_image_detectionViT (torch/transformers — the[rerank]extra). Fails closed: a candidate whose image can’t be fetched or classified is dropped, never passed.
- illustration.package_version() str[source]
The installed package version, or
'0+unknown'if not installed.>>> isinstance(package_version(), str) True
- illustration.persist_sequence(result: Any, *, store: Any = None, actor: str = 'agent:illustration-curate', activity: str = 'infer', at_time: Any = None) Any[source]
Persist a
SequenceResultas lacing annotations.One
selections-tier annotation per beat (machine choices), keyed on the ordinal beat timeline. Returns the store (a freshlacing.MemoryStorewhenstoreis None).at_time(alacing.RationalTime) overrides the provenance timestamp — pass it for deterministic ordering in tests; defaults to wall-clockRationalTime.now().
- illustration.phash(image, *, hash_size: int = 8, highfreq_factor: int = 4) int[source]
A DCT perceptual hash of a PIL image, as a
hash_size**2-bit integer.The standard pHash: resize to grayscale, take the low-frequency DCT block, threshold against its median (excluding the DC term), pack into bits. Implemented with a NumPy DCT matrix so it needs only Pillow + NumPy.
- illustration.prefilter(results: Sequence[ImageResult], *, checks: Sequence[Callable[[ImageResult, Callable[[], Any]], CheckOutcome]] | None = None, field: str = 'thumbnail_url', fetch: Callable | None = None) PrefilterResult[source]
Run
checksoverresults, keeping only candidates that pass all.Checks run cheapest-first and short-circuit on the first failure, so a candidate dropped on its (free) metadata never triggers an image fetch. Images are fetched at most once per URL (in-memory, for this call only);
fetchoverrides the fetch function (a test double avoids the network).checksdefaults todefault_checks()(core CV + NSFW where its deps are available).Returns a
PrefilterResultwith the survivingpassedresults and aPrefilterReportper input candidate.
- illustration.record_override(store: Any, beat_index: int, chosen: ImageResult | None, *, beat: str | None = None, actor: str = 'user:director', reason: str | None = None, at_time: Any = None) Any[source]
Append a director override for
beat_index(a new, superseding annotation).Never mutates the machine’s annotation — the override is a fresh annotation on the same beat, with
provenance.was_derived_frompointing at the most-recent prior selection (STAM-style append-only layering). Returns the new annotation.
- illustration.refine_query(beat: str, critique: str, *, refiner: Callable[[str, str], str] | None = None, model: str | None = None) str[source]
Refine a query for
beatgiven a shortcritiqueof the last round.refinerdefaults to anaix.prompt_func()-backed refiner (lazy); inject a(beat, critique) -> querycallable to override or test offline. Falls back to the verbatim beat if the refiner errors or returns nothing.
- illustration.register_source(source: RetrievalSource, *, name: str | None = None) RetrievalSource[source]
Register a source instance under
name(defaultsource.name).Returns the source, so it can be used as
SRC = register_source(MySource()).
- illustration.render_sequence_video(selections: Any, *, saveas: str, durations: float | Sequence[float] = 4.0, narration_audio: str | None = None, fps: int = 30, style: str = 'push', output_aspect: float | None = None, image_loader: Callable[[ImageResult], Any] | None = None, render: Callable[[...], Any] | None = None) Any[source]
Render chosen images into a single Ken-Burns film via
burns.selectionsmay be aSequenceResult, aSequenceSelection, or a plain list ofImageResult. Each image gets an auto motion path (burns.ken_burns_path, alternating push/pull for rhythm) and itsdurationsslice;narration_audio(a pre-built track) is muxed in. Beats with no chosen image are skipped.Seams:
image_loaderfetches an image to a PIL image (default: the shared cached fetch —burnsdecodes PIL, not URLs);renderis the renderer (default:burns.ken_burns_film) — inject a stub to test without ffmpeg. Returns whateverrenderreturns (the output path for the default).
- illustration.requires_credentials(provider: str) Callable[source]
Decorator separating credential-checking from a function’s business logic.
Runs
check_requirements()forproviderbefore the wrapped function body, so the function never inlines key handling. (The built-in sources callcheck_requirements()directly; this decorator is the functional-style equivalent for Layer-2 helpers.)>>> @requires_credentials("pexels") ... def fetch(): return "ok" >>> with using_credentials(pexels="k"): ... fetch() 'ok'
- illustration.rerank(query: str, results: Sequence[ImageResult], *, scorer: Callable[[str, Sequence[ImageResult]], Sequence[float]] | None = None, descending: bool = True) list[ImageResult][source]
Re-score
resultsagainstqueryand return them sorted by score.Each returned
ImageResultis a copy with.scorepopulated.scorerdefaults to the SigLIP scorer (needs the[rerank]extra); inject any(query, results) -> scorescallable to use a different model or a test double. An emptyresultsreturns[]without loading a model.
- illustration.resolve_api_key(provider: str, *, api_key: str | None = None) str | None[source]
Resolve the API key for
providerby precedence, orNoneif absent.Does not raise — callers that require a key use
check_requirements(). Reads are non-interactive (never prompts).
- illustration.resolve_selection(store: Any, beat_index: int) dict | None[source]
The active selection body for a beat — the latest annotation wins.
Resolves machine choice vs. director override by provenance timestamp, so a later override supersedes the machine’s choice without deleting it.
- illustration.resolved_selections(store: Any) dict[int, dict][source]
The active selection body per beat index (resolved over all overrides).
- illustration.score_grade(results: Sequence[ImageResult], selection: Any) Grade[source]
The default, model-free grader — scale-robust via
ir.selectstructure.Maps the conservative selection’s shape (not an absolute score) onto CRAG’s grade: nothing/abstained → Incorrect; a single dominant pick → Correct; several comparable picks → Ambiguous. Because it reads relative structure, it works regardless of the reranker’s score magnitude — the absolute quality bar lives in the rubric (0-1), applied only on the Ambiguous path.
- illustration.search(query: str, *, n: int = 10, source: str | list[str] | None = None, orientation: str | None = None, size: str | None = None, safe: bool = True, license_type: str | None = None, color: str | None = None, content_type: str | None = None, license_allow: bool | Iterable[str] = False, rerank: bool | Callable = False, provider_params: Mapping[str, Mapping[str, Any]] | None = None, api_key: str | None = None, cache: bool | SearchCache = True, refresh: bool = False, **provider_kwargs: Any) list[ImageResult][source]
Search for up to
nimages matchingqueryfrom one or more sources.- Parameters:
query – The free-text query (first positional; required).
n – Number of results wanted per source (default
DFLT_N).source – A source name, list of names, or
Nonefor the default set.orientation –
landscape|portrait|square.size –
large|medium|small(minimum-size filter).safe – Exclude mature content where the provider supports it (default True).
license_type –
commercial|all-cc|modification|all(honored by providers with license filtering, e.g. Openverse).color – A named color or
#hex(Pexels, Pixabay).content_type –
photo|illustration|vector(Openverse, Pixabay; providers map/skip values they don’t support).license_allow – License gate (R3).
False(default) = no gate;True= keep only commercial-safe licenses (CC0/PD/BY/BY-SA + Pexels); an iterable of license codes = keep only those. Aggregators disclaim license accuracy, so gate when commercial use matters.rerank – Local cross-modal precision rerank (R1).
False(default) = off;True= SigLIP-2 (needs theillustration[rerank]extra); a(query, results) -> scorescallable = a custom scorer. Applied to the assembled results, which it re-scores (populating.score) and sorts. Use the recall→rerank pattern:search(q, n=50, rerank=True)[:10].provider_params – Per-source native params, e.g.
{"pexels": {"color": "blue"}}— used when fanning out to multiple sources so each gets the right native overrides.api_key – An explicit API key. Single-source only — raises if combined with multiple sources; use
using_credentials()for keyed fan-out.cache –
Trueto use the default cache,Falseto bypass, or aSearchCacheinstance to inject one.refresh – If True, ignore any cached entry and re-fetch (then re-store).
**provider_kwargs – Flat native params (escape-hatch rung 3a). Single- source only — raises if combined with multiple sources; use
provider_params={source: {...}}for fan-out.
- Returns:
for multiple sources the per-source lists are concatenated (up to
n × len(sources)) and Layer-2 adds rank fusion viair.- Return type:
A list of
ImageResult.nis per source
>>> isinstance(search.__doc__, str) True
- illustration.search_cache_key(source: str, query: str, params: Mapping[str, Any]) str[source]
Content-addressed SHA-256 key for
(source, query, params).paramsshould be the normalized request parameters (the canonical args actually sent, includingn), so two calls that differ only cosmetically share a key.
- illustration.select_sequence(per_beat_candidates: Sequence[Sequence[ImageResult]], *, relevance: Callable[[ImageResult], float] | None = None, embed: Callable[[Sequence[ImageResult]], Sequence[Any]] | None = None, hasher: Callable[[ImageResult], int | None] | None = None, shortlist: Callable[[Sequence[ImageResult]], Sequence[ImageResult]] | None = None, alpha: float = 0.3, beta: float = 0.5, phash_threshold: int = 6) SequenceSelection[source]
Choose one image per beat optimizing relevance + coherence − redundancy.
A greedy left-to-right pass: at each beat, pick the candidate maximizing
rel + α·coherence(prev) − β·max_redundancy(chosen), excluding any near-duplicate (pHash Hamming <phash_threshold) of an already-chosen image. If every candidate for a beat is a near-duplicate, the constraint is relaxed for that beat and the choice is flaggedforced_duplicate.Seams (all default to the lean in-house / M2b path, injectable for tests or upgrades):
relevance(default: candidate.scoreor 0),embed(default: cached SigLIP embeddings via[rerank]; coherence/redundancy are skipped when unavailable),hasher(default: in-house DCT pHash; dedup is skipped when Pillow/NumPy are unavailable),shortlist(optional per-beat pre-filter, e.g. anapricotsubmodular representative set).
- illustration.to_search_hit(result: ImageResult)[source]
Map an
ImageResultto anir.SearchHitfor Layer-2 fusion.The bridge into the
irretrieval substrate so the agentic layer canir.fuse_hitsacross providers.iris imported here, not at module top, to keep the base façade dependency-light. Identity follows ir’s(source, artifact_id)keying:sourceis the provider andartifact_idis the provider-native id. The image URL is placed under thepathmetadata key soSearchHit.pointer(which scansir.base.POINTER_KEYS) resolves to it; the full normalized result rides along inmetadatatoo.scoreis0.0for any hit not yet reranked (Layer 1 leavesImageResult.scoreasNone) — rely on rank, not magnitude, until a Layer-2 reranker populates it;ir.fuse_hits(RRF) is rank-based, so this is correct for fusion.
- illustration.to_walkthru_document(selections: Any, *, narration: Sequence[str] | None = None, durations: float | Sequence[float] = 4.0, doc_id: str = 'illustration-storyboard', title: str | None = None) Any[source]
Build a
walkthru.DemoDocumentfrom the selections (pure data, no render).Emits one b-roll beat per chosen image (
poster= the image URL,timingfromdurations) and, ifnarrationis given (one string per beat), a narration track anchored to each beat. The consumer then runswalkthru.realize_narration/pace_steps_to_narration/ its renderer. Beats with no chosen image are skipped. Needs the[video]extra.
- illustration.unregister_source(name: str) None[source]
Remove a source from the registry (no error if absent).
- illustration.using_credentials(**provider_keys: str)[source]
Bind per-request provider API keys for the duration of the
withblock.Falsy values are ignored (so an optional request header passes straight through). Bindings nest: an inner block overlays the outer.
>>> with using_credentials(pexels="k1"): ... with using_credentials(pexels="k2"): ... inner = resolve_api_key("pexels") ... outer = resolve_api_key("pexels") >>> inner, outer ('k2', 'k1')