docs: lock inference review — prompts, Call 1/2, feat tags

Restore CONTENT-WOO, add PROMPTS.md, and align PRD/architecture/API
with inference decisions: prompt files on disk, 0.7 confidence gate,
species constrained to Woo leaves, Call 2 prose-only + templates,
feat-* tags, and inference_enabled gating.
This commit is contained in:
Ty
2026-08-31 17:11:05 -07:00
parent 4e97770373
commit 4d9f3070a7
10 changed files with 420 additions and 33 deletions
+1
View File
@@ -19,6 +19,7 @@ Owner: Ty (Whidbey). Store: www.whidbeywoodstore.com. Deploy: .201 (Caddy, Docke
| `docs/DATA-MODEL.md` | SQLite schema + status state machine |
| `docs/OPENAPI.md` | Client↔server API contract (deliverable #2) |
| `docs/CONTENT-WOO.md` | Content templates + WooCommerce payload/integration |
| `docs/PROMPTS.md` | Call 1 vision prompt + Call 2 content prompt (server files) |
| `docs/DEPLOYMENT.md` | Compose, Caddy, backup/restore, key rotation (deliverable #4) |
| `docs/IMPL-PLAN.md` | Phased build plan with acceptance criteria + exit gates |
| `AGENTS.md` | Working contract + hard rules for anyone implementing |
+35 -15
View File
@@ -20,16 +20,20 @@ publish pipeline.
1. No calibration photos — user manually enters SKU, length, thickness.
2. First photo is the measurement photo (must be top-down, lens parallel to broad side).
3. All photos go through background removal + 80% frame-fill crop.
4. Output: transparent PNGs, minimum 1600 px on the smaller dimension, aspect ratio preserved.
4. Output: transparent PNGs, minimum 1600 px on the smaller dimension,
cropped to 3:4 portrait (W:H 3:4 = 4:3 portrait) to match the WooCommerce
template; configurable in settings.
5. Green or black sheet — algorithm auto-detects background color from image borders.
6. WooCommerce categories synced from store: Species (1 required, leaf-only), Wood Category (1+ required, leaf-only).
7. WooCommerce attributes: Edge Type (1 required), Figure (1+ required), Grade (1+ required), Thickness (1 required, rounded up to nearest Woo attribute range).
8. WooCommerce tags: fig-* only (1+ required, e.g., fig-chat = Chatoyant).
8. WooCommerce tags: fig-* and feat-* only (1+ required, e.g., fig-chat, feat-makcr).
WooCommerce is the authoritative source; the design does not duplicate tag lists.
9. Two-call inference: Call 1 (vision, wood expert) → Call 2 (text, content gen). Both configurable.
10. Call 1 runs automatically after calibration. User reviews/corrects before triggering Call 2.
10. Call 1 runs automatically after calibration **only if** inference is enabled.
User reviews/corrects before triggering Call 2.
11. Inference timeout >45s or non-200 → error with retry. User can fill fields manually.
12. Woo product status set to "published" by default (configurable in settings).
13. All inference prompts stored server-side, editable in settings with reset-to-default.
13. Inference prompts are server-side text files, read fresh on each invocation.
14. Brand voice + GEO context injected into Call 2 prompt.
15. WooCommerce credentials (username + App Password) stored client-side in settings.
16. Inference endpoint: configurable OpenAI-compatible API (user provides base URL, key, model).
@@ -94,8 +98,8 @@ publish pipeline.
- **FR3:** App detects background color from image borders (green or black sheet).
- **FR4:** First photo: BG removal - find longest dimension - compute scale
(longest_px / user-length) - compute area (sqft) - bdft = sqft * thickness / 12.
- **FR5:** All photos: BG removal - crop to 80% frame fill - save as transparent PNG,
minimum 1600 px on smaller dimension, aspect ratio preserved.
- **FR5:** All photos: BG removal - crop to 80% frame fill - save as transparent
PNG, minimum 1600 px on smaller dimension, 3:4 portrait crop (configurable).
- **FR6:** User can retake the first photo or abort and start fresh.
### Dimensions and calculations
@@ -124,22 +128,38 @@ publish pipeline.
### Inference (two-call architecture)
- **FR22:** Call 1 (vision): auto-run after calibration. Input: all inventory photos
(downscaled to 1024px max, using original pre-crop photos), user metadata, synced
Woo taxonomy. Output: species, wood categories, edge type, figure, grade,
character/inclusions/voids/checks with severity, top 3 wood categories with
confidence. If no species match above threshold, omit and require manual selection.
- **FR23:** Call 2 (text): user-triggered after Call 1 curation. Input: deterministic
data + Call 1 results + brand voice + GEO context. Output: title, description,
short description.
- **FR24:** Inference prompts stored server-side, editable in settings, reset-to-default available.
Woo taxonomy. Output: species, wood categories, edge type, figure, grade —
selected from Woo taxonomy, each with a confidence score. The model is
constrained to choose from the synced Woo taxonomy lists.
- **FR22a:** Confidence threshold: 0.7. If the model's confidence for any field
is below 0.7, the field is left blank and requires manual user selection.
Threshold is configurable in settings for tuning.
- **FR22b:** Species bias: the prompt includes the full Woo species leaf list,
constraining the model to select from known species only. This prevents
hallucinated species and significantly improves accuracy.
- **FR22c:** Free-text observations: character, inclusions, voids, and checks
are free-text observations. The server maps them to Woo `feat-*` product
tags after the call returns. WooCommerce is the authoritative source for
feat-* tag definitions; the design does not duplicate them.
- **FR23:** Call 2 (text): user-triggered. Can use curated Call 1 results OR
manually-entered taxonomy. The LLM writes only descriptive prose
(character, color, grain, use cases). The server assembles the final title,
short description, and full description from deterministic templates with the
LLM prose injected. Dimensions are never generated by the LLM.
- **FR24:** Inference prompts are server-side text files, read fresh on each
invocation. A reset-to-default script restores the shipped defaults. Prompts
are not user-facing settings; they are tuned by the owner directly.
- **FR25:** Progress indicator during inference calls (latency expected).
- **FR26:** Timeout >45s or non-200 - visible error with retry option.
- **FR26:** Timeout >45s or non-200 - visible error with retry option. Retry is
available if the inference endpoint was previously tested as connected.
Intermittent failures are expected in low-connectivity environments.
- **FR27:** All inference results editable; no automatic publish of LLM output.
- **FR28:** Inference endpoint configured via settings: base URL, API key, model name.
Test connection verifies text + vision support.
### Photo normalization and output
- **FR29:** Background removal: chroma-key for green/black, fallback to U2Net matting.
- **FR30:** Crop to 80% frame fill, slab centered, aspect ratio preserved,
- **FR30:** Crop to 80% frame fill, slab centered, 3:4 portrait aspect,
minimum 1600 px on smaller dimension.
- **FR31:** Output as transparent PNG (ideal for WooCommerce).
- **FR32:** Store both original and processed images. Purge after successful publish.
+11 -4
View File
@@ -18,7 +18,7 @@ phone (SvelteKit)
server
reverse proxy → frontend (static)
→ fastapi : store, settings, prompts, taxonomy, Woo, inference proxy, U2Net
→ fastapi : store, settings, taxonomy, Woo, inference proxy, U2Net, prompt files
```
- Happy path never leaves the phone.
@@ -77,8 +77,15 @@ flowchart TB
1. **Happy path** stays on the phone through PNG + numbers. FastAPI is idle until the user has a confirmed mask (or taps Try harder).
2. **Try harder** is U2Net on **one photo** (the one on screen). Mask comes back; sliders still apply. Not a full re-pipeline on the server.
3. **Draft upload** sends **originals + processed PNGs + length/thickness/SKU/sqft/bdft/widths**. Originals are needed if Call 1 or a later retry must not depend on the tab still being open. After successful Woo publish, server deletes both.
4. **Call 1** is server-side so the vision key never sits in the browser. Body: all originals downscaled to 1024 + Woo taxonomy snapshot + SKU/length/thickness. Timeout 45s → error + retry on the phone. User **must** curate before Call 2.
5. **Call 2** does not auto-fire. User taps Generate text. Server sends curated Call 1 + deterministic numbers + brand/GEO + prompt.
4. **Call 1** is server-side so the vision key never sits in the browser. Auto-run
on slab create **only if** `inference_enabled` is true. If disabled, Call 1
is skipped; user can manually fill taxonomy and still trigger Call 2. Body:
all originals downscaled to 1024 + Woo taxonomy snapshot + SKU/length/thickness.
Timeout 45s → error + retry on the phone. User reviews/corrects results.
5. **Call 2** does not auto-fire. User taps Generate text. Server sends curated
Call 1 results (or manually-entered taxonomy if Call 1 failed) + deterministic
numbers + brand/GEO for prose generation; assembles title/description/short
description from deterministic templates with the LLM prose injected.
6. **Woo** is only FastAPI. Browser never sees the App Password. Duplicate SKU → stop, show error, no edit-same-SKU in MVP.
7. **Settings** (species $/bdft, prompts, endpoint, Woo, publish status) live on the server; the phone is just the form.
@@ -90,7 +97,7 @@ flowchart TB
| Sheet detect, chroma-key / black threshold, sliders, flood-fill | Client |
| U2Net | Server, on demand |
| Length axis overlay, 6" widths, sqft, bdft | Client |
| 3:4 PNG, 80% fill | Client |
| 3:4 PNG, 80% fill (configurable aspect) | Client |
| Draft + originals + processed PNGs | Server (after user continues) |
| Species $/bdft, Woo creds, prompts, brand/GEO | Server |
| Call 1 (vision, all photos @1024) | Server proxy |
+153
View File
@@ -0,0 +1,153 @@
# 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_enabled` is 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` = `llm` when 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 (PRD "inference calls logged"),
and the final stored content.
## 2. WooCommerce integration (FR24–28)
### 2.1 Auth & client
- REST v3: `GET/POST/PATCH/DELETE {woo_base_url}/wp-json/wc/v3/…`
- Basic auth: `woo_consumer_key:woo_consumer_secret` (Decision 10; the "username" is
the consumer key, the "app password" is the consumer secret).
- Client: `httpx` with timeouts (connect 10s, read 60s for image-heavy create).
- `woo_base_url` default: `https://www.whidbeywoodstore.com`.
### 2.2 Taxonomy sync (FR25)
`POST /api/admin/taxonomy/sync` pulls, in order:
1. `GET /products/categories?per_page=100` (paginate) → `woo_taxonomy(kind=category)`
2. `GET /products/tags?per_page=100` → `kind=tag`
3. `GET /products/attributes` → `kind=attribute`
Rebuilds the cache table (delete + insert, single transaction). Returns counts.
Failure → `502 woo_error`; previous cache retained (never delete-then-fail).
### 2.3 Auto-assign taxonomy (FR26)
- **Category**: species value is matched to a Woo category by name (case-insensitive,
trimmed). Match → that category id. No match → the slab's `category_override`
(user picks in review) or empty (Woo required-field validation will surface it).
- **Tags**: `[character]` plus optional `[species]` if present as a tag.
Call 1 feat-* observations are mapped to Woo feat-* tags (e.g., Crack/Check,
Resin Ready). WooCommerce is authoritative for tag definitions.
- **Attributes**: species/character/length/thickness as product attributes if the
corresponding Woo attributes exist in the cache (by name). Missing attributes are
skipped, not created (v1: no attribute creation).
- Every assignment is overridable in the review screen (FR26).
### 2.4 Product create payload (FR27)
`POST /products` — body (only non-null fields sent):
```jsonc
{
"name": "{title}",
"type": "simple",
"status": "pending", // test-target decision: NEVER "publish" automatically
"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": <char 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:
1. For each customer-facing (inventory, normalized) photo in `seq` order:
`POST /media` with the JPEG bytes → get attachment `source_url`.
2. Reference those `source_url` values in `images` (≤5, per PRD 1–5 inventory photos).
3. Calibration photos are excluded (FR2).
**Status = `pending`** by default (test-target decision: test products must never go
live without human action). The Settings key `woo_publish_status` (default `pending`)
controls this; Ty flips it to `publish` only when intentionally going live.
### 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`, write
`sync_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 normalized inventory images → media → source_urls
6. POST /products (status=pending) → woo_product_id
7. write sync_log(success), set slab published, purge 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.
+8 -2
View File
@@ -38,6 +38,7 @@ Draft-only. Deleted on successful publish. WooCommerce is the source of truth fo
| `thickness_term_id` | INTEGER | → woo_attribute_terms (round-up) |
| `moisture_term_id` | INTEGER | → woo_attribute_terms (1 required, defaults to kiln-dried term; user overrides; not inferred) |
| `fig_tag_ids` | INTEGER[] JSON | → woo_tags (fig-*, 1+) |
| `feat_tag_ids` | INTEGER[] JSON | → woo_tags (feat-*, 0+) |
| `price_per_bdft` | REAL | from pricing_rules for species |
| `price` | REAL NOT NULL | 2 decimals |
| `price_source` | TEXT | 'recommendation' \| 'override' |
@@ -158,7 +159,8 @@ The actual selectable values. E.g., "Cathedral" under Figure, "1\"–1 1/2\"" un
| `slug` | TEXT | |
| `synced_at` | TEXT | |
Only `fig-*` tags. Other Woo tags ignored.
Only `fig-*` and `feat-*` tags. Other Woo tags ignored. WooCommerce is the
authoritative source for tag definitions; this table caches synced Woo tags.
`UNIQUE(woo_id)`.
@@ -177,11 +179,15 @@ Keys:
- `inference_base_url`, `inference_api_key`, `inference_model`
- `inference_enabled`, `content_llm_enabled`
- `brand_voice`, `geo_context`
- `prompts` (JSON: call1_prompt, call2_prompt, reset-to-default)
- `publish_status` ('published' | 'draft')
- `aspect_ratio` ('3:4'), `output_px` (1600), `output_px_min` (1600), `fill_target` (0.80)
- `user_sensitivity`, `user_edge_offset`, `user_feather`, `user_sheet_mode`
**Prompt files** — not in the settings table. Stored as text files in
`/data/prompts/` (server volume). Read fresh on each invocation. Default
prompts shipped with the app; owner edits directly. A reset-to-default script
restores originals.
AES master key is an env var (`SLAB_AES_KEY`, 32 bytes base64). Never in the DB.
Each `value_enc` = `nonce(12) || ciphertext`.
+6 -2
View File
@@ -42,6 +42,7 @@ SlabUploader/
│ ├── pipeline/ # deterministic core (constants.py, ruler, geometry, widths, pricing, normalize)
│ ├── woo/ # Woo client, taxonomy, publish
│ ├── inference/ # vision + content wrappers (toggleable)
│ ├── prompts/ # call1.txt, call2.txt (shipped defaults)
│ ├── api/ # routers (slabs, photos, pricing, taxonomy, settings)
│ ├── db/ # schema.sql, models, migrations (alembic)
│ └── security/ # AES-GCM settings encryption
@@ -67,6 +68,7 @@ services:
volumes:
- slab_db:/data
- slab_images:/images
- ./prompts:/app/prompts:ro # prompt files, read-only mount
ports: ["127.0.0.1:${FASTAPI_PORT:-18081}:8000"] # Caddy-only
mem_limit: 2g # env convention: mem_limit preferred
restart: unless-stopped
@@ -119,8 +121,9 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
```
1. sqlite3 /data/slab.db ".backup /backups/slab-YYYYMMDD.db" # WAL-safe
2. tar czf /backups/slab-images-YYYYMMDD.tgz -C /images .
3. retain: 7 daily + 4 weekly; prune older
4. log result to /var/log/slab-backup.log; alert (Hermes) on failure
3. cp -r /backups/prompts-YYYYMMDD.tar.gz prompts/ # prompt files
4. retain: 7 daily + 4 weekly; prune older
5. log result to /var/log/slab-backup.log; alert (Hermes) on failure
```
- Backup target: local dir first; mirror to TrueNAS SMB share
(`\\truenas.local\…\backups\slab`) for off-box safety (same pattern as other stacks).
@@ -138,6 +141,7 @@ provide `python -m app.security.rekey NEW_KEY` (reads old key from env
| Rekey AES | §5 |
| Rotate Woo creds | Woo UI → new consumer key/secret → Settings UI → `POST /api/settings/test-woo` |
| Rotate inference key | Settings UI (if external) |
| Edit a prompt | Edit the text file on host, no restart needed (read fresh each time) |
| Rollback image | `git checkout <tag>` → rebuild → up -d; DB down-migrations provided for schema changes |
| Reset one slab | delete its photos dir + `UPDATE slabs SET status='draft'` (admin script) |
+8 -5
View File
@@ -85,7 +85,7 @@ Deliverables:
threshold).
- Publish button (online-only), status polling, success/failure states, error detail.
- Settings UI (FR32): Woo creds + test, inference endpoint + test (vision check),
pricing rules editor, brand voice/GEO, templates.
pricing rules editor, brand voice/GEO. Prompts are server files, not UI settings.
Acceptance:
- Full round-trip in browser: capture→upload→review→edit→publish→published (against
@@ -119,9 +119,12 @@ Goal: scoped, toggleable inference with guardrails.
Deliverables:
- Vision wrapper: species/character detection from the top-down photo via the
configurable OpenAI-compatible endpoint; returns confidence; logs request/response.
- Content refinement (optional LLM) with the numeric-accuracy guardrail (CONTENT-WOO
§1.2).
configurable OpenAI-compatible endpoint; model constrained to Woo species list
for accuracy; returns confidence (threshold 0.7, configurable); logs
request/response.
- Content wrapper: Call 2 reads prompts/call2.txt; LLM writes prose only,
dimensions injected by deterministic templates. No numeric guardrail needed.
See docs/PROMPTS.md.
- `test-inference` endpoint (vision capability check).
- All inference code behind feature flags; suite passes with everything OFF.
@@ -130,8 +133,8 @@ Acceptance:
confidence; low confidence → manual confirm required.
- With inference OFF: full app works, tests green (explicit test asserts no network
calls to the inference endpoint).
- LLM refinement rejects output that alters a measurement (guardrail test).
- Inference payloads stored for audit (asserted present after a call).
- Call 1 prompt file edits survive server restart (read fresh each invocation).
Exit gate: Ty runs species detection on 3 real slabs; results acceptable or manually
corrected (that's the design).
+17 -3
View File
@@ -60,6 +60,7 @@ URL is `/api/v1`. Breaking changes bump to `/api/v2`.
"thickness_term_id": 12, // → woo_attribute_terms (round-up)
"moisture_term_id": 5, // → woo_attribute_terms (defaults kiln-dried)
"fig_tag_ids": [99], // JSON array → woo_tags (fig-*, 1+)
"feat_tag_ids": [42], // JSON array → woo_tags (feat-*, 0+)
"price_per_bdft": 12.50, // from pricing_rules
"price": 12.00, // 2 decimals
"price_source": "recommendation|override|null",
@@ -86,6 +87,11 @@ All `SlabCreate` fields (resolved) plus:
```
### InferenceCall1Result (returned by infer-taxon)
Taxonomy fields are Woo IDs from the synced cache. Character/inclusions/voids/checks
are free-text observations; the server maps them to Woo `feat-*` product tags after
the call returns. WooCommerce is the authoritative source for `feat-*` definitions.
```jsonc
{ "species_id": 42, "species_confidence": 0.92,
"wood_category_ids": [15, 23],
@@ -139,7 +145,7 @@ Secrets (keys) are never returned. Only a `*_configured` boolean.
### Slabs
- `POST /api/v1/slabs` — Create a calibrated draft. Body: multipart with `data` (SlabCreate JSON) + `files[]` (photos) + `meta[]` (photo metadata).
- Returns `201` `Slab` (status `draft`).
- Server auto-runs Call 1 (vision inference). `inference_status` becomes `inferring`, then `done`/`failed`.
- Server auto-runs Call 1 **only if** `inference_enabled` is true. `inference_status` becomes `inferring`, then `done`/`failed`.
- `409 duplicate_sku` if SKU already published.
- `GET /api/v1/slabs` → `200` `[Slab]` (most recent first).
- `GET /api/v1/slabs/{id}` → `200` `Slab` / `404`.
@@ -159,11 +165,19 @@ Secrets (keys) are never returned. Only a `*_configured` boolean.
### Inference
- `POST /api/v1/slabs/{id}/infer-taxon` — Trigger or retry Call 1 (vision).
- Server uses cached taxonomy + original photos downscaled to 1024px + user metadata.
- Server reads the Call 1 prompt file fresh, uses cached taxonomy + original
photos downscaled to 1024px + user metadata, and sends to the configured
vision endpoint.
- Returns `202` `{ "status": "inferring" }`. Client polls `GET /api/v1/slabs/{id}` for results.
- Timeout > 45s → `504 gateway_timeout` with error detail.
- `POST /api/v1/slabs/{id}/infer-content` — Trigger Call 2 (text).
- Server uses curated Call 1 results + deterministic numbers + brand voice + GEO context + prompt.
- Server reads the Call 2 prompt file fresh, uses available taxonomy
(from Call 1 results or manually-entered values) + deterministic numbers
+ brand voice + GEO context, sends to the LLM for prose generation, then
assembles title/description/short description from deterministic templates
with the LLM prose injected.
- Requires that inference was previously tested as connected
(`test-inference` succeeded at least once). If not connected, returns `422`.
- Returns `202` `{ "status": "inferring" }`. Client polls for results.
- Timeout > 45s → `504` with error detail.
+178
View File
@@ -0,0 +1,178 @@
# Inference Prompts
Status: SPEC · 2026-08-27 · SlabUploader
Prompts are server-side text files, not settings values. Read fresh on each
invocation. Shipped defaults live in `backend/prompts/`.
## Management
- **Location:** `/app/prompts/` inside the FastAPI container, mounted read-only
from host `prompts/`.
- **Editing:** Edit the text files on the host. No server restart needed.
- **Versioning:** The inference_log stores the prompt content (or hash) with each
inference call, so you can trace which prompt version produced which result.
- **Reset-to-default:** `python -m app.prompts.reset` restores shipped defaults
from the app bundle.
## Call 1 — Vision (species/character classification)
File: `/app/prompts/call1.txt`
### Purpose
Classify the slab's species, edge type, figure, and grade by analyzing
inventory photos. Constrained to choose from the synced Woo taxonomy lists.
### Inputs assembled by the server
- 1–5 inventory photos, downscaled to 1024px max
- Species leaf categories from Woo (list with IDs and brief definitions)
- Edge Type attribute terms with IDs
- Figure attribute terms with IDs
- Grade attribute terms with IDs
- User metadata: SKU, length, thickness
### System prompt (template)
```
You are an expert wood slab classifier. Analyze the provided slab photos and
return taxonomy data. Only use the values provided in the taxonomy lists — do
not invent categories or attribute values.
```
### User prompt (assembled at runtime)
```
Analyze the following wood slab photos and classify the species, edge type,
figure, and grade.
Species options (choose ONLY from this list):
1. [species name] — [brief definition] (woo_id: <id>)
2. ...
Edge Type options:
- Live Edge — natural, uncut bark edge
- Flat Sawn — squared, milled edges
...
Figure options:
- Cathedral — U-shaped grain patterns
- Burl — swirling, knotted grain
...
Grade options:
- Premium — minimal defects, clean grain
- Standard — some minor characteristics
...
Slab metadata: SKU={sku}, Length={length}in, Thickness={thickness}in
Return valid JSON matching this schema:
{
"species_id": <woo_id or null>,
"species_confidence": 0.0-1.0,
"wood_category_ids": [<woo_id>, ...],
"edge_type_term_id": <woo_id or null>,
"figure_term_ids": [<woo_id>, ...],
"figure_confidence": 0.0-1.0,
"grade_term_ids": [<woo_id>, ...],
"grade_confidence": 0.0-1.0,
"character": { "type": "string", "severity": "string" },
"inclusions": { "type": "string", "severity": "string" },
"voids": "string",
"checks": "string",
"wood_categories": [
{ "name": "string", "confidence": 0.0-1.0 },
...
]
}
```
### Notes
- The taxonomy list (species, edge type, figure, grade) is pulled from the
synced Woo cache each time, so the options always match what's in the store.
- Brief definitions help the model distinguish ambiguous cases
(e.g., "quarter-sawn" vs "cathedral").
- Character/inclusions/voids/checks are free-text observations. The server
maps them to Woo `feat-*` product tags after the call returns. WooCommerce
is the authoritative source for feat-* tag definitions; the design does not
duplicate them.
---
## Call 2 — Content (description prose)
File: `/app/prompts/call2.txt`
### Purpose
Generate compelling product description prose for the slab. The model writes
only the descriptive portion — dimensions, species, and measurements are
injected by deterministic templates.
### Inputs assembled by the server
- Curated Call 1 results (species, figure, character, grade)
- Deterministic measurements: length, widths, sqft, bdft, thickness
- Brand voice
- GEO context
- Title template and short description template
### System prompt (template)
```
You are a professional product copywriter for premium, one-of-a-one wood
slabs. Write compelling, natural-sounding descriptions. Use the brand voice
provided. Do not invent measurements — use only the provided data.
```
### User prompt (assembled at runtime)
```
Write product copy for this wood slab.
Species: {species}
Figure: {figure}
Character: {character description from Call 1}
Length: {length_ft}ft {length_in_remainder}in
Average width: {width_avg_in}in
Width range: {width_min_in}in to {width_max_in}in
Thickness: {thickness_in}in
Board feet: {bdft} bdft
Grade: {grade}
Brand voice: {brand_voice}
GEO context: {geo_context}
Write:
1. A compelling opening sentence about the slab
2. A paragraph describing the character, grain, color, and use cases
3. A call to action
Keep the tone appropriate for a premium lumber buyer. Do not repeat the
dimensions — those are handled by the template. Focus on the beauty and
versatility of the slab.
Return JSON: { "prose": "..." }
```
### Template assembly (server-side, after LLM returns)
The server assembles the final title and description from templates with
deterministic measurements, inserting the LLM prose where appropriate.
Example title: `{species} {length_ft}ft × {width_avg}in Slab — {figure}`
Example description: `{llm_prose}. Each slab is naturally shaped — width varies
along the length at {width_min}in to {width_max}in. Sold by the slab;
dimensions as measured. {geo_sentence}`
### Notes
- The LLM never touches dimensions. Templates handle all numbers.
- No numeric-accuracy guardrail needed — dimensions never pass through the LLM.
- If `content_llm_enabled` is false, the LLM call is skipped and templates
are used with a generic description paragraph.
---
## Tuning workflow
1. Deploy the default prompts
2. Capture a real slab, let Call 1 run
3. Review the results against your expert judgment
4. Edit `call1.txt` to improve classification accuracy
5. Re-run Call 1 via `POST /api/v1/slabs/{id}/infer-taxon` to compare
6. Iterate until results are consistently accurate
The inference_log stores each iteration's input/output so you can track
progress across prompt versions.
+3 -2
View File
@@ -115,7 +115,8 @@ Round to 2 decimals. Identity: 1 sqft at 1" thick = 1 bdft.
Applies to every inventory photo of the slab (first photo and the rest).
- Crop aspect: **3:4 portrait** (settings default; matches the current PIP).
- Crop aspect: **3:4 portrait** (default; matches the WooCommerce template;
configurable in settings).
- Transparent **PNG only**.
- Slab centered. **80% fill at the extremes** (widest and tallest points of the slab). Extra margin where the slab is narrower.
- After crop, if the source has enough pixels, shorter side ≥ **1600 px**. If it cannot, warn and ask for a retake — do not invent pixels.
@@ -202,7 +203,7 @@ Price 12.345 → **12.35**.
4. sqft is the projected top-down area of the mask. Live-edge thickness and underside are not in the area.
5. bdft = sqft × thickness (inches). Not rounded up to whole board feet.
6. Sheet is uniform green or black. Odd colors → user picks sheet mode or retakes.
7. 3:4 is the PIP crop. Changing it later is a settings/code change.
7. 3:4 portrait is the PIP crop, configurable in settings.
8. Client does all of this math. Server stores the numbers the client sends.
## 10. Out of scope here (covered in other docs)