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:
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user