Skip to content

Stories — the long-form text on artwork and artist pages ​

Current process (owner, 2026-10-04): no paid AI API. Stories are written in Claude sessions, English and Vietnamese first, other languages later; all text is stored in D1 (stories table). The Workers AI pipeline below is kept but switched off (STORIES_PER_RUN = 0).

  1. node scripts/stories/sources.mjs --limit 40 — fetches the next items to write (public /v1/seo/story-queue) with their Wikipedia/Wikidata sources into .story-src/ (gitignored).
  2. Write content/stories/{artwork|artist}/{slug}/{en,vi}.json — {hook, sections[{key, body}], facts, sources}.
  3. node scripts/stories/check.mjs — validates (sections, 70–260 words each, sources, no stray CJK) and adds the fixed headings. Also runs in CI.
  4. Push to main → CI writes the changed stories into D1 and marks them storied; the next daily publish makes them public. A page in a language without a story shows the English one.

Owner's direction (2026-10-04): every painting needs a detailed, emotional text — the history, the artist's intent, what to look at, the stories around it; every artist page needs the life story and origins.

What a story contains ​

ArtworkArtist
Hook (1–2 sentences)Hook
The story behind the paintingOrigins (birthplace, family, training)
What to look for (from the image itself)Life and turning points
Meaning and intentStyle and technique
LegacyStories and anecdotes
"Did you know?" (2–4 sourced facts)Legacy
Sources (links) + an honest AI noteSources + AI note

Each section is 90–170 words → ~500–800 words per page, in 6 languages. Section headings are fixed translations (HEADINGS in generate.ts), never model output.

How it is made (grounded, not invented) ​

  1. Sources: the painting's Wikipedia article (via its Wikidata sitelinks, en → fr → de → es), the artist's article, Wikidata facts (year, size, museum, movement, subjects).
  2. The image itself goes to a vision model (@cf/meta/llama-4-scout-17b-16e-instruct), so "What to look for" describes what is really on the canvas.
  3. Rules in the prompt: only facts from the sources or visible in the image; no guessed places, sitters, dates or quotes; if sources are thin, say so once and stay with what is visible.
  4. Translation: @cf/meta/llama-3.3-70b-instruct-fp8-fast with a per-language style guide and glossary (e.g. vi: cypress = cây bách, still life = tranh tĩnh vật), using the painting's own localized title.
  5. Quality gate: normalizeStory() rejects missing sections or too-short text (tested); one immediate retry, then up to 3 attempts across cron runs.

Measured: ~45 s per painting for all 6 languages; Van Gogh's The Starry Night came back with the asylum at Saint-Rémy (June 1889), the east-facing window, the imagined village and MoMA since 1941 — all from the cited article. An obscure Renoir without a Wikipedia article came back honest ("details about its creation are scarce") and focused on the visible painting.

Where it lives ​

  • Text in D1, table stories (kind, slug, locale, data JSON) (migration 0009). Stories first written by the old Workers AI cron sat in R2; the cron copies them into D1 for free (dropping any language with stray CJK characters).
  • Progress in D1: artworks.story_status, artists.story_status (none / done / failed), story_attempts.
  • Artist portrait (artists.image_url, Wikidata P18) and artist links (artist_links: influenced by / teacher / student) are captured while writing the artist story.

Pipeline ​

  • Cron every 10 minutes (runStoryPass): STORIES_PER_RUN (6) artworks + 1 artist. Order: published works without a story → works of public artists that still have fewer than 10 → queued works by fame. ≈ 860 stories/day, ahead of publishing.
  • Publishing is gated: only artworks with story_status = 'done' are published.
  • ≥ 10 works per artist: an artist becomes public only with ≥ MIN_ARTIST_WORKS (10) published works; a new artist enters with a block of its 10 best storied works; public artists below 10 are topped up first.

relatedArtists() in catalog/routes.ts: Wikidata links in both directions (influenced by / inspired, teacher / student), then the artists sharing the most works in the same movement. The artwork page shows the two best works of each, labelled with the relation.

Cost (Workers AI, list prices) ​

≈ $0.01 per painting for the story in 6 languages → ≈ $3/day at 300/day; the full 277k catalog ≈ $2,500 over the year. Artists ≈ $0.015 each.

Admin ​

POST /v1/admin/seo/stories with {"artwork": "slug"} or {"artist": "slug"} writes one now; empty body runs a pass. GET /v1/admin/seo/stats shows story progress.