返回 ppt-master
image-searcher.md
根目录 / skills / ppt-master / references / image-searcher.md
1 > See [`image-base.md`](./image-base.md) for the common framework. Technical SVG/PPT constraints are in [`shared-standards-core.md`](./shared-standards-core.md).
2
3 # Image_Searcher Reference Manual
4
5 Role definition for the **web image acquisition path**: translate the active resource owner's intent into keyword queries, search openly-licensed providers, download a license-cleared image into `project/images/`, and record provenance + license metadata into `image_sources.json`.
6
7 **Trigger**: the Default Generate resource list contains `Acquire Via: web`, or Quick Generate has resolved a required web image in active context. Load only when at least one such resource exists.
8
9 ---
10
11 ## 1. License Tier Discipline
12
13 Every **provider-sourced** image is classified into one of two tiers; anything else is rejected outright. A third tier, `manual`, exists **only** for a user-supplied [`--from-url`](#5-running-image_searchpy) replacement — it is never the result of a provider search accepting an unknown license.
14
15 | Tier | Licenses | On-slide attribution |
16 |---|---|---|
17 | `no-attribution` | CC0, Public Domain, Pexels License, Pixabay Content License | None |
18 | `attribution-required` | CC BY, CC BY-SA | Inline credit `<text>` on the slide |
19 | `manual` | User-supplied via `--from-url` (license unverified) | None — verifying rights / any credit is the user's responsibility |
20
21 **Forbidden — auto-rejected licenses**:
22
23 - CC BY-NC, CC BY-NC-SA (non-commercial)
24 - CC BY-ND, CC BY-NC-ND (no derivatives)
25 - All Rights Reserved
26 - Unknown / missing license
27
28 > `license_tier` is the central abstraction. Downstream consumers (Executor) read this single field and never interpret raw license strings.
29
30 ---
31
32 ## 2. Search Strategy
33
34 Default: quality-first across all allowed license tiers. Do not prefer CC0 / Public Domain over a better CC BY / CC BY-SA image; rely on the manifest's `license_tier` so Executor can add attribution only when needed.
35
36 ```
37 Default: provider chain, license filter = cc0,pdm,pexels,pixabay,cc by,cc by-sa
38 → rank candidates across providers; first downloadable ranked hit wins.
39 Strict: provider chain, license filter = cc0,pdm,pexels,pixabay
40 → fail if no no-attribution image can be downloaded.
41 ```
42
43 `--strict-no-attribution` is opt-in. Use it only when the deck cannot tolerate any on-slide credit (corporate template, full-bleed hero).
44
45 ---
46
47 ## 3. Providers
48
49 | Provider | Config | Strength |
50 |---|---|---|
51 | Pexels | recommended: `PEXELS_API_KEY` (free, [signup](https://www.pexels.com/api/)) | modern stock photography, people, workplace, lifestyle |
52 | Pixabay | recommended: `PIXABAY_API_KEY` (free, [signup](https://pixabay.com/api/docs/)) | broad type coverage including photos and illustrations |
53 | Openverse | zero-config | fallback aggregator: Wikimedia + Flickr + museums + rawpixel |
54 | Wikimedia Commons | zero-config | educational, scientific, geographic, historical |
55
56 Default chain (when `--provider` is unset):
57
58 `pexels` (when keyed) → `pixabay` (when keyed) → `openverse` → `wikimedia`.
59
60 Keyed providers without an API key are silently skipped — not an error.
61
62 **Default — keyed providers for broader stock coverage (may override when zero-config sources fit)**: Configure Pexels or Pixabay when their stock-photo coverage serves the brief. Their absence is not a validation failure; Openverse and Wikimedia remain valid zero-config acquisition paths.
63
64 ---
65
66 ## 4. Intent → Query Translation
67
68 Keep two layers distinct:
69
70 | Layer | Owner and grammar |
71 |---|---|
72 | Default Generate `design_spec.md §VIII Reference` | Strategist's complete visual intent: exact subject, desired view/mood, focal or quiet region, and crop-safety constraints. Positive quality cues are valid here. |
73 | Quick Generate active `Reference` | Current main agent's active-context intent after honoring explicit user assets, URLs, subjects, and constraints; unspecified choices are resolved automatically without confirmation. |
74 | `image_queries.json.items[].query` / positional query | Image_Searcher's concrete entity/identity keyword string. Start with the shortest phrase that preserves identity; keep exact multi-word names and necessary disambiguators even when they exceed four words. Omit mood, quality, composition, HEX, and negative wording. |
75
76 Web APIs match metadata, not semantic intent. Providers try the original query first, then progressively simplified four/three/two/one-word variants. A pipeline manifest should therefore use a concise query without pre-truncating exact names. For Chinese landmarks, use the precise Chinese name with Wikimedia; for stock providers, use compact English identity terms when they retain the subject.
77
78 Image_Searcher consumes the active Reference and never rewrites its owner. In Default Generate, that means no rewrite of `design_spec.md` or `spec_lock.md`; in Quick Generate, the active-context Reference remains fixed for the run. A candidate either satisfies that existing subject/focal/crop intent, or the role tries materially different query/provider/permitted-license strategies until no untried strategy remains, then marks `Needs-Manual`. Never loosen `required_terms`, the license policy, or the active intent to manufacture a match.
79
80 When the subject is an exact entity (landmark / person / company / product / venue), write `required_terms` at the same time you write the row's `query`. Use one required group per identity anchor and `|` for aliases / translations, e.g. `["Chongqing|重庆", "Jiefangbei|解放碑|Liberation Monument"]`. This keeps the query short for provider search while preventing metadata-ranked wrong entities from being accepted.
81
82 Do **not** loosen `required_terms` to generic category words just to improve coverage. Terms like `canyon`, `grand canyon`, `stone pillar`, `ground fissure`, `ancient town`, `bridge`, `temple`, or `village` belong in the search query, not as the only identity gate. For small / Chinese-local attractions, the correct failure mode is `Needs-Manual` or a user-provided `--from-url`, not a visually plausible image of the wrong place.
83
84 **Forbidden — web negative prompts**: `not tourist snapshot`, `no amateur photo`, `avoid low quality`.
85
86 > Note: Keyword APIs search negative words literally.
87
88 | §VIII Reference (intent) | Provider query |
89 |---|---|
90 | "Offshore wind farm at dusk, aerial view, quiet sky on the left for safe crop" | `offshore wind farm` |
91 | "Diverse engineering team around a laptop, modern office, natural light" | `engineering team laptop` |
92 | "Chongqing Jiefangbei monument, full structure visible, landscape frame" | `Chongqing Jiefangbei monument` |
93
94 ---
95
96 ## 5. Running `image_search.py`
97
98 ```bash
99 python3 scripts/image_search.py "<query>" \
100 --filename <name>.jpg \
101 --slide <slide_id> \
102 --orientation landscape \
103 --purpose background \
104 -o <project_path>/images
105 ```
106
107 | Parameter | Required | Default | Description |
108 |---|---|---|---|
109 | `query` | yes | — | Positional. Pre-simplification not necessary; CLI runs `simplify_query` internally. |
110 | `--filename` | yes | — | Output filename matching the resource list |
111 | `-o / --output` | no | `.` | Output directory; manifest defaults to `<output>/image_sources.json` |
112 | `--slide` | no | `""` | Slide ID from resource list (recorded in manifest) |
113 | `--purpose` | no | `""` | `background` / `hero` / `side` / `accent` |
114 | `--orientation` | no | `any` | `any` / `landscape` / `portrait` / `square` |
115 | `--min-width / --min-height` | no | `1200 / 800` | Actual downloaded-pixel floors; `--from-url` honors explicit lower overrides |
116 | `--provider` | no | (chain) | Pin one provider |
117 | `--strict-no-attribution` | no | off | Restrict to no-attribution licenses; refuse CC BY / CC BY-SA |
118 | `--require-terms` | no | — | Entity-safety gate for exact subjects. Repeatable; comma separates required groups; `A|B` means aliases within one group. Example: `--require-terms Chongqing --require-terms "Jiefangbei|Liberation Monument"` |
119 | `--manifest` | no | (default) | Override manifest path |
120 | `--save-candidates` | no | off | Escalation only: also keep a review pool in `candidates/<stem>/`. Default downloads just the best match (+ a review copy) |
121 | `--max-candidates` | no | `4` | Pool size when `--save-candidates` is set |
122 | `--promote` | no | — | Human-selected candidate override; low resolution warns but does not block promotion |
123 | `--from-url` | no | — | Manual replace: download a user-supplied image URL into `--filename` (recorded `license_tier: manual`); works without a multimodal model |
124
125 ### Batch mode (≥ 2 web rows) — preferred
126
127 When more than one row is `Acquire Via: web`, do **not** call the CLI once per row. Write all rows into one `image_queries.json` and run a single concurrent batch — the web sister of `image_gen.py --manifest`:
128
129 ```bash
130 python3 scripts/image_search.py --batch <project_path>/images/image_queries.json \
131 -o <project_path>/images
132 ```
133
134 `image_queries.json` schema (one item per web row):
135
136 ```json
137 {
138 "items": [
139 {
140 "filename": "jiefangbei.jpg",
141 "query": "Jiefangbei Chongqing downtown monument",
142 "slide": "03_landmark",
143 "purpose": "exact landmark photo",
144 "orientation": "landscape",
145 "required_terms": ["Chongqing", "Jiefangbei|Liberation Monument"],
146 "status": "Pending"
147 }
148 ]
149 }
150 ```
151
152 Required per item: `filename`, `query`, `status` (`Pending`). Optional per-item overrides: `slide`, `purpose`, `orientation`, `provider`, `strict_no_attribution`, `min_width`, `min_height`, `required_terms`.
153
154 Use `required_terms` for **exact-entity images**: landmarks, people, companies, products, venues, named artworks, and named institutions. Each list item is required; alternatives inside one item use `|`. Example for a Chongqing landmark: `["Chongqing|重庆", "Jiefangbei|解放碑|Liberation Monument"]`. This is a metadata gate: candidates whose title / author / source URL do not satisfy every group are rejected before ranking, so a visually polished but wrong Rome / Hoi An image cannot win a Chongqing landmark row. Do **not** use `required_terms` for generic mood / background rows such as "modern city skyline" or "team collaboration".
155
156 For less-covered local attractions, keep the strict identity gate rather than progressively deleting location anchors or replacing proper names with category words. If strict metadata cannot prove the entity, mark the row `Needs-Manual` and use the manual URL path when the user supplies a confirmed source.
157
158 The runner first revalidates every `Sourced` row against its readable file, requested dimensions, and `image_sources.json` entry; drift returns that row to `Failed`. It then searches all `Pending` / `Failed` rows concurrently, appends each success to the provenance manifest, and writes status back into `image_queries.json`: `Sourced` on success, retryable `Failed` on provider/download errors, and terminal `Needs-Manual` only after a clean provider/stage exhaustion. Status is saved after each completion. A single `web` row may still use single-query mode above.
159
160 **Pacing**: free providers (Wikimedia/Openverse) are rate-sensitive, so batch concurrency defaults to a modest **3** (`--concurrency N`, or `IMAGE_SEARCH_CONCURRENCY` env). Use `--concurrency 1` to restore strict one-at-a-time pacing. Single-query mode is one request at a time by nature.
161
162 ### Ranking model
163
164 `image_search.py` ranks provider metadata, not pixels. The order is deliberately conservative:
165
166 1. hard reject: invalid license, zero query relevance, or any missing `required_terms`;
167 2. identity priority: every required term group must match; candidates whose title also contains the required entity terms get an additional boost over candidates that only match via URL;
168 3. query relevance: concrete query tokens dominate generic visual words like "photo", "high quality", "background";
169 4. layout fit: requested orientation helps; mismatched orientation is a small penalty, not a hard reject;
170 5. license / size tie-breakers: no-attribution is a small bonus; pixel count is capped so a huge but weakly relevant image cannot outrank a smaller accurate image.
171
172 Do not tune this into a visual taste engine. The scorer prevents obvious metadata failures and produces a reviewable best match; the `.review` copy still decides whether the image is visually fit for the slide.
173
174 ### Suitability review — with or without a multimodal model
175
176 A metadata-ranked top hit is *downloadable and token-relevant*, not necessarily *visually suitable* — `score_candidate` never sees pixels. Review it against the active Reference and Crop Policy before it is trusted:
177
178 - **Multimodal model**: each download writes a downscaled review copy to `images/.review/<stem>.jpg` (the placed asset stays full-resolution). Judge subject identity, intended mood/view, focal or quiet region, and whether the active crop policy remains safe.
179 - **Non-multimodal model (no vision)**: do **not** pretend to confirm. Default Generate hands off via each `source_page_url`. Quick Generate does not open an interaction; mark a required image `Needs-Manual` when visual suitability cannot be established, preserve provenance, and let the quick export gate block.
180
181 For exact-entity rows, suitability has two gates: `required_terms` first enforces metadata identity, then the `.review` image confirms the pixels actually show the right subject and satisfy the active focal/crop intent. Passing metadata never authorizes changing that intent downstream.
182
183 Never treat a generic `required_terms` pass as acceptance. For example, matching `Ground Fissure` can return an unrelated transit station named Yunlong, and matching `stone pillar` can return a different scenic area. If the proper name / geography cannot be retained, stop at `Needs-Manual`.
184
185 **Replacement ladder when a best match is not right** (any reviewer):
186
187 1. refine the query and re-run that row while each revision tests a materially different identity phrase or disambiguator; do not repeat a semantically exhausted query;
188 2. **manual URL replace (universal, model-agnostic)** — use a user-supplied URL and swap it in:
189 ```bash
190 python3 scripts/image_search.py --from-url <image-url> --filename <name>.jpg -o <project_path>/images
191 ```
192 Recorded with `license_tier: manual` — verifying usage rights is the user's
193 call. In Quick Generate, use this step only when the URL was already
194 supplied; never pause to request one. The command updates the image and
195 `image_sources.json` but does **not** rewrite `image_queries.json`. Validate
196 the downloaded file and matching manual-provenance entry, then reconcile
197 that query row and the active roster to `Sourced` before export; a stale
198 `Needs-Manual` status remains blocking
199 ([`executor-web-image.md`](./executor-web-image.md) §1);
200 3. (opt-in) `--save-candidates` to pull auto-alternatives with their own `source_page_url`s, then `--promote` the best (below);
201 4. when the query variants, configured provider chain, and permitted license stages are exhausted and no user-confirmed manual URL is available, mark the row `Needs-Manual`.
202
203 Web search is far cheaper than AI generation, so this review pass is well worth it.
204
205 **This review never opens an acquisition-time interaction** ([`image-base.md`](./image-base.md) §6). Default Generate may build a placeholder and continue to Step 6. Quick Generate finishes all permitted automated strategies, records `Needs-Manual`, and blocks direct export when the unresolved image is required.
206
207 ### Manual review candidates (escalation, opt-in)
208
209 Candidate-pool saving is **off by default** — reach for it only when a best match fails confirmation, on a subjective topic, or for a prominent image (cover / chapter divider / `hero_page` / photo-led page).
210
211 ```bash
212 python3 scripts/image_search.py "<query>" --filename <name>.jpg -o <project_path>/images \
213 --save-candidates --max-candidates 4
214 ```
215
216 Saves the top candidates to `images/candidates/<stem>/` with a `candidates.json` manifest and one downscaled review copy per candidate under `candidates/<stem>/review/`. **Read the candidate review copies**, pick the best fit, then promote it — the full-resolution original is copied to the target filename:
217
218 ```bash
219 python3 scripts/image_search.py --promote candidate_03.jpg --filename <name>.jpg -o <project_path>/images
220 ```
221
222 ---
223
224 ## 6. Manifest Format (`image_sources.json`)
225
226 Every successful download appends or replaces one entry keyed on `filename`:
227
228 ```json
229 {
230 "license_verification": "provider metadata used; manual review recommended for external delivery",
231 "generated_at": "2026-05-01T12:17:59.856275Z",
232 "items": [
233 {
234 "filename": "team.jpg",
235 "slide": "03_team",
236 "purpose": "Leadership photo",
237 "search_query": "executive boardroom meeting",
238 "orientation": "landscape",
239 "provider": "openverse",
240 "stage": "all",
241 "title": "Untitled",
242 "author": "",
243 "source_page_url": "https://www.rawpixel.com/...",
244 "download_url": "https://...",
245 "license_name": "CC0",
246 "license_url": "https://creativecommons.org/publicdomain/zero/1.0/",
247 "license_tier": "no-attribution",
248 "attribution_required": false,
249 "width": 1024,
250 "height": 683,
251 "metadata_dimensions": {
252 "width": 4800,
253 "height": 3200,
254 "note": "upstream-reported size; actual downloaded file is smaller (likely a preview)"
255 },
256 "attribution_text": "team.jpg — \"Untitled\" via Openverse — license: CC0 (...)",
257 "status": "sourced"
258 }
259 ]
260 }
261 ```
262
263 | Field | Notes |
264 |---|---|
265 | `width` / `height` | Measured from the file actually saved to disk. Use these for layout. |
266 | `metadata_dimensions` | Present only when upstream-claimed size differs from the saved file (preview vs original). Informational only. |
267 | `license_tier` | Drives Executor's attribution decision: `no-attribution` / `attribution-required` for provider-sourced images, or `manual` for a user-supplied `--from-url` replacement (embed only; rights/credit are the user's responsibility). |
268 | `attribution_required` | Boolean alias of `license_tier == "attribution-required"`. |
269 | `attribution_text` | Canonical credit source. Preserve its author/provider/license facts; compress only through §7's visual grammar rather than inventing or dropping identity. |
270 | `stage` | `all` by default, or `no-attribution-only` when strict mode is used. |
271
272 > Manifest is **idempotent on `filename`** and written atomically. Rerunning replaces that entry while preserving all others. An existing unreadable/non-object manifest blocks the write instead of being overwritten as fresh state.
273
274 ---
275
276 ## 7. On-Slide Attribution Contract
277
278 Applied by Executor when an image's `license_tier == "attribution-required"`.
279
280 **Hard rule — legal content and binding**: Every slide that uses the asset carries a visible, readable credit bound unambiguously to that asset. Preserve its author, source/provider, and CC BY / CC BY-SA license facts from `attribution_text`; do not invent, merge away, or drop identity.
281
282 **Reference — visual treatment is not a constraint**: Position, size, color, line structure, per-image versus combined credits, labels, and contrast treatment belong to the page composition. Use any treatment that stays readable and preserves the asset-to-credit binding; a scrim or gradient is optional, not required.
283
284 **Reference — attribution treatments, not constraints**:
285
286 | Page situation | Possible treatment |
287 |---|---|
288 | One credited image | Place a compact credit near the image edge or in a page footnote area |
289 | Several credited images | Use per-image credits or one combined source line with labels when needed for unambiguous mapping |
290 | Hero / full-bleed image | Place the credit in an available quiet region; add a scrim or gradient only when contrast otherwise fails |
291
292 Use `attribution_text` from the manifest as the **starting point**. Compress when the chosen page treatment needs a shorter line, without dropping the required facts:
293
294 | Manifest | Slide credit |
295 |---|---|
296 | `team.jpg — "Untitled" via Openverse — license: CC0 (...)` | `via Openverse / CC0` |
297 | `team.jpg — "Sunset" by Jane Doe via Wikimedia Commons — license: CC BY-SA 4.0 (...)` | `© Jane Doe / Wikimedia / CC BY-SA 4.0` |
298
299 ---
300
301 ## 8. Failure Handling (web-specific)
302
303 Extends [`image-base.md`](./image-base.md) §6.
304
305 | Situation | Behavior |
306 |---|---|
307 | No candidates from any provider in either stage | Mark row `Needs-Manual`. Suggest a more precise query or another configured provider; rerun without `--strict-no-attribution` only when the confirmed page may carry visible credit. |
308 | Single candidate fails to download (HTTP 403/404) | Dispatcher auto-falls through to the next ranked candidate. No user action. |
309 | All candidates from one provider fail | Dispatcher moves to the next provider in the chain. |
310 | Provider/network failure remains after dispatch | Mark row `Failed`; a later batch run retries it. |
311 | Keyed provider has no API key | Silently skipped. Not an error. |
312
313 CLI exit: `0` when all attempted rows resolve; `1` while any row remains `Failed` or `Needs-Manual`.
314
315 ---
316
317 ## 9. Handoff with the Intent Owner
318
319 Reference field is **intent description**, not a query. See [`image-base.md`](./image-base.md) §8 for the rule.
320
321 Keep it intact as the acceptance contract. In Default Generate the owner is Strategist; in Quick Generate it is the current main agent's active-context resource decision. Derive a separate concise provider query that preserves exact names and necessary disambiguation; do not pass the Reference verbatim or rewrite it after search.
322
323 ---
324
325 ## 10. Handoff with Executor
326
327 Executor reads `image_sources.json` per slide that uses a Sourced image. For each entry:
328
329 | `license_tier` | Slide-level action |
330 |---|---|
331 | `no-attribution` | Embed `<image>` only |
332 | `attribution-required` | Embed `<image>` **and** an inline credit element per §7 |
333 | `manual` | Embed `<image>` only — user-supplied URL (`--from-url`); verifying usage rights / any required credit is the user's responsibility |
334
335 Executor does not interpret raw license strings — `license_tier` is sufficient.
336
337 `svg_quality_checker.py` verifies this handoff before post-processing: a referenced attribution-required image needs its own visible author + CC BY / CC BY-SA credit; one generic deck-level CC token cannot satisfy several images.
338
339 ---
340
341 ## 11. Task Completion Checkpoint
342
343 In addition to the shared checkpoint in [`image-base.md`](./image-base.md) §10:
344
345 - [ ] Every web row has a downloaded file at `project/images/<filename>` OR is marked `Needs-Manual`
346 - [ ] Each `Sourced` web image was reviewed against the active Reference/Crop Policy — a multimodal model via `images/.review/<stem>.jpg`; without vision, Default Generate hands off via `source_page_url` while Quick Generate records `Needs-Manual` without interaction. A mismatch was re-queried, replaced, escalated, or marked `Needs-Manual`, never repaired by rewriting the active intent
347 - [ ] Each `Sourced` row has a manifest entry with valid `license_tier` and non-empty `attribution_text` (except `manual` `--from-url` rows, which carry no `attribution_text`)
348 - [ ] Any `attribution-required` image has visible author + license credit in every SVG that references it
349 - [ ] `metadata_dimensions` warnings surfaced when downloaded preview is much smaller than upstream-claimed size
350 - [ ] `Needs-Manual` rows include the failure reason
351
351 lines MARKDOWN