Add checkup review flags, duplicate detection, and corrected painting images.

Introduce checkup_checked/checkup_fixed on paintings with Checkup page filters and API. Fixed paintings auto-mark as reviewed. Add find-duplicates tooling and document debug/checkup workflow. Include Botticelli and Michelangelo image fixes from checkup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-06-20 20:45:55 +03:00
1 parent 4683089495
commit 4c6acd5a3a
55 files changed
+750 -39

No files matched your search

+111
View File
@@ -204,6 +204,112 @@ Returns the image bytes with `Cache-Control: public, max-age=86400`, or `404` if
---
## Developer image audit
Routes for the **Checkup** page and **Debug mode** on painting detail. Register `GET /api/paintings/checkup` **before** `GET /api/paintings/:id` so `"checkup"` is not parsed as a painting id.
### `GET /api/paintings/checkup`
Full-catalog audit table for the Checkup UI.
**Response**
```json
{
"paintings": [
{
"id": 36,
"title": "Trinity",
"artist_name": "Andrei Rublev",
"year": 1411,
"gallery_file": "paintings/Andrei_Rublev_Trinity.jpg",
"gallery_preview": "paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg",
"detail_file": "paintings/Andrei_Rublev_Trinity.jpg",
"detail_preview": "paintings/thumbs/Andrei_Rublev_Trinity_thumb.jpg",
"detail_on_demand": false,
"gallery_file_exists": true,
"detail_file_exists": true,
"checked": false,
"fixed": false
}
],
"total": 1201
}
```
| Field | Meaning |
|-------|---------|
| `checked` | Reviewed in checkup workflow (`checkup_checked`) |
| `fixed` | Image replaced via Fix (`checkup_fixed`) |
| `detail_on_demand` | `true` when detail would use `GET /api/paintings/:id/image?size=full` |
| `gallery_file_exists` / `detail_file_exists` | Disk check under `IMAGE_DIR` |
---
### `PATCH /api/paintings/:id/checkup-flags`
Update review flags. Body: `{ "checked"?: boolean, "fixed"?: boolean }` — at least one field required.
Setting `fixed: true` also sets `checked: true`. While `fixed` is true, the API rejects clearing `checked`.
**Response**
```json
{ "checked": true, "fixed": false }
```
---
### `GET /api/paintings/:id/debug-image-search`
Google-family image search for debug / checkup (Custom Search → Google Arts & Culture → scrape → DuckDuckGo). Used by **Search visible** on the Checkup page and the debug panel on painting detail.
**Response**
```json
{
"query": "Andrei Rublev Trinity painting",
"imageUrl": "https://…",
"source": "google-arts",
"pageUrl": "https://…"
}
```
---
### `POST /api/paintings/:id/fix-image`
Download a remote URL and replace the painting’s local full image + thumbnail. Sets `checkup_fixed = true` and `checkup_checked = true`.
**Body**
```json
{ "imageUrl": "https://…" }
```
**Response**
```json
{
"imagePath": "paintings/Artist_Title.jpg",
"thumbnailPath": "paintings/thumbs/Artist_Title_thumb.jpg",
"fixed": true,
"checked": true
}
```
---
### `GET /api/debug/image-proxy`
Proxy a remote image URL for debug preview (avoids hotlink / CORS blocks in the browser).
**Query:** `url` — must be `http://` or `https://`
Returns image bytes with appropriate `Content-Type`.
---
## Frontend helpers
The React client wraps these endpoints in `client/src/api/client.ts`:
@@ -220,3 +326,8 @@ The React client wraps these endpoints in `client/src/api/client.ts`:
| `imageUrl(path)` | `/images/<path>` or placeholder |
| `galleryImageUrl(painting)` | Local thumb/full only (3D) |
| `paintingImageUrl(painting)` | Local file or on-demand API |
| `api.getPaintingCheckup()` | `GET /api/paintings/checkup` |
| `api.updatePaintingCheckupFlags(id, flags)` | `PATCH /api/paintings/:id/checkup-flags` |
| `api.getPaintingDebugImageSearch(id)` | `GET /api/paintings/:id/debug-image-search` |
| `api.fixPaintingImage(id, imageUrl)` | `POST /api/paintings/:id/fix-image` |
| `debugImageProxyUrl(imageUrl)` | `GET /api/debug/image-proxy?url=…` |
+7 -1
View File
@@ -98,6 +98,12 @@ Phases within an artist’s career (e.g. “Blue Period”, “Roman Period”).
| `thumbnail_path` | VARCHAR(500) | Smaller variant for lists / 3D |
| `wikipedia_title` | VARCHAR(300) | Used by image fetcher |
| `sort_order` | INTEGER | |
| `checkup_checked` | BOOLEAN NOT NULL DEFAULT false | Reviewed in image checkup workflow (UI label: **Reviewed**) |
| `checkup_fixed` | BOOLEAN NOT NULL DEFAULT false | Image corrected via checkup / debug **Fix** |
When `checkup_fixed` is true, `checkup_checked` is set automatically and cannot be cleared until **Fixed** is off.
Applied by `npm run migrate:checkup-flags` (`db/migrate-checkup-flags.sql`).
### `painting_influences`
@@ -150,7 +156,7 @@ Used by painting detail API (`influencedBy`). Painting-type rows also feed `has_
## Indexes
- `artists(movement_id)`, `artists(century)`
- `paintings(artist_id)`, `paintings(period_id)`
- `paintings(artist_id)`, `paintings(period_id)`, `paintings(checkup_checked)`, `paintings(checkup_fixed)`
- `art_movements(era_id)`, `art_movements(start_year, end_year)`
- `painting_influences(painting_id)`, `painting_influences(influenced_by_painting_id)`
- `painting_influence_sources(painting_id)`, `painting_influence_sources(source_artist_id)`, `painting_influence_sources(source_movement_id)`
+20 -1
View File
@@ -36,11 +36,12 @@ Gallery/
│ │ ├── data/movement-lineage.ts # Curated movement predecessor links (UI)
│ │ └── utils/timelineView.ts # Shared zoom/pan math for timeline + movements
│ └── dist/ # Production build (served by API when present)
├── scripts/ # Seed, bios, catalog expansion, image fetch
├── scripts/ # Seed, bios, catalog expansion, image fetch, checkup tools
│ ├── fetch-artist-bios.js
│ ├── expand-paintings.js
│ ├── famous-paintings-data.js
│ ├── fetch-missing-images.js
│ ├── find-duplicate-paintings.js
│ └── image-fetcher.js
├── data/images/ # Local portraits and paintings (+ thumbs/)
├── db/ # SQL schema and migrations (when present)
@@ -222,6 +223,24 @@ Opened from the 3D hall (click a frame) or from influence thumbnails on another
- **3D gallery images** use locally cached files only; slow remote fetches would break realtime rendering.
- **Influence data** is stored as directed links from paintings to sources (another painting, an artist, or a movement), with optional period fields and citation metadata (source author, quote, URL).
## Developer tools (image audit)
Optional workflow for curating local image files — not part of the public visitor experience.
| Feature | Where | Purpose |
|---------|--------|---------|
| **Debug mode** | Home header toggle (`client/src/utils/debugMode.ts`) | Persists in `localStorage`; enables debug panel on painting detail |
| **Checkup page** | Home header → **Checkup** (`CheckupPage.tsx`) | Full-catalog table: gallery vs detail thumbnails, search, fix, review flags |
| **Debug panel** | Painting detail (bottom-left, when debug mode on) | Google-family image search preview + **Fix it** for the current work |
**Checkup columns:** Gallery and Detail thumbnails, Search (reference image), Fix (replace local file), **Reviewed** (`checkup_checked`), **Fixed** (`checkup_fixed`).
**Search visible** runs image search only for rows currently shown after text/filter — not automatically on page load. Fixing an image sets **Fixed** and **Reviewed**.
Run `npm run migrate:checkup-flags` once on existing databases. After server code changes, restart `npm run dev` so new routes (e.g. `PATCH …/checkup-flags`) are registered.
See [API.md](API.md#developer-image-audit) and [data-and-images.md](data-and-images.md#duplicate-paintings).
## Related docs
| Document | Contents |
+37
View File
@@ -36,6 +36,8 @@ File names are sanitised `{Artist}_{Title}.{ext}`. The image service can redisco
| `image-fetcher.js` | *(library)* | Wikimedia / museum resolution used by fetch scripts and API |
| `regenerate-thumbnails.js` | `npm run regenerate-thumbnails` | Rebuild thumbs from full images via `sharp` |
| `audit-painting-images.js` | `npm run audit-painting-images` | Detect thumb/full aspect-ratio mismatches |
| `find-duplicate-paintings.js` | `npm run find-duplicates` | Report exact and near-duplicate catalog rows |
| `migrate-checkup-flags.js` | `npm run migrate:checkup-flags` | Add `checkup_checked` / `checkup_fixed` columns |
## Typical workflow
@@ -305,3 +307,38 @@ Examples already in the repo:
- `Charing Cross Bridge` → Derain (not Monet)
After adding an override, delete any wrong cached file under `data/images/paintings/` and re-run fetch or call the on-demand image endpoint for that painting.
## Duplicate paintings
The catalog can contain the same work more than once — usually from a **double import** (identical artist + title + year + image) or **Wikipedia scrape variants** (different article titles for one icon, e.g. Andrei Rublev’s Trinity).
### Find duplicates
```bash
npm run find-duplicates
```
Runs `scripts/find-duplicate-paintings.js`, which reports:
| Report | Rule |
|--------|------|
| **Exact duplicates** | Same `artist_id`, `title`, and `year` |
| **Normalized title duplicates** | Same artist, titles differing only in punctuation/spacing |
| **Rublev / Trinity cluster** | Known multi-entry example for manual merge |
As of a recent audit (~1200 paintings): **52 exact duplicate pairs** (52 removable rows), concentrated in **Hieronymus Bosch** (25), **Albrecht Dürer** (14), and **Domenico Ghirlandaio** (13). Duplicate copies typically share the same image file and have **no influence links**, so the higher id in each pair is safe to delete after review.
**Near-duplicates** (different titles, same work) need curator judgment — e.g. Rublev ids 36, 316, 320, 322, 323 all describe the Trinity icon under different Wikipedia labels; keep id **36** (`Trinity`, wiki `Trinity (Andrei Rublev)`).
`expand-paintings.js` skips inserts when normalized titles match, but duplicates can still appear if seed and expansion use different title strings or if influence discovery creates works independently.
## Debug and checkup image fix
When **Debug mode** is on (home header) or from the **Checkup** page:
1. **Search** — `GET /api/paintings/:id/debug-image-search` tries Google Custom Search (if `GOOGLE_CSE_API_KEY` + `GOOGLE_CSE_CX` are set in `.env`), Google Arts & Culture, Google Images scrape, then DuckDuckGo (`searchGoogleImagesFirst` in `scripts/image-fetcher.js`).
2. **Fix** — `POST /api/paintings/:id/fix-image` downloads the chosen URL via `replacePaintingImageFromUrl` in `server/image-service.js`, regenerates the thumbnail with `sharp`, and sets checkup flags.
Checkup **Search visible** queues search for filtered rows only (3 concurrent); it does not search the full catalog on load.
See [API.md](API.md#developer-image-audit) and [basics.md](basics.md#developer-tools-image-audit).
+7
View File
@@ -65,6 +65,7 @@ After a fresh seed, run these to match a fully populated local install:
npm run fetch-artist-bios # bio_short / bio_full from Wikipedia
npm run expand-catalog # famous works for artists below MIN_PAINTINGS
npm run update-influences # painting influence graph for detail view + hall exits
npm run migrate:checkup-flags # optional: review/fixed flags for Checkup page
npm run fetch-images -- --limit=50 # random sample; 10s max per painting (default)
npm run fetch-images -- --limit=50 --max-wait=120 # same batch size, longer lookup per work
cd client && npm run build && cd ..
@@ -112,6 +113,8 @@ Open http://localhost:3001 (or your configured `PORT`).
| `npm run fetch-artist-images` | `scripts/fetch-artist-images.js` | Backfill portrait files *(if present)* |
| `npm run sync-image-paths` | `scripts/sync-image-paths.js` | Align DB paths with disk *(if present)* |
| `npm run migrate:influence-sources` | `scripts/migrate-influence-sources.js` | Create `painting_influence_sources` + backfill legacy edges |
| `npm run migrate:checkup-flags` | `scripts/migrate-checkup-flags.js` | Add `checkup_checked` / `checkup_fixed` on `paintings` |
| `npm run find-duplicates` | `scripts/find-duplicate-paintings.js` | Report duplicate and near-duplicate painting rows |
| `npm run update-influences` | `scripts/update-influences.js` | Insert influence links (painting / artist / movement) from `art-influences-data.js` |
| `npm run update-influences -- --fetch-images` | ↑ | Also download images for newly created works |
| `npm run update-influences -- --discover` | ↑ | Curated pass + web discovery (Wikipedia, Wikidata, Met, art-history sites) |
@@ -127,6 +130,8 @@ These are checked in and maintained:
- `update-influences.js` + `art-influences-data.js` — influence graph (paintings, artists, movements)
- `influence-discovery.js` + `influence-resolver.js` — web discovery and polymorphic source resolution
- `fetch-missing-images.js` — batch image backfill
- `find-duplicate-paintings.js` — duplicate catalog audit
- `migrate-checkup-flags.js` — checkup workflow columns
- `regenerate-thumbnails.js`, `audit-painting-images.js`
These are referenced in `package.json` but may need to be restored from git history if missing locally: `seed-wikipedia.js`, `fetch-artist-images.js`, `sync-image-paths.js`.
@@ -162,3 +167,5 @@ After clone: copy `.env.example` → `.env`, install dependencies, run `npm run
| Default Vite page instead of gallery | `client/dist` missing or stale | `cd client && npm run build` |
| Permission denied creating tables | `gallery` user lacks CREATE | Run admin grants, then migrate |
| Wikipedia API rate limit during fetch | Too many requests in a row | Wait and re-run; scripts retry with backoff |
| Checkup **Reviewed** toggle returns 404 | Stale server process missing new routes | Restart `npm run dev` after pulling API changes |
| Duplicate works in gallery / timeline | Double import or variant Wikipedia titles | `npm run find-duplicates`; merge or delete spare rows manually |