illustration-search 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,451 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const imageResultSchema: z.ZodObject<{
4
+ attribution: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
5
+ author: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
6
+ author_url: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
7
+ avg_color: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
8
+ cacheable: z.ZodDefault<z.ZodBoolean>;
9
+ description: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
10
+ height: z.ZodDefault<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
11
+ id: z.ZodString;
12
+ license: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
13
+ license_url: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
14
+ provider: z.ZodString;
15
+ query: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
16
+ raw: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodAny>>;
17
+ score: z.ZodDefault<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
18
+ source_page_url: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
19
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
20
+ thumbnail_url: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
21
+ title: z.ZodDefault<z.ZodUnion<readonly [z.ZodString, z.ZodNull]>>;
22
+ url: z.ZodString;
23
+ width: z.ZodDefault<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>;
24
+ }, z.core.$strip>;
25
+ type ImageResult = z.infer<typeof imageResultSchema>;
26
+ type ImageResultInput = z.input<typeof imageResultSchema>;
27
+
28
+ /**
29
+ * The front door: `search(query, options)`, the TS twin of `illustration.search`.
30
+ *
31
+ * Layer 1 only. Rerank, dedupe and curation (illustration's Layer 2) are model
32
+ * and numpy territory and stay on the Python side; caching is the application's
33
+ * (a browser has no content-addressed store, the server relay's side does).
34
+ *
35
+ * `media` is accepted now so the signature has room for video (illustration#33);
36
+ * `"video"` raises until video sources are registered on the Python side and
37
+ * exported.
38
+ */
39
+
40
+ type MediaType = 'image' | 'video';
41
+ interface SearchOptions {
42
+ /** Results wanted **per source** (default `CONSTANTS.defaults.n`). */
43
+ readonly n?: number;
44
+ /** A source name, a list of names, or omitted for the default set. */
45
+ readonly source?: string | readonly string[];
46
+ readonly orientation?: 'landscape' | 'portrait' | 'square' | (string & {});
47
+ readonly size?: 'large' | 'medium' | 'small' | (string & {});
48
+ /** Exclude mature content where the provider supports it (default `true`). */
49
+ readonly safe?: boolean;
50
+ /** `commercial` | `all-cc` | `modification` | `all` (providers with licence filtering). */
51
+ readonly licenseType?: string;
52
+ /** A named colour or `#hex` (Pexels, Pixabay). */
53
+ readonly color?: string;
54
+ /** `photo` | `illustration` | `vector` (providers map or skip what they lack). */
55
+ readonly contentType?: 'photo' | 'illustration' | 'vector' | (string & {});
56
+ /** The licence gate: `false` (default) = none; `true` = the default commercial-safe
57
+ * allowlist; an iterable of codes = keep only those. Aggregators disclaim licence
58
+ * accuracy, so gate when commercial use matters. */
59
+ readonly licenseAllow?: boolean | Iterable<string>;
60
+ /** Per-source native params, e.g. `{ pexels: { color: 'blue' } }` (the escape hatch). */
61
+ readonly providerParams?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
62
+ /** The caller's keys by provider name. The facade never reads storage. */
63
+ readonly credentials?: Readonly<Record<string, string | null | undefined>>;
64
+ /** `'caller'` (default): keyed providers need `credentials`. `'transport'`: the
65
+ * `fetch` you pass is a relay that adds keys itself — none required, none sent. */
66
+ readonly auth?: 'caller' | 'transport';
67
+ /** `"image"` (default). `"video"` is reserved for illustration#33. */
68
+ readonly media?: MediaType;
69
+ /** The transport seam (default `globalThis.fetch`). */
70
+ readonly fetch?: typeof globalThis.fetch;
71
+ readonly signal?: AbortSignal;
72
+ /** Sent as `Api-User-Agent`; Wikimedia etiquette asks for a descriptive one. */
73
+ readonly userAgent?: string;
74
+ }
75
+ /** Search for up to `n` images per source. Per-source lists are concatenated in
76
+ * source order (up to `n × sources`); the licence gate is applied over the whole. */
77
+ declare function search(query: string, opts?: SearchOptions): Promise<ImageResult[]>;
78
+
79
+ /**
80
+ * The declared half of a source, as exported by `illustration export-schema`.
81
+ *
82
+ * Everything a Python `RetrievalSource` subclass *declares* (endpoint, parameter
83
+ * names, paging caps, fixed params, auth style, canonical→native parameter names,
84
+ * `SourceInfo`) arrives here as data (`src/generated/sources.ts`). What it *codes*
85
+ * (`coerce` functions, `_items`, `_normalize`, a `_query_params` override) is
86
+ * ported by hand in `src/providers/` and pinned by the parity fixtures.
87
+ */
88
+ interface SourceInfo {
89
+ readonly name: string;
90
+ readonly description: string;
91
+ readonly requires_key: boolean;
92
+ readonly homepage: string | null;
93
+ readonly default_cacheable: boolean;
94
+ readonly license_note: string;
95
+ readonly rate_limit: string;
96
+ readonly tags: readonly string[];
97
+ }
98
+ /** How the key travels: a header (`Authorization: <key>`), a query param (`key=`), or not at all. */
99
+ interface AuthRecord {
100
+ readonly kind: 'none' | 'header' | 'query';
101
+ readonly name: string | null;
102
+ /** Template with `{key}` where the raw key goes. */
103
+ readonly format: string | null;
104
+ }
105
+ /** A canonical parameter's native name and guard; `coerce: true` means the Python
106
+ * side transforms the value and the provider module must do the same. */
107
+ interface ParamRecord {
108
+ readonly name: string | null;
109
+ readonly choices: readonly string[] | null;
110
+ readonly coerce: boolean;
111
+ }
112
+ interface SourceRecord {
113
+ readonly name: string;
114
+ readonly endpoint: string;
115
+ readonly query_param: string;
116
+ readonly page_param: string;
117
+ readonly per_page_param: string;
118
+ readonly max_per_page: number;
119
+ readonly min_per_page: number;
120
+ readonly fixed_params: Readonly<Record<string, unknown>>;
121
+ readonly auth: AuthRecord;
122
+ /** Keyed by canonical parameter name; `null` = explicitly unsupported (degrades). */
123
+ readonly params: Readonly<Record<string, ParamRecord | null>>;
124
+ readonly info: SourceInfo;
125
+ readonly env_var: string | null;
126
+ readonly console_url: string | null;
127
+ }
128
+
129
+ /**
130
+ * Canonical → native parameter translation, mirroring `illustration.translation`.
131
+ *
132
+ * A `paramMap` value may be a string (rename), a spec object (`name`, optional
133
+ * `choices` guard, optional `coerce`), a bare function (`coerce`, same name), or
134
+ * `null` (explicitly unsupported: the parameter degrades). A canonical key
135
+ * absent from the map is unsupported too. A `null`/`undefined` *value* is an
136
+ * unset filter and is skipped, not dropped.
137
+ */
138
+ type Coerce = (value: unknown) => unknown;
139
+ interface ParamSpec {
140
+ readonly name?: string;
141
+ readonly choices?: ReadonlySet<string> | readonly string[];
142
+ readonly coerce?: Coerce;
143
+ }
144
+ type ParamMapValue = string | ParamSpec | Coerce | null;
145
+ type ParamMap = Readonly<Record<string, ParamMapValue>>;
146
+ interface Translation {
147
+ readonly native: Record<string, unknown>;
148
+ readonly dropped: string[];
149
+ }
150
+ type ParamTranslator = (canonical: Readonly<Record<string, unknown>>) => Translation;
151
+ /** Build a translator from a `paramMap`. Unsupported parameters are dropped
152
+ * silently (Python's `on_unsupported="ignore"`, the façade's setting). */
153
+ declare function makeParamTranslator(paramMap: ParamMap): ParamTranslator;
154
+
155
+ /**
156
+ * A source = its declared record (generated) + its coded hooks (a provider module),
157
+ * and the one search template every source runs through — the TS twin of
158
+ * `illustration.base.RetrievalSource`.
159
+ *
160
+ * `searchSource` is the template method: it enforces the credential check,
161
+ * canonical→native translation, pagination capped by `max_pages`, and per-item
162
+ * normalisation that *skips* a malformed item rather than failing the search.
163
+ * Provider modules supply hooks; they never re-implement the template.
164
+ *
165
+ * Transport is one argument, `fetch` (default `globalThis.fetch`). A server relay
166
+ * is a `fetch` that rewrites the URL; nothing else changes.
167
+ */
168
+
169
+ type Json = Record<string, unknown>;
170
+ /** The coded half of a source. */
171
+ interface SourceHooks {
172
+ /** Canonical→native spec; `coerce` functions live here because they cannot be data. */
173
+ readonly paramMap: ParamMap;
174
+ /** Extract the raw result items from a decoded response. */
175
+ items(response: Json): Iterable<Json>;
176
+ /** Map one raw item to an `ImageResult`. Throw to have the item skipped. */
177
+ normalize(item: Json, query: string): ImageResult;
178
+ /** Every param that depends on the query (the query itself and paging).
179
+ * Override when the request *shape* changes with the query (Wikimedia). */
180
+ queryParams?(query: string, page: number, perPage: number): Record<string, unknown>;
181
+ /** Native pagination params for a 1-based page. Override for offset models. */
182
+ pageParams?(page: number, perPage: number): Record<string, unknown>;
183
+ }
184
+ interface RetrievalSource extends SourceHooks {
185
+ readonly name: string;
186
+ readonly record: SourceRecord;
187
+ readonly translate: ParamTranslator;
188
+ queryParams(query: string, page: number, perPage: number): Record<string, unknown>;
189
+ pageParams(page: number, perPage: number): Record<string, unknown>;
190
+ }
191
+ /** The generated record for `name`, or throw: a provider module cannot exist
192
+ * without its Python twin having been exported. */
193
+ declare function sourceRecord(name: string): SourceRecord;
194
+ /** Bind a provider module's hooks to its generated record. */
195
+ declare function defineSource(name: string, hooks: SourceHooks): RetrievalSource;
196
+ /** Fill schema defaults and validate: what the Python side's `ImageResult(...)`
197
+ * constructor does. Undefined fields take their defaults (`null`, `[]`, `{}`). */
198
+ declare function makeResult(fields: Partial<ImageResult> & Pick<ImageResult, 'provider' | 'id' | 'url'>): ImageResult;
199
+ interface SearchSourceOptions {
200
+ /** Results wanted (default `CONSTANTS.defaults.n`). */
201
+ readonly n?: number;
202
+ /** The caller's key for a keyed provider. */
203
+ readonly apiKey?: string | null;
204
+ /** Who supplies the credential. `'caller'` (default): a keyed provider needs `apiKey`
205
+ * here, and it is attached to the request. `'transport'`: the `fetch` you pass (a
206
+ * server relay) adds the key itself — no pre-flight check, nothing attached. */
207
+ readonly auth?: 'caller' | 'transport';
208
+ /** Provider-native params merged last, overriding translated ones (the escape hatch). */
209
+ readonly nativeParams?: Readonly<Record<string, unknown>> | null;
210
+ /** Canonical filters (`orientation`, `size`, `safe`, …), translated per source. */
211
+ readonly canonical?: Readonly<Record<string, unknown>>;
212
+ /** The transport seam. A relay is a `fetch` that rewrites the URL. */
213
+ readonly fetch?: typeof globalThis.fetch;
214
+ readonly signal?: AbortSignal;
215
+ /** Sent as `Api-User-Agent` (a browser cannot set `User-Agent`; Wikimedia honours this one). */
216
+ readonly userAgent?: string;
217
+ }
218
+ /** The per-page clamp: at least `min_per_page`, at most `max_per_page`, ideally `n`. */
219
+ declare function perPageFor(record: SourceRecord, n: number): number;
220
+ /** Search one source and return up to `n` normalised results. */
221
+ declare function searchSource(source: RetrievalSource, query: string, opts?: SearchSourceOptions): Promise<ImageResult[]>;
222
+
223
+ /**
224
+ * The source registry, mirroring `illustration.registry`: the open-closed seam.
225
+ *
226
+ * The four built-in sources are registered at import. Adding a provider means
227
+ * exporting it on the Python side (so its record and fixtures exist), writing its
228
+ * hooks module under `providers/`, and calling `registerSource` — the façade is
229
+ * untouched.
230
+ */
231
+
232
+ declare function registerSource(source: RetrievalSource): RetrievalSource;
233
+ declare function getSource(name: string): RetrievalSource;
234
+ /** Registered source names, in registration order. */
235
+ declare function listSources(): string[];
236
+ /** The sources a bare `search(q)` fans out to (`DFLT_SOURCES` on the Python side). */
237
+ declare function defaultSources(): string[];
238
+
239
+ /**
240
+ * Licence normalisation and the allowlist gate, mirroring `illustration.licensing`
241
+ * and `illustration.schema.license_allowlist`.
242
+ *
243
+ * The alias table is generated data, not re-typed here. The transform is the
244
+ * same three steps in the same order as Python, because the order is what makes
245
+ * `cc-0` reachable (the version strip would otherwise eat its `-0`), and the
246
+ * invariant is the same: **a restriction token is never dropped**. The Python
247
+ * side's `license_normalization_cases` are replayed in `parity.test.ts`.
248
+ */
249
+
250
+ /** The default commercial-safe allowlist (`DFLT_LICENSE_ALLOWLIST` on the Python side). */
251
+ declare const DEFAULT_LICENSE_ALLOWLIST: readonly string[];
252
+ /** Fold a provider's licence spelling onto one canonical, comparable code.
253
+ * `null` for null/blank: an absent licence is never a code. */
254
+ declare function normalizeLicense(value: string | null | undefined): string | null;
255
+ /** Keep only results whose licence is on the allowlist; unknown is not allowed.
256
+ * Both sides are normalised, so provider dialects match without enumeration. */
257
+ declare function licenseAllowlist<T extends Pick<ImageResult, 'license'>>(results: Iterable<T>, opts?: {
258
+ allow?: Iterable<string> | null;
259
+ }): T[];
260
+
261
+ /**
262
+ * The error tree, mirroring `illustration.errors`.
263
+ *
264
+ * Every error names the provider and, for a missing key, says how to supply one
265
+ * and where to get one. Key *values* never appear in a message.
266
+ */
267
+ declare class IllustrationError extends Error {
268
+ constructor(message: string);
269
+ }
270
+ declare class UnknownSourceError extends IllustrationError {
271
+ readonly source: string;
272
+ constructor(source: string, known: readonly string[]);
273
+ }
274
+ declare class MissingCredentialError extends IllustrationError {
275
+ readonly provider: string;
276
+ constructor(provider: string, opts?: {
277
+ envVar?: string | null;
278
+ consoleUrl?: string | null;
279
+ });
280
+ }
281
+ declare class ProviderError extends IllustrationError {
282
+ readonly provider: string;
283
+ readonly status: number | null;
284
+ constructor(provider: string, message: string, status?: number | null);
285
+ }
286
+ declare class RateLimitError extends ProviderError {
287
+ }
288
+
289
+ /** One record per source registered on the Python side, in registration order. */
290
+ declare const SOURCE_RECORDS: readonly SourceRecord[];
291
+
292
+ /** Rights fields, licence tables, façade defaults and credential lookups. */
293
+ declare const CONSTANTS: {
294
+ readonly canonical_params: readonly ["orientation", "size", "safe", "license_type", "color", "content_type"];
295
+ readonly default_license_allowlist: readonly ["by", "by-sa", "cc0", "pdm", "pexels-license", "pixabay-license"];
296
+ readonly defaults: {
297
+ readonly http_timeout_s: 30;
298
+ readonly max_pages: 25;
299
+ readonly n: 10;
300
+ readonly sources: readonly ["openverse"];
301
+ };
302
+ readonly license_aliases: {
303
+ readonly "cc-0": "cc0";
304
+ readonly "cc-pdm": "pdm";
305
+ readonly "cc-publicdomain": "pdm";
306
+ readonly "cc-zero": "cc0";
307
+ readonly pd: "pdm";
308
+ readonly "pdm-owner": "pdm";
309
+ readonly "public-domain": "pdm";
310
+ readonly "public-domain-mark": "pdm";
311
+ readonly publicdomain: "pdm";
312
+ readonly zero: "cc0";
313
+ };
314
+ readonly license_normalization_cases: readonly [{
315
+ readonly input: null;
316
+ readonly output: null;
317
+ }, {
318
+ readonly input: "";
319
+ readonly output: null;
320
+ }, {
321
+ readonly input: " ";
322
+ readonly output: null;
323
+ }, {
324
+ readonly input: "by-sa";
325
+ readonly output: "by-sa";
326
+ }, {
327
+ readonly input: "BY";
328
+ readonly output: "by";
329
+ }, {
330
+ readonly input: " BY ";
331
+ readonly output: "by";
332
+ }, {
333
+ readonly input: "cc0";
334
+ readonly output: "cc0";
335
+ }, {
336
+ readonly input: "CC0 1.0";
337
+ readonly output: "cc0";
338
+ }, {
339
+ readonly input: "cc-0";
340
+ readonly output: "cc0";
341
+ }, {
342
+ readonly input: "cc-zero";
343
+ readonly output: "cc0";
344
+ }, {
345
+ readonly input: "cc-by-sa-4.0";
346
+ readonly output: "by-sa";
347
+ }, {
348
+ readonly input: "CC BY-SA 4.0";
349
+ readonly output: "by-sa";
350
+ }, {
351
+ readonly input: "cc-by-nc-nd-2.0";
352
+ readonly output: "by-nc-nd";
353
+ }, {
354
+ readonly input: "cc_by_3.0";
355
+ readonly output: "by";
356
+ }, {
357
+ readonly input: "Pexels License";
358
+ readonly output: "pexels-license";
359
+ }, {
360
+ readonly input: "Pixabay License";
361
+ readonly output: "pixabay-license";
362
+ }, {
363
+ readonly input: "public domain";
364
+ readonly output: "pdm";
365
+ }, {
366
+ readonly input: "Public Domain Mark";
367
+ readonly output: "pdm";
368
+ }, {
369
+ readonly input: "pdm";
370
+ readonly output: "pdm";
371
+ }, {
372
+ readonly input: "cc-pdm";
373
+ readonly output: "pdm";
374
+ }, {
375
+ readonly input: "sampling+";
376
+ readonly output: "sampling+";
377
+ }, {
378
+ readonly input: "by-nc";
379
+ readonly output: "by-nc";
380
+ }, {
381
+ readonly input: "CC-BY-SA-v2.5";
382
+ readonly output: "by-sa";
383
+ }];
384
+ readonly provider_console_urls: {
385
+ readonly pexels: "https://www.pexels.com/api/new/";
386
+ readonly pixabay: "https://pixabay.com/api/docs/";
387
+ };
388
+ readonly provider_env_vars: {
389
+ readonly pexels: "PEXELS_API_KEY";
390
+ readonly pixabay: "PIXABAY_API_KEY";
391
+ };
392
+ readonly restriction_tokens: readonly ["nc", "nd", "sampling"];
393
+ readonly rights_fields: readonly ["license", "license_url", "attribution", "source_page_url", "author", "author_url", "cacheable"];
394
+ };
395
+
396
+ /**
397
+ * Openverse (anonymous tier, no key): 800M+ CC / public-domain images.
398
+ * Twin of `illustration/providers/openverse.py`; pinned by `schema/fixtures/openverse.expected.json`.
399
+ */
400
+ declare const openverse: RetrievalSource;
401
+
402
+ /**
403
+ * Wikimedia Commons (no key): 140M+ free media files with deep per-file metadata.
404
+ * Twin of `illustration/providers/wikimedia.py`; pinned by `schema/fixtures/wikimedia.expected.json`.
405
+ *
406
+ * Quirks carried over: `query.pages` (a list under formatversion 2) is sorted by search `index`;
407
+ * pagination is offset-based (`gsroffset`); a `Category:…` query routes to the category-members
408
+ * generator and a `File:…` query to an exact-title lookup (which *drops* `generator`, expressed
409
+ * as a `null` value the request builder removes); the `Artist` field is HTML; non-images in the
410
+ * File namespace are dropped by MIME during normalisation.
411
+ */
412
+ declare const wikimedia: RetrievalSource;
413
+
414
+ /**
415
+ * Pexels (key required, raw value in `Authorization`): curated stock photos under the Pexels License.
416
+ * Twin of `illustration/providers/pexels.py`; pinned by `schema/fixtures/pexels.expected.json`.
417
+ */
418
+ declare const pexels: RetrievalSource;
419
+
420
+ /**
421
+ * Pixabay (key required, as the `key=` query param): free commercial-use images whose licence
422
+ * permits caching and self-hosting. Twin of `illustration/providers/pixabay.py`; pinned by
423
+ * `schema/fixtures/pixabay.expected.json`.
424
+ */
425
+ declare const pixabay: RetrievalSource;
426
+
427
+ /**
428
+ * illustration-search — text-to-image retrieval over open-media corpora, in the browser.
429
+ *
430
+ * The TypeScript twin of the Python `illustration` package. The result schema,
431
+ * rights fields, licence tables and provider registry are generated from the
432
+ * Python side (`schema/` at the repo root); the provider code is ported by hand
433
+ * and pinned to it by fixtures.
434
+ *
435
+ * ```ts
436
+ * import { search } from 'illustration-search';
437
+ * const hits = await search('stormy harbour at dusk', { n: 5 }); // Openverse, no key
438
+ * const stock = await search('harbour', { source: ['pexels', 'pixabay'], // the caller's own keys
439
+ * credentials: { pexels, pixabay } });
440
+ * ```
441
+ *
442
+ * Every hit carries `license`, `license_url`, `attribution`, `source_page_url`,
443
+ * `author`, `author_url` and `cacheable` (`RIGHTS_FIELDS`). Whether a hit may be
444
+ * used, and whom to credit, is answered from those; the facade never stores bytes.
445
+ */
446
+
447
+ /** The seven fields that answer "may we ship this, and whom must we credit?". */
448
+ declare const RIGHTS_FIELDS: readonly ["license", "license_url", "attribution", "source_page_url", "author", "author_url", "cacheable"];
449
+ type RightsField = (typeof RIGHTS_FIELDS)[number];
450
+
451
+ export { type AuthRecord, CONSTANTS, DEFAULT_LICENSE_ALLOWLIST, IllustrationError, type ImageResult, type ImageResultInput, type Json, type MediaType, MissingCredentialError, type ParamMap, type ParamRecord, type ParamSpec, type ParamTranslator, ProviderError, RIGHTS_FIELDS, RateLimitError, type RetrievalSource, type RightsField, SOURCE_RECORDS, type SearchOptions, type SearchSourceOptions, type SourceHooks, type SourceInfo, type SourceRecord, type Translation, UnknownSourceError, defaultSources, defineSource, getSource, imageResultSchema, licenseAllowlist, listSources, makeParamTranslator, makeResult, normalizeLicense, openverse, perPageFor, pexels, pixabay, registerSource, search, searchSource, sourceRecord, wikimedia };