Repository navigation
Add a harness to evaluate images in Seed Search - #611
Conversation
Collects each query's top 20 Wikimedia Commons candidates for the 849 training and 295 en-gb queries, runs image search's own safety layers (deny rules, NudeNet, SigLIP2) on each, and asks Jev the text-only NSFW question from #610 and a new "would images improve this page" question. Also defines the image relevance question and the combined request production would send. Everything is cached under devdata/seed_images/. Part of #610 No functional changes to the crawler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jev_rank reaches Django models at import time, so run_jev failed with AppRegistryNotReady outside pytest. Part of #610 No functional changes to the crawler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
A response already in jev_cache, left by a killed run or sent for an identical request body, was timed at file-read speed and recorded as if it were a real call's latency. Part of #610 No functional changes to the crawler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They are offline evaluation tools for #610, not served code, so their patch coverage shouldn't gate the pull requests that add them. No functional changes to the crawler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
||
|
|
||
| def image_from_json(fields: dict) -> SourceImage: | ||
| parsed = {"categories": tuple(fields["categories"]), "uploaded_at": datetime.fromisoformat(fields["uploaded_at"])} |
There was a problem hiding this comment.
Shouldn't we add validation here? What if there's a field missing or an invalid format?
There was a problem hiding this comment.
If that was the case it would have been found when running the evaluation.
There was a problem hiding this comment.
Probably a good idea to still add simple validation here (fail-fast, defense in depth, etc.). If there's a missing or invalid field, we'd rather know sooner than later.
| "Judging only by its Wikimedia Commons file name, description and categories, how likely is it that the " | ||
| "image `image` shows nudity under `policy`?" | ||
| ) | ||
| NSFW_CRITERIA = [ |
There was a problem hiding this comment.
Most other systems (e.g. SafeSearch, models we looked at) use a three-level classification system. Are you sure we want four? Should we combine the first/lowest two?
There was a problem hiding this comment.
Reply from Claude: The four levels are the criteria for a Jev score, not output classes. Jev returns a continuous score from 0 to 3, and we apply one threshold: an image passes only if it scores below 1.0. The three-way safe/uncertain/NSFW verdict comes from the local layers, not this scale.
Keeping the two lowest levels apart is what makes the gate strict. If we merged them, "a person may appear, but nothing suggests they are unclothed" would sit at the bottom of the scale, many more pictures of people would pass under the same threshold, and we'd have to measure a new threshold. The question and criteria are also the #610 safety benchmark's, word for word, and the threshold was measured on them. So I've left it as it is.
| start = time.monotonic() | ||
| try: | ||
| result = fetch() | ||
| except ImageSourceUnavailable as e: |
There was a problem hiding this comment.
We should separately catch SourceRejectedRequest, which is a subclass of this exception, and propagate it immediately. SourceRejectedRequest is a permanent failure, not a transient one.
There was a problem hiding this comment.
Reply from Claude: Done in d90d71c. with_backoff now re-raises SourceRejectedRequest straight away instead of retrying it, with tests for both the retry path and the rejection path. The candidates already collected don't change.
| def image_text(image: SourceImage) -> str: | ||
| description = image.description[:DESCRIPTION_CHARS] | ||
| categories = " | ".join(image.categories) | ||
| return f"File name: {image.title}\nDescription: {description}\nCategories: {categories}" |
There was a problem hiding this comment.
These three fields are specified by uploaders *unknown) and should be treated as potentially unsafe data. We should tell Jev that this data is untrusted user input and cannot/should-not be treated as instructions.
There was a problem hiding this comment.
Reply from Claude: Agreed. Production should tell Jev that the file name, description and categories are untrusted uploader text and must not be followed as instructions. I haven't changed the prompt here, because every Jev score in this stack was measured with it. Changing it now would put the code out of step with the results. Instead it's in the eval doc's production recommendations (#614), next to the prompt-injection residual risk. It applies to the relevance question as well as NSFW. The new prompt's threshold must be re-checked on the benchmark, and tested separately with descriptions that address Jev.
SourceRejectedRequest subclasses ImageSourceUnavailable, so the collection retry treated a permanent refusal as a back-off and retried it ten times. The collected candidates are unchanged. No functional changes to the crawler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… production list Production must tell Jev that Commons file names, descriptions and categories are untrusted uploader text, and re-check the NSFW threshold with that prompt (raised in review on #611). The wrong-sense item now says how: give Jev's image-relevance question the top web result. The production list also carries the holistic comparison's escaping and Wikipedia-card items, and the gate recommendation links the holistic section instead of naming a PR. No functional changes to the crawler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stack
PR 1 of 7 in the #610 evaluation stack: #611 ← #612 ← #613 ← #616 ← #615 ← #617 ← #614. Merge in that order.
The base is
wikicommons_image_search, not main. That branch holdsmwmbl/imagesearch/. It is unmerged, and its PR (#607) is closed.What changed
This adds
mwmbl/rankeval/seed_images/, an offline harness for testing whether Wikimedia Commons images belong in Seed Search (Combined Search, ordered by Jev). It has four resumable steps. Each caches its output under the gitignoreddevdata/seed_images/.queries: builds two query sets, each with the web top ten it is served. train is the 849 LLM-labelled queries, in Staan's order. engb is the 295 en-gb evaluation queries, in production order (Jev composite + Staan rank). It also writes the en-gb top tens as aholistic_judgelists file for the baseline.collect: fetches the top 20 Commons candidates per query with image search's ownsearch_commons. That reuses its User-Agent, concurrency cap and back-off, and adds a retry that waits out the cooldown.local_verdicts: runs image search's own layers on every distinct candidate: the deny rules, NudeNet and SigLIP2. Thumbnails are held in memory only, as in the pipeline. It keeps the scores as well as the verdicts.run_jev: asks Jev in "separate" mode and records each request's latency:jev_imagesalso defines the image-relevance question andcombined_request, the single request production would send. It reusesjev_order's retry and request-keyed disk cache.pyproject.tomlleavesmwmbl/rankeval/seed_images/*out of coverage.Results
The harness ran end to end on all 1,144 queries. These counts and latencies are from its output in
devdata/seed_images/.The later PRs use this data. Of the 16,512 candidates, 15,846 score below 1.0 on Jev NSFW (#612).
How verified
make checkpasses.test/test_seed_images_eval.py(new, no network) andtest/test_image_search.pypass.jev_cache(a killed run, or an identical request) was timed at file-read speed. It is now recorded with a null latency.run_jevset up Django before importingjev_rank, which failed withAppRegistryNotReadyoutside pytest.mwmbl/rankeval/seed_images/*is omitted from coverage, set in this PR, because it is offline evaluation code.Decisions already made
Nothing outside
mwmbl/rankevalimports this code, so production is unchanged.Part of #610
🤖 Generated with Claude Code