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
- Tags — AI classifies each photo using the existing
tagstaxonomy. Also adds mood tags (uplifting, calm, soothing, serene) from a new predefined list once defined. - Color — AI selects from the existing 16-term
colortaxonomy. Multi-color supported. - Review queue — KJP-facing web dashboard to approve/edit/reject AI outputs before they touch WordPress.
- WP push — Approved tags and color terms sync to WordPress via REST API.
- Ongoing processing — New uploads detected and queued automatically (daily cron).
- Taxonomy cleanup — Zero-post terms and typos removed before AI backfill (pre-req, not part of the app build).
Out of scope
- Region — Client handles manually. AI will not attempt region classification.
- Descriptions — Phase 2 only, pending SEO value decision. Architecture supports adding it later with minimal rework.
- Type taxonomy — Already 99%+ covered. No AI work needed.
- WP Media Library integration — Images are not in the Media Library; this system bypasses it entirely.
Pre-requisites (before build starts)
- 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.
- Mood tag list — Casey needs to define the approved mood/emotional tags (uplifting, calm, soothing, etc.). These need to be added to the WP
tagstaxonomy as real terms before AI can classify them. - WP Application Password — Create one for the Vercel app to use for REST API auth.
- 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
- Approve — one click, next image
- Edit — tag picker opens, KJP can add/remove tags before approving
- Skip — leaves in queue, processes later
- No reject/reason flow needed for tag classification
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)
- [ ] Taxonomy cleanup (WP-CLI — delete zero-post terms, fix typos)
- [ ] Get mood tag list from Casey → add to
tagstaxonomy - [ ] Create WP Application Password for Vercel app
- [ ] Confirm multi-color decision with Casey
- [ ] Set up Vercel Pro project + Neon database
- [ ] Build and deploy WP plugin (
kjp-ai-bridge) - [ ] Build and deploy Vercel app
- [ ] Ingest all posts, sync taxonomy terms to Neon
Deliverable: App is live. No images processed yet.
Phase 1 — Tag calibration (50 images)
- G&M creates a batch of 50 mixed images, runs AI
- KJP reviews in dashboard — approve/edit/skip
- G&M reviews the edits: are there systematic patterns?
- Adjust prompt if needed (add a rule, refine the tag list)
- Run another 50 if needed, repeat until 85%+ approved without edits
KJP time: ~20-30 min in review UI Deliverable: Prompt locked in for tags
Phase 2 — Color calibration (50 images)
- Same process focused on color output
- Color is simpler (16 terms) — calibration likely takes 1 round
KJP time: ~15-20 min Deliverable: Prompt locked in for color
Phase 3 — Full backfill (~8,400 images)
- Run in background, 200-500 images/batch, overnight
- G&M monitors for failures or confidence dips
- KJP spot-checks a sample (~100 images)
- G&M triggers WP push after KJP green lights
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:
- Mood tag list — Casey needs to define it. What exact terms? How many?
- Multi-color — All prominent colors (max 3) or just dominant? Recommend: all prominent.
- Tag append vs. replace — Add AI tags to existing, or replace? Recommend: append.
- Descriptions — In scope for Phase 5 or skip entirely?
- 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
- Taxonomy cleanup (WP-CLI, ~30 min)
- WP plugin (
kjp-ai-bridge) — 2 endpoints, auth, tests - Vercel project setup — Neon DB, env vars, deploy
- Ingest job — pull posts + taxonomy terms into Neon
- Batch runner — fetch image, resize to 1024px max (Sharp), Claude Vision integration, prompt, JSON parsing
- Review UI — KJP-facing, clean and simple
- Push job — approved items → WP REST API
- Admin panel — batch controls, stats, push trigger
- Cron job — daily ingest + batch creation
- Phase 5 additions (descriptions) — if green-lit