UAT is POC, so #10 is in DOC COMPLETE. Capture knobs live on-screen, review confidence presentation, 409/422 field recovery, taxonomy cache, portable Call 1→Call 2, shared validation. U2Net Try harder deferred, not deleted.
11 KiB
Content Generation & WooCommerce Integration
Status: SPEC · 2026-08-24 · SlabUploader Covers: FR16 (deterministic content templates + optional LLM refinement), FR17 (brand voice / GEO), FR18 (editable, never auto-published), FR25–28 (taxonomy sync, product create, sync log), and the exact WooCommerce REST v3 payload.
1. Content generation (FR16–18)
1.1 Deterministic templates (always on)
Templates are configured in Settings (content_templates, editable per FR32).
Defaults below. Placeholders use {field}.
title (default):
{species} {length_ft}ft {length_in_remainder}in × {width_avg_in}in Wood Slab — {character}
Example: Black Walnut 16ft 0in × 18.2in Wood Slab — Cathedral
length_ft= floor(length_in / 12),length_in_remainder= length_in mod 12 (1 dp).
short_title (default):
{species} Slab — {character}
Example: Black Walnut Slab — Cathedral
description (default, multi-paragraph):
A one-of-a-one {character} {species} slab, {length_ft}ft {length_in_remainder}in long with an
average width of {width_avg_in}in and {thickness_in}in thickness ({bdft} bdft).
Each slab is naturally shaped — width varies along the length at {width_min_in}in to
{width_max_in}in. Sold by the slab; dimensions as measured. {geo_sentence}
geo_sentence = GEO context sentence from Settings (FR17), e.g.
"Locally milled and shipped from the Pacific Northwest." (default if unset: none).
All template math fields come from the deterministic pipeline (never from the LLM).
1.2 Call 2 content generation (toggleable)
- Triggered only if
content_llm_enabledis true AND the user taps "Generate text" on the review screen (never automatic). - LLM writes prose only: the prompt sends the slab's deterministic measurements as facts, and asks the model to write compelling product description prose (character, color, warmth, grain, use cases). Dimensions are never generated by the LLM — they are injected by deterministic templates.
- Template assembly: after the LLM returns, the server assembles the final title, short description, and full description from deterministic templates with the LLM prose injected into the description.
- No numeric guardrail needed — dimensions never pass through the LLM.
- Output is always editable (FR18);
content_source=llmwhen used. - If the inference endpoint is text-only or unreachable → the button is disabled, templates still work. No failure mode blocks publish.
1.3 Audit
Every content generation (template or LLM) is logged with inputs (template version, slab facts), the LLM request/response payload when used (inference calls logged for audit), and the final stored content.
2. WooCommerce integration (FR24–28)
2.1 Auth & client
- REST v3 surface unchanged:
GET/POST/PATCH/DELETE {WOO_BASE_URL}/wp-json/wc/v3/… - Auth is WordPress Application Passwords, not WooCommerce consumer keys
(
ck_/cs_) and not the user's login password. - Create a dedicated low-privilege WordPress user (shop manager, or a custom role with product create/edit + media upload). Do not reuse an admin account.
- Generate the application password under Users → Profile → Application Passwords. WordPress requires HTTPS for that UI; the store must already be on TLS (Caddy enforces HTTPS on the app side too).
- Basic auth shape:
woo_wp_username:woo_app_password(username + application password). Spaces in the generated password are ignored by WordPress; store the value as entered. - FastAPI is the sole Woo caller. Credentials live encrypted at rest in SQLite
(AES-GCM,
SLAB_AES_KEYenv). The browser never holds them. - Rotation: revoke the application password in the WP user profile, enter a new
one in Settings UI, then
POST /api/v1/settings/test-woo. - Client:
httpxwith timeouts (connect 10s, read 60s for image-heavy create). - Store URL is
WOO_BASE_URLfrom Docker Compose / host.env(HTTPS only). FastAPI refuseshttp://at startup. Not a Settings field; UI shows it read-only.
2.2 Taxonomy sync (FR25)
POST /api/admin/taxonomy/sync pulls, in order:
GET /products/categories?per_page=100(paginate) →woo_taxonomy(kind=category)GET /products/tags?per_page=100→kind=tagGET /products/attributes→kind=attributeRebuilds the cache table (delete + insert, single transaction). Returns counts plustaxonomy_updated_at. Bump that timestamp only if a stored-subset row changed. A no-op sync (identical rows) leaves the anchor alone. Client re-pulls when the anchor is newer than its snapshot.
When to sync: (1) always after successful test-woo; (2) opportunistic on
publish if last bump is older than 1 hour; (3) optional launch / Settings refresh.
Failure → 502 woo_error; previous cache retained (never delete-then-fail).
2.3 Taxonomy assignment (FR26)
Primary source when inference is on and confidence ≥ threshold (default 0.7):
Call 1 vision results pre-fill the review screen for species, wood categories,
edge type, figure, grade, and mapped fig-* / feat-* tags (character /
inclusions / voids / checks → existing feat-*). All pre-filled values are
mutable. The user confirms or overrides; manual entry always wins.
Below threshold: those fields stay empty. The review screen gates continue
until the user manually selects exactly one species, ≥1 wood category, and ≥1
figure. Inline nudges: docs/UX.md. Do not auto-fill below-threshold
guesses. Full functional rules: AGENTS §6.
Deterministic name-matching resolves Call 1 names (or manual picks) to synced Woo IDs case-insensitive, trimmed. No match → empty; user picks in review. Woo required-field validation still surfaces gaps at publish.
Field rules:
- Categories: species leaf (1 required) plus wood-category leaf ids (1+).
- Tags:
fig-*(1+ required) andfeat-*(0+). Do not invent generic character or species product tags. Woo is authoritative for tag definitions. - Attributes: the five locked Woo attributes only — Edge Type, Figure, Grade, Thickness, Moisture — using term ids from the cache. Do not publish species, character, length, or free-text thickness as product attributes. Missing attributes are skipped, not created (v1: no attribute creation).
Call 1 is assist-only and never gates publish. Inference OFF → full manual path.
2.4 Product create payload (FR27)
POST /products — body (only non-null fields sent):
{
"name": "{title}",
"type": "simple",
"status": "{woo_create_status}", // draft | publish; default draft; SLAB-UAT-* forced draft
"sku": "{sku}",
"regular_price": "{price}", // 2 dp string, e.g. "2730.00"
"description": "{description}",
"short_description": "{short_title}",
"categories": [ { "id": <species category id> } ],
"tags": [ { "id": <fig-* tag id> }, { "id": <feat-* tag id> }, … ],
"images": [
{ "src": "<data URL or pre-uploaded attachment URL>", "alt": "{species} {character} slab" }
],
"attributes": [ … if present … ],
"virtual": false,
"downloadable": false,
"catalog_visibility": "visible"
}
Image upload strategy (Woo v3): images[].src accepts a URL or a data URL.
For reliability the server:
- For each customer-facing inventory photo in
seqorder (processed transparent PNGs from the client TECH-SPEC path):POST /mediawith the PNG bytes → get attachmentsource_url. Woo acceptsimage/*; keep alpha. Do not re-encode to JPEG and drop alpha. - Reference those
source_urlvalues inimages(≤5 inventory photos). - Only
kind=inventoryphotos. Calibration photos do not exist in this design.
Status comes from Settings key woo_create_status (draft | publish, default
draft). Woo safety policy (UAT draft default, SLAB-UAT-* force draft even when
Settings say publish, no edit-same-SKU, purge originals + processed images after successful
publish) is owned by AGENTS.md §5. This file owns payload shape and publish sequence
only. Do not diverge from AGENTS on force-draft. Do not conflate Woo create status with
slab lifecycle status published (means Woo create succeeded locally).
2.5 Dedupe & idempotency (FR24a)
Before create: GET /products?sku={sku}.
- Found →
409 duplicate_sku(v1 has no update path; human resolves). - Not found → proceed. (Single-owner, slab-by-slab, so no TOCTOU concern in practice; the check is still done for correctness.)
2.6 Sync result & logging (FR28)
- On success: set
slabs.woo_product_id,status=published,published_at, writesync_log(status=success, woo_product_id, payload_hash). - On Woo HTTP error:
status=failed,sync_log(status=failed, http_status, error, detail=<Woo error body>). Surface in UI + poll response. - Partial-failure note (risk, see IMPL-PLAN): if media uploads succeed but the final product POST fails, orphaned Woo attachments may exist. v1: log it; a manual cleanup note is surfaced. Not auto-deleted (could delete a real image).
3. End-to-end publish sequence (server side)
1. validate slab publishable (length,width,thickness,price,species,char,≥1 photo)
2. dedupe by SKU (GET /products?sku=) → 409 duplicate_sku if exists
3. (re)sync taxonomy if cache older than 1h → best-effort; use existing cache on fail
4. resolve category/tags/attributes (auto-assign + overrides)
5. upload processed PNG inventory images → media → source_urls
6. POST /products (status per AGENTS §5 + woo_create_status) → woo_product_id
7. write sync_log(success), set slab published, purge originals + processed images
on any failure at 2–6: write sync_log(failed), slab=failed, (images retained for retry)
4. What is deterministic vs scoped-inference here
- Deterministic: templates, all dimension text, category/tag/attribute resolution, payload assembly, dedupe, logging, title assembly.
- Scoped inference: optional LLM prose generation for description only. The LLM never touches dimensions.
4. Field-level Woo errors (presentation in docs/UX.md)
API codes stay on the wire. The phone never displays 409 or 422 as the message.
- 409 duplicate SKU (exists in Woo under any status): inline on SKU. Actions: edit SKU, or open/copy the existing listing. Draft stays on the phone. No in-place update of the other product in POC.
- 422 stale or invalid field (category/attribute/tag/price no longer valid): after a submission-time taxonomy refresh, return the field key plus the current allowed options. Inline on that field. Draft stays.
The shared validation module (GET /api/v1/validation-module) is the client cache for required/length/charset. Hash check on submit only.