KJP AI Tagging System — PRD

Consolidated from Notion 2026-04-23. Notion is deprecated. Status: Phase 2 — deferred to fall/winter 2026.

Last updated: 2026-03-10 (source) · 2026-04-23 (migrated here) Stack: Next.js (Vercel Pro) · Vercel Postgres (Neon) · Claude Vision · Custom WP Plugin


Goal

Build a custom AI pipeline that analyzes KJP's ~8,400 photography posts and classifies each one using their existing taxonomy. Tags and color are the primary deliverable. Descriptions are Phase 2, contingent on SEO value confirmation.

End state: every photo has accurate tags and a color classification. A review queue ensures KJP approves everything before it goes live. The system runs ongoing, so new uploads never fall behind.


Scope

In scope

Out of scope


Pre-requisites (before build starts)

  1. Taxonomy cleanup — Delete zero-post color terms (Pruple, Yelllow, Gold, Grasses, Gray, Black) and zero-post region terms. Fix type taxonomy typos. ~30 min WP-CLI job.
  2. Mood tag list — Casey needs to define the approved mood/emotional tags (uplifting, calm, soothing, etc.). These need to be added to the WP tags taxonomy as real terms before AI can classify them.
  3. WP Application Password — Create one for the Vercel app to use for REST API auth.
  4. Multi-color decision — Tag all prominent colors or just dominant? Determines prompt design. (Leaning: all prominent, max 3.)

Architecture

[WordPress (Kinsta)]
    |
    |-- Custom WP Plugin
    |     GET  /wp-json/kjp-ai/v1/posts          (paginated list of posts needing work)
    |     GET  /wp-json/kjp-ai/v1/taxonomy-terms (all valid tag + color terms)
    |     POST /wp-json/kjp-ai/v1/update-post    (batch taxonomy update)
    |
    v
[Vercel App (Next.js)]
    |
    |-- Ingest job     Pull posts from WP, store in Neon DB
    |-- Batch runner   Fetch image → resize to 1024px max → send to Claude Vision
    |-- Review UI      KJP approves/edits/rejects AI outputs
    |-- Push job       Approved items -> WP REST API
    |-- Cron (daily)   Pick up new uploads, queue them
    |
    v
[Vercel Postgres (Neon)]
    images, batches, taxonomy_terms tables
    |
    v
[Claude Vision API]
    Receives: image URL + predefined tag list + color list
    Returns:  { tags: [...], colors: [...] }

Image access: Photos are publicly served at https://kurtjohnsonphotography.com/wp-content/uploads/wpallimport/files/{filename}. No auth needed.

Image resize (required): KJP's imports include images up to 3,000×3,000px or larger. At full resolution a 3000×3000 image costs ~12,000 image tokens vs ~1,400 for a resized version — nearly 9× more expensive with zero classification benefit. The batch runner must fetch each image and resize to 1024px max on the longest side before sending to the API. Sharp (Node.js) handles this in-memory; no disk writes needed. All cost estimates assume this step is implemented.

WP auth: Application Password stored as Vercel env var. All WP REST calls use HTTP Basic auth.


Data Model (Neon Postgres)

images

Column Type Notes
id UUID PK
wp_post_id INTEGER UNIQUE WordPress post ID
image_filename TEXT ACF featured_image field value
image_url TEXT Full public URL
current_tags INTEGER[] WP term IDs currently assigned
current_color INTEGER[] WP term IDs currently assigned
ai_tags INTEGER[] Term IDs returned by AI
ai_colors INTEGER[] Term IDs returned by AI
ai_confidence FLOAT AI self-reported confidence
status TEXT pending processing needs_review approved pushed skipped
reviewed_at TIMESTAMP
pushed_at TIMESTAMP
created_at TIMESTAMP
updated_at TIMESTAMP

batches

Column Type Notes
id UUID PK
name TEXT e.g. "Backfill Round 1"
size INTEGER Number of images in batch
status TEXT pending running complete failed
created_at TIMESTAMP
completed_at TIMESTAMP

batch_items

Column Type Notes
id UUID PK
batch_id UUID FK
image_id UUID FK
status TEXT pending processed failed
error TEXT Error message if failed

taxonomy_terms

Column Type Notes
wp_term_id INTEGER
taxonomy TEXT tags color
name TEXT
slug TEXT
post_count INTEGER
synced_at TIMESTAMP When we last pulled from WP

WP Plugin Spec

Lightweight plugin in /wp-content/plugins/kjp-ai-bridge/. Auth: WordPress Application Password on all routes.

Endpoints

GET /wp-json/kjp-ai/v1/posts — Returns photography posts that are missing tags or color.

Params:
  needs=tags|color|any   (filter)
  page=1
  per_page=100

Returns:
  [
    {
      "post_id": 12345,
      "filename": "2566.jpg",
      "tags": [101, 204, 87],
      "color": [],
      "has_description": false
    },
    ...
  ]

GET /wp-json/kjp-ai/v1/taxonomy-terms — Returns all terms for a given taxonomy.

Params:
  taxonomy=tags|color

Returns:
  [
    { "id": 101, "name": "flower", "slug": "flower", "count": 2425 },
    ...
  ]

POST /wp-json/kjp-ai/v1/update-post — Batch-updates a post's taxonomies.

Body:
  {
    "post_id": 12345,
    "tags": [101, 204, 87, 312],      // term IDs
    "color": [55, 78],                // term IDs
    "append_tags": true               // true = add to existing, false = replace
  }

Returns:
  { "success": true, "post_id": 12345 }

Vercel App

Pages

Route Audience Purpose
/ KJP Dashboard — progress stats, queue depth
/review KJP Review queue — approve/edit/reject per image
/admin G&M Run batches, view logs, trigger WP push

API Routes

Route Method Description
/api/ingest POST Pull posts from WP, upsert to Neon
/api/taxonomy/sync POST Pull all tag + color terms from WP, cache in Neon
/api/batch/create POST Create new batch from pending images. Body: { size: 50, focus: "tags" }
/api/batch/run POST Process batch — calls Claude Vision per image. Body: { batch_id }
/api/review/submit POST Submit review decision. Body: { image_id, action: "approve\|reject\|edit", tags?, colors? }
/api/push POST Push approved items to WP. Body: { image_ids?: [...] }
/api/stats GET Dashboard stats — totals by status
/api/cron/daily GET Vercel Cron trigger — ingest + create batch for new uploads

Review UI — KJP Experience

One image at a time:

+-------------------------------------------------------+
|  [Photo]              Suggested tags:                 |
|                       flower, purple, calm, soothing  |
|                       close-up, lily, midwest         |
|                                                       |
|                       Color: Purple                   |
|                                                       |
|           [ ✓ Approve ]    [ Edit ]    [ ✗ Skip ]     |
+-------------------------------------------------------+
  Photo 247 of 500 in this batch  |  83% approved so far

AI Prompt Design

Tag + Color Classification

Sent to Claude Sonnet vision for each image.

You are classifying a professional nature photograph for Kurt Johnson Photography.
Your job is to select tags and colors from the EXACT lists below — do not invent new terms.

ACTIVE TAG LIST:
{full tag list from Neon, name only, comma-separated}

MOOD TAGS (always consider these even if not in main list):
{mood_tag_list: uplifting, calm, soothing, serene, energetic, peaceful, dramatic}

COLOR LIST (select all prominent colors, max 3):
Blue, Brown, Green, Orange, Pink, Purple, Red, White, Yellow, Black/White

RULES:
- Select 5-15 tags that accurately describe what is in this photo
- Be specific: name visible plant species, describe composition and mood
- Include at least one mood tag if applicable
- Select all prominent colors (max 3), not just dominant
- Return ONLY valid JSON, no explanation

Return:
{
  "tags": ["flower", "purple", "lily", "close-up", "calm"],
  "colors": ["Purple", "White"],
  "confidence": 0.92
}

Model

Use Claude Sonnet 4.6 for calibration, then Haiku 4.5 for bulk run (pending quality validation on a 50-image test). See KJP-AI-Cost-Estimate.md.

Tag list strategy

With ~2,200 active terms after cleanup, the list is long but fits in Sonnet's context window. Terms are short (avg 2-3 words), so the full list is ~15-20K tokens. Pass it on every request — no need for semantic pre-filtering.


Phased Rollout

Phase 0 — Pre-build (G&M)

Deliverable: App is live. No images processed yet.

Phase 1 — Tag calibration (50 images)

KJP time: ~20-30 min in review UI Deliverable: Prompt locked in for tags

Phase 2 — Color calibration (50 images)

KJP time: ~15-20 min Deliverable: Prompt locked in for color

Phase 3 — Full backfill (~8,400 images)

KJP time: ~30-45 min spot-check, then approve push Deliverable: All ~8,400 posts have tags and color

Phase 4 — Ongoing

Vercel Cron runs daily: 1. Pull new posts from WP (anything imported since last run) 2. Create a batch and process it 3. New images land in review queue within 24 hours 4. KJP reviews at their pace, G&M triggers push

KJP time: Periodic 15-min review sessions as new content comes in

Phase 5 — Descriptions (conditional)

Only if Casey confirms descriptions have meaningful SEO value.

Scope sidebar — Is adding descriptions a big lift? Short answer: no — it's a clean add-on, not a rebuild. The entire infrastructure is shared.

What's the same (nothing to redo): - WP plugin — no changes - Ingest job — no changes - Batch runner architecture — no changes - Cron — no changes - WP push job — one extra field added

What changes (the actual delta): - Data model: add ai_description + approved_description columns — trivial - Prompt: add a description output field to the existing JSON return — trivial - Review UI: add an inline text area for reading/editing descriptions — the main UI work - WP push: write to post_content in addition to taxonomies — small - WP plugin update-post endpoint: accept a description field — small

Extra dev effort: ~5–8 hours total.

Where the real lift is — and it's not dev work: the harder part is voice/tone calibration. Tags are fast to review (does this tag fit? yes/no). Descriptions require reading, evaluating prose, and editing. KJP's review time per image goes from ~10 seconds to ~45-60 seconds. The calibration loop will also take more rounds — tone is subjective in a way that taxonomy classification is not. Expect 3-4 calibration rounds instead of 1-2.

For quoting purposes: charge for ~6 hours additional dev. Keep descriptions as a separate line item. Extra API cost per image is marginal — descriptions add ~150 output tokens at $15/MTok, roughly +$0.002/image or ~+$17 for the full backfill.

Recommendation: Don't build it speculatively. Get Casey's yes on SEO value, then quote it as a ~6hr add-on once the tag system is proven.

Voice rules for when descriptions are activated: - Match Casey's existing description style - Healthcare-focused clients — avoid negative/dark/threatening language - Never use the word "magical" - 2-3 sentences, specific and warm


Cost Estimate

See KJP-AI-Cost-Estimate.md for full token math, model comparison, and OpenRouter strategy.

Recommendation: Sonnet for calibration, Haiku for bulk run (pending 50-image quality test). Use OpenRouter for routing + tiered escalation.

Infrastructure

Item Cost Notes
Vercel Pro $20/mo Required: function timeouts + Cron
Vercel Postgres (Neon) Included Bundled with Pro
Domain $0 Use .vercel.app or add to kjp project

Vercel Pro can be downgraded to Hobby (free) after backfill is complete and only the ongoing cron job remains — or repurposed for another client.


Open Decisions

Must be resolved before writing a line of code:

  1. Mood tag list — Casey needs to define it. What exact terms? How many?
  2. Multi-color — All prominent colors (max 3) or just dominant? Recommend: all prominent.
  3. Tag append vs. replace — Add AI tags to existing, or replace? Recommend: append.
  4. Descriptions — In scope for Phase 5 or skip entirely?
  5. Auto-push threshold — After calibration, does KJP want to review every image, or auto-push anything with confidence > 0.95 and spot-check?

Build Order

  1. Taxonomy cleanup (WP-CLI, ~30 min)
  2. WP plugin (kjp-ai-bridge) — 2 endpoints, auth, tests
  3. Vercel project setup — Neon DB, env vars, deploy
  4. Ingest job — pull posts + taxonomy terms into Neon
  5. Batch runner — fetch image, resize to 1024px max (Sharp), Claude Vision integration, prompt, JSON parsing
  6. Review UI — KJP-facing, clean and simple
  7. Push job — approved items → WP REST API
  8. Admin panel — batch controls, stats, push trigger
  9. Cron job — daily ingest + batch creation
  10. Phase 5 additions (descriptions) — if green-lit