docs(#10): UX interaction spec plus remaining POC issue write-back
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.
This commit is contained in:
+66
-31
@@ -10,22 +10,24 @@ Companion: `docs/TECH-SPEC-PIPELINE.md`. Historical PRD: `docs/archive/PRD-2026-
|
||||
|
||||
## 1. Topology (hybrid)
|
||||
|
||||
Phone does the happy-path pipeline (mask, length axis, sqft/bdft, 3:4 PNG).
|
||||
FastAPI stores drafts, runs U2Net only on demand, proxies inference, talks to Woo.
|
||||
reverse proxy terminates HTTPS.
|
||||
Phone does the happy-path pipeline (mask knobs, length axis, sqft/bdft, 3:4 PNG).
|
||||
FastAPI stores drafts, proxies inference, talks to Woo. reverse proxy terminates HTTPS.
|
||||
Server U2Net (“Try harder”) is **deferred from POC**, not deleted.
|
||||
|
||||
```
|
||||
phone (SvelteKit)
|
||||
capture, mask, sliders, length axis, sqft/bdft, 3:4 PNG
|
||||
if mask still bad → POST one photo → FastAPI U2Net → mask back → user continues
|
||||
capture, four knobs inline, live mask overlay on the source photo,
|
||||
length axis, sqft/bdft, 3:4 PNG
|
||||
(POC: retake if mask still bad — no U2Net control)
|
||||
|
||||
server
|
||||
reverse proxy → frontend (static)
|
||||
→ fastapi : store, settings, taxonomy, Woo, inference proxy, U2Net, prompt files
|
||||
→ fastapi : store, settings, taxonomy cache, Woo, inference proxy, prompt files
|
||||
→ U2Net endpoint reserved post-POC
|
||||
```
|
||||
|
||||
- Happy path never leaves the phone.
|
||||
- U2Net is explicit “Try harder” (or coverage failure), not every slab.
|
||||
- Mask knobs live on capture; changing a knob re-runs client BG removal immediately.
|
||||
- Woo + LLM keys stay on the server. Browser does not call Woo or the vision endpoint directly.
|
||||
|
||||
## 2. Swimlane
|
||||
@@ -45,7 +47,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph API["server — FastAPI"]
|
||||
A1[U2Net: one photo in, mask out]
|
||||
A1[U2Net reserved post-POC]
|
||||
A2[Store draft: originals + PNGs + numbers]
|
||||
A3[Call 1 proxy — vision]
|
||||
A4[Call 2 proxy — text]
|
||||
@@ -63,8 +65,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
P1 --> P2 --> P3
|
||||
P3 -->|no — Try harder| A1
|
||||
A1 --> P2
|
||||
P3 -->|no — retune knobs or retake| P2
|
||||
P3 -->|no — Retake| P1
|
||||
P3 -->|yes| P4 --> P5 --> P6
|
||||
P6 --> A2
|
||||
@@ -77,34 +78,34 @@ flowchart TB
|
||||
|
||||
## 3. Clarifications
|
||||
|
||||
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.
|
||||
1. **Happy path** stays on the phone through PNG + numbers. FastAPI is idle until the user has a confirmed mask.
|
||||
2. **Mask knobs** live on capture; each change re-runs client BG removal. Server U2Net “Try harder” is deferred from POC (keep the one-photo contract; do not ship the control).
|
||||
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. 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. When inference is on, Call 1 results
|
||||
**pre-populate** the review screen: species, wood categories, edge/figure/grade
|
||||
attributes, `fig-*`/`feat-*` tags (from character/inclusions/voids/checks), with
|
||||
confidence. User reviews and overrides. Manual entry stays authoritative;
|
||||
Call 1 is assist-only.
|
||||
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.
|
||||
all originals downscaled to 1024 + **Woo-synced taxonomy only** + SKU/length/thickness.
|
||||
Timeout 45s → error + retry on the phone. ≥0.7 confidence **pre-populates**
|
||||
species, wood categories, edge/figure/grade, feat-* (mutable). Below 0.7 those
|
||||
fields stay **empty**. Manual entry stays authoritative. Presentation: `docs/UX.md`.
|
||||
5. **Call 2** does not auto-fire. User taps Generate text. Server must send
|
||||
**portable Call 1 context** (default: full conversation resend). Stateful
|
||||
`previous_response_id` only after `test-inference` proves the provider supports
|
||||
it. Call 2 inputs distinguish confirmed user values from inferred ones. Per-slab
|
||||
thread; delete on submit or abandon.
|
||||
6. **Woo** is only FastAPI. Browser never sees the WordPress application
|
||||
password. Duplicate SKU → stop, show error, no edit-same-SKU in MVP.
|
||||
password. Duplicate SKU → 409 mapped on the SKU field (edit SKU or open existing).
|
||||
Stale value → 422 on that field. Draft stays on the phone.
|
||||
7. **Settings** (species $/bdft, prompts, inference endpoint, Woo credentials,
|
||||
publish status) live on the server; the phone is just the form.
|
||||
publish status) live on the server. Store URL is env, displayed read-only.
|
||||
|
||||
## 4. What runs where
|
||||
|
||||
| Job | Where |
|
||||
|---|---|
|
||||
| Camera, 1-5 photos | Client |
|
||||
| Sheet detect, chroma-key / black threshold, sliders, flood-fill | Client |
|
||||
| U2Net | Server, on demand |
|
||||
| Sheet detect, chroma-key / black threshold, four knobs, live re-run, flood-fill | Client |
|
||||
| U2Net “Try harder” | Server, **deferred from POC** |
|
||||
| Length axis overlay, 6" widths, sqft, bdft | Client |
|
||||
| 3:4 PNG, 80% fill (configurable aspect) | Client |
|
||||
| Draft + originals + processed PNGs | Server (after user continues) |
|
||||
@@ -157,11 +158,11 @@ any → failed (with error)
|
||||
2. **calibrated** — user confirmed the BG edge, confirmed length axis, entered length/thickness/SKU. Client computed sqft/bdft/widths. Draft uploaded to server.
|
||||
3. **ready** — all mandatory fields populated (species, wood category, edge type,
|
||||
figure, grade, thickness, price, title/desc/short desc, at least one photo).
|
||||
Values can come from Call 1 (≥ threshold pre-fill the user kept), manual
|
||||
entry, or a mix. **Review gate:** if Call 1 returned below-threshold for a
|
||||
field, that field stays empty until the user fills it. At minimum **species**
|
||||
and **≥1 figure attribute** must be set before ready. Inference is optional;
|
||||
with inference OFF the user fills everything by hand.
|
||||
Values can come from Call 1 (≥ 0.7 pre-fill the user kept), manual entry, or a mix.
|
||||
**Review gate:** below-threshold Call 1 leaves that field empty. Mandatory before
|
||||
ready: **exactly one species, ≥1 wood category, ≥1 figure**. Inline nudges, not
|
||||
submit-only (`docs/UX.md`). Inference is optional; with inference OFF the user
|
||||
fills everything by hand.
|
||||
4. **publishing** — Woo create in flight. Poll for result.
|
||||
5. **published** — Woo product created, woo_product_id stored. Images purged.
|
||||
6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available.
|
||||
@@ -169,3 +170,37 @@ any → failed (with error)
|
||||
Call 1 and Call 2 are assist-only. They do not gate publish. Low-confidence Call 1
|
||||
results do not auto-fill. If the user skips inference, the path is: calibrated →
|
||||
ready → publishing → published.
|
||||
|
||||
## 7. Taxonomy cache (manufactured last-modified)
|
||||
|
||||
Woo category/tag/attribute `date_modified` is not a reliable client cache key (the store does not bump it for every edit we care about). FastAPI **manufactures** a `taxonomy_updated_at` timestamp:
|
||||
|
||||
- Bump **only** when a stored-subset row actually changes (species/wood-category leaves, five attributes + terms, `fig-*`/`feat-*` tags).
|
||||
- A no-op sync (same rows) **does not** bump.
|
||||
- Client re-pulls if the returned anchor is newer than its snapshot.
|
||||
|
||||
**Triggers**
|
||||
|
||||
1. `POST /api/v1/settings/test-woo` always runs a taxonomy resync after a successful connection test.
|
||||
2. Publish path: opportunistic resync if the last successful bump is older than **1 hour**.
|
||||
3. Optional: app launch and a Settings “Refresh taxonomy” control.
|
||||
|
||||
Species pickers and Call 1 option lists are **exactly** this cached set. No hardcoded mill list.
|
||||
|
||||
## 8. Shared validation module
|
||||
|
||||
One FastAPI module is source of truth for field rules (required, length cap, charset, Woo option membership). Client caches `{ rules, hash }`.
|
||||
|
||||
- Field-exit: local check against the cache.
|
||||
- Length: hard input cap (cannot type past Woo max).
|
||||
- Hash is compared **on submit only**. Mismatch → refresh module → re-validate → retry. Draft stays.
|
||||
|
||||
## 9. Call 1 → Call 2 context
|
||||
|
||||
Providers differ. Default portable path: **resend the full Call 1 turn** (prompt + images metadata + assistant JSON) with Call 2. Opt into `previous_response_id` only after probe.
|
||||
|
||||
`POST /api/v1/settings/test-inference` fails closed (`ok: false`) if **neither** full-resend nor stateful id works. Do not ship a Call 2 that silently drops Call 1 context.
|
||||
|
||||
Per-slab thread. Delete stored thread on successful publish or user abandon.
|
||||
|
||||
Presentation of the review screen: `docs/UX.md`.
|
||||
|
||||
+20
-4
@@ -84,7 +84,14 @@ and the final stored content.
|
||||
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.
|
||||
Rebuilds the cache table (delete + insert, single transaction). Returns counts plus
|
||||
`taxonomy_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)
|
||||
@@ -96,9 +103,9 @@ 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 at least **species** and **one figure attribute**
|
||||
(and the rest of the ready mandatory set). Do not auto-fill below-threshold
|
||||
guesses. Full rules: AGENTS §6.
|
||||
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
|
||||
@@ -184,3 +191,12 @@ Before create: `GET /products?sku={sku}`.
|
||||
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.
|
||||
|
||||
+5
-1
@@ -50,6 +50,7 @@ Draft-only. Deleted on successful publish. WooCommerce is the source of truth fo
|
||||
| `server_rev` | INTEGER NOT NULL DEFAULT 1 | |
|
||||
| `created_at` | TEXT NOT NULL | |
|
||||
| `updated_at` | TEXT NOT NULL | |
|
||||
| `inference_thread_id` | TEXT | optional stateful provider id; delete on publish or abandon |
|
||||
|
||||
Indexes: `sku` (unique), `status`, `updated_at`.
|
||||
|
||||
@@ -64,7 +65,7 @@ any → failed (with error)
|
||||
|
||||
1. **draft** — photos taken, no mask confirmed.
|
||||
2. **calibrated** — BG edge confirmed, length axis confirmed, length/thickness/SKU entered. Client computed sqft/bdft/widths. Draft uploaded to server.
|
||||
3. **ready** — all mandatory fields populated. Values from Call 1, manual, or mix. Inference is optional. Publishable.
|
||||
3. **ready** — exactly one species, ≥1 wood category, ≥1 figure, plus remaining listing fields. Call 1 may prefill only when confidence ≥ 0.7. Inference optional. Publishable.
|
||||
4. **publishing** — Woo create in flight. Poll for result.
|
||||
5. **published** — Woo product created. This row + all photos deleted. sync_log retained.
|
||||
6. **failed** — pipeline, inference, or publish error. Error detail stored. Retry available.
|
||||
@@ -113,6 +114,7 @@ Rule: only inventory photos. Calibration photos do not exist in this design.
|
||||
| `synced_at` | TEXT | |
|
||||
|
||||
Only species + wood category. Sync pulls all Woo categories; the app filters to the two we use.
|
||||
Species leaves from this table (after filter) are the **only** species the UI and Call 1 may offer.
|
||||
|
||||
`UNIQUE(woo_id)`.
|
||||
|
||||
@@ -183,8 +185,10 @@ Keys (server settings; secrets encrypted at rest):
|
||||
- `inference_enabled`, `content_llm_enabled`
|
||||
- `brand_voice`, `geo_context`
|
||||
- `woo_create_status` ('draft' | 'publish'), default `draft` — Woo product create status key only; safety policy lives in AGENTS.md §5
|
||||
- `taxonomy_updated_at` — manufactured ISO timestamp; bump only when a stored-subset taxonomy row changes. No-op sync does not bump. Not Woo's own `date_modified`.
|
||||
- `aspect_ratio` ('3:4'), `output_px` (1600), `output_px_min` (1600), `fill_target` (0.80)
|
||||
|
||||
|
||||
Sheet/slider prefs (`sheet_mode`, sensitivity, edge offset, feather) are **not** server
|
||||
settings keys in POC. Client localStorage is source of truth (AGENTS §4 / ARCHITECTURE §5).
|
||||
Optional server sync of those prefs is out of scope for POC.
|
||||
|
||||
+20
-17
@@ -116,7 +116,7 @@ Deliverables:
|
||||
settings (AES-GCM + test stubs), taxonomy (cache read/write).
|
||||
- Optimistic concurrency (`client_rev` / `server_rev`, 409 paths).
|
||||
- Status machine only: `draft → calibrated → ready → publishing → published | failed`.
|
||||
Job queue only for publish, inference (later), and U2Net-on-demand. Never for measure/crop.
|
||||
Job queue only for publish and inference (later). U2Net-on-demand is deferred from POC. Never for measure/crop.
|
||||
- Integration tests (httpx TestClient): create calibrated → poll → ready (manual fields);
|
||||
revision conflict; duplicate SKU; settings encryption round-trip (secret never returned).
|
||||
|
||||
@@ -162,15 +162,16 @@ Deliverables:
|
||||
re-crop UI as SoT.
|
||||
- Inference UI (only meaningful after Gate C and when `inference_enabled`): Call 1
|
||||
results with confidence. **≥ 0.7** (configurable default) → pre-fill mutable
|
||||
fields. **< 0.7** → leave empty; block continue until species + ≥1 figure
|
||||
(and other mandatories) are set by hand. "Generate text" triggers Call 2;
|
||||
never auto.
|
||||
fields. **< 0.7** → leave empty; inline nudges; block continue until exactly
|
||||
one species, ≥1 wood category, and ≥1 figure (`docs/UX.md`). "Generate text"
|
||||
triggers Call 2; never auto.
|
||||
- Capture: four knobs on the mask screen with live re-run. No U2Net control in POC.
|
||||
- Publish button (online-only), poll `publishing` → `published` | `failed`, surface
|
||||
error detail from sync_log.
|
||||
- Settings UI: WP username + application password + test-woo, inference endpoint +
|
||||
test (vision check), pricing rules editor, brand voice/GEO, `woo_create_status`.
|
||||
Sheet/slider prefs stay in client localStorage. Prompts are server files, not UI
|
||||
settings.
|
||||
test (vision + portable-context probe), pricing rules editor, brand voice/GEO,
|
||||
`woo_create_status`. Store URL read-only from `WOO_BASE_URL`. Sheet/slider prefs
|
||||
stay in client localStorage. Prompts are server files, not UI settings.
|
||||
- Secrets never appear in GET settings responses (masked `*_configured` only).
|
||||
|
||||
Acceptance:
|
||||
@@ -228,14 +229,16 @@ Deliverables:
|
||||
|
||||
- Call 1 (vision) proxy: **all inventory photos** downscaled to 1024 + Woo taxonomy
|
||||
snapshot + user metadata; OpenAI-compatible endpoint; model constrained to cached
|
||||
taxonomy; confidence threshold **0.7** (configurable default; not a POC Settings
|
||||
slider). Auto-run on calibrated create only if `inference_enabled`. Not top-down-only.
|
||||
taxonomy **from the Woo cache only**; confidence threshold **0.7** (configurable
|
||||
default; not a POC Settings slider). Auto-run on calibrated create only if
|
||||
`inference_enabled`. Not top-down-only.
|
||||
- Apply AGENTS §6 gate: ≥ threshold → pre-fill mutable species/categories/attributes/
|
||||
tags; < threshold → empty fields + review gate requiring species + ≥1 figure
|
||||
before ready.
|
||||
- Call 2 (text) proxy: never auto. User taps Generate text. LLM prose only; templates
|
||||
inject deterministic numbers. See `docs/PROMPTS.md`.
|
||||
- `test-inference` endpoint (vision capability check) before Call 2 is allowed.
|
||||
tags; < threshold → empty fields + review gate requiring species + wood category
|
||||
+ figure before ready (`docs/UX.md`).
|
||||
- Call 2 (text) proxy: never auto. Portable Call 1 context (full resend default).
|
||||
LLM prose only; templates inject deterministic numbers. See `docs/PROMPTS.md`.
|
||||
- `test-inference` endpoint (vision + portable vs stateful probe). Fail closed if
|
||||
neither Call 1→Call 2 path works.
|
||||
- Feature flags; `inference_log` audit rows; prompt files read fresh each call.
|
||||
- No numeric/dimension invention by the LLM. LoRA deferred.
|
||||
|
||||
@@ -243,8 +246,8 @@ Acceptance:
|
||||
|
||||
- Local vision model configured: ≥ 0.7 fields pre-fill and remain editable; user
|
||||
override sticks.
|
||||
- Below 0.7: fields stay empty; UI blocks continue/ready until species and ≥1
|
||||
figure attribute are set manually (asserted in integration or UI test).
|
||||
- Below 0.7: fields stay empty; UI blocks continue/ready until exactly one
|
||||
species, ≥1 wood category, and ≥1 figure are set manually (asserted).
|
||||
- No doc/code path auto-fills below-threshold guesses or treats inference as a
|
||||
publish gate.
|
||||
- Inference OFF: app works end-to-end; tests assert zero calls to the inference endpoint.
|
||||
@@ -288,7 +291,7 @@ Exit gate: Ty signs off on UAT results; v1 declared done.
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|---|---|---|
|
||||
| Green-sheet color varies / U2Net CPU latency high | slow or failed “Try harder” | chroma-key / black threshold primary on client; U2Net on demand only; never drop `output_px_min` below 1600 (undersized → retake) |
|
||||
| Green-sheet color varies | bad mask, wrong sqft | chroma-key / black threshold + inline knobs + live re-run; retake; U2Net deferred from POC; never drop `output_px_min` below 1600 |
|
||||
| No vision model available at deploy | species inference off | inference is toggleable; manual species/character fully functional — v1 not blocked |
|
||||
| Woo partial publish (media up, product POST fails) | orphaned images, confusing state | sync_log + retained images for retry; manual cleanup note; no auto-delete |
|
||||
| Local vision endpoint not up | inference off | configurable endpoint; deploy-time config; app works without it |
|
||||
|
||||
+29
-17
@@ -148,17 +148,20 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
|
||||
- `POST /api/v1/slabs` — Create a calibrated draft. Body: multipart with `data` (SlabCreate JSON) + `files[]` (photos) + `meta[]` (photo metadata).
|
||||
- Returns `201` `Slab` with `status: "calibrated"` (mask + axis confirmed; client-sent sqft/bdft/widths stored).
|
||||
- Server auto-runs Call 1 **only if** `inference_enabled` is true. `inference_status` becomes `inferring`, then `done`/`failed`.
|
||||
- `409 duplicate_sku` if SKU already exists in Woo, any status.
|
||||
- After Call 1, persist inferred values onto the slab **only** for fields with confidence ≥ 0.7. Below 0.7 leave those columns null so GET does not prefill.
|
||||
- `409 duplicate_sku` if SKU already exists in Woo, any status (payload as PUT).
|
||||
- `GET /api/v1/slabs` → `200` `[Slab]` (most recent first).
|
||||
- `GET /api/v1/slabs/{id}` → `200` `Slab` / `404`.
|
||||
- Also used as the poll endpoint for inference and publish. Long-running ops set status to `inferring`/`publishing`; client backs off (1s, 2s, 4s, cap 10s).
|
||||
- `PUT /api/v1/slabs/{id}` — Update draft fields. Body: partial `SlabCreate` (any subset of editable fields + `client_rev`).
|
||||
- Server **stores** client-sent derived fields (`sqft`, `bdft`, widths). It may validate shape and ranges. It must **not** recompute mask, sqft, bdft, or widths as source of truth. Client re-runs TECH-SPEC math and PUTs the new numbers.
|
||||
- `409 revision_conflict`, `409 duplicate_sku`.
|
||||
- `409 revision_conflict`.
|
||||
- `409 duplicate_sku` → `{ "error": "duplicate_sku", "sku": "...", "woo_admin_url": "..."? }`. UI maps this to the SKU field (`docs/UX.md`). Never show the code.
|
||||
- `422` stale/invalid field → `{ "error": "stale_field", "field": "species_id", "options": [...] }` after a submission-time taxonomy refresh. UI maps to that field. Draft stays.
|
||||
- `DELETE /api/v1/slabs/{id}` → `204`. Soft: only unpublished drafts. Published slabs cannot be deleted from the app.
|
||||
|
||||
### Photo operations
|
||||
- `POST /api/v1/slabs/{id}/u2net/{pid}` — "Try harder." Server runs U2Net on this one photo, returns a better mask.
|
||||
- `POST /api/v1/slabs/{id}/u2net/{pid}` — **post-POC.** "Try harder." Server runs U2Net on this one photo, returns a better mask. Do not implement or expose in Gate C / first listing. Keep the contract; do not delete.
|
||||
- Returns `202` `{ mask_url: "..." }`. Client uses the mask to re-compute area/widths locally.
|
||||
- `404` if photo not found.
|
||||
- `GET /api/v1/slabs/{id}/photos/{pid}/original` → `image/*` (original, while retained).
|
||||
@@ -173,20 +176,20 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
|
||||
- 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 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`.
|
||||
- Server reads the Call 2 prompt file fresh. Default: **resend full Call 1
|
||||
context** (prompt + assistant JSON + confirmed vs inferred labels). Use
|
||||
`previous_response_id` only if test-inference reported stateful support.
|
||||
Per-slab thread; delete on publish or abandon.
|
||||
- Taxonomy + deterministic numbers + brand/GEO; LLM prose then templates.
|
||||
- Requires `test-inference` `ok: true`. Else `422` (not connected / not portable).
|
||||
- Returns `202` `{ "status": "inferring" }`. Client polls for results.
|
||||
- Timeout > 45s → `504` with error detail.
|
||||
|
||||
### Publish
|
||||
- `POST /api/v1/slabs/{id}/publish` — Publish to WooCommerce.
|
||||
- Pre-conditions: status in `ready`, all required fields set. Else `422 unpublishable`.
|
||||
- Dedupe: if SKU already exists in Woo under any status → `409 duplicate_sku`.
|
||||
- Pre-conditions: status in `ready`, mandatory set (exactly one species, ≥1 wood category, ≥1 figure, remaining listing fields). Else `422 unpublishable` with field keys — UI maps, no raw code.
|
||||
- Dedupe: if SKU already exists in Woo under any status → `409 duplicate_sku` (same payload as PUT).
|
||||
- Stale Woo value → `422 stale_field` as above. Draft stays on the phone.
|
||||
- Returns `202` `{ "status": "publishing" }`. Client polls.
|
||||
- Woo failure → slab `failed`, `sync_log` written, poll returns `502 woo_error`.
|
||||
- Success → slab `published`, `woo_product_id` set, images purged, draft row deleted.
|
||||
@@ -195,10 +198,13 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
|
||||
- `GET /api/v1/admin/taxonomy` → `200` taxonomy grouped by kind:
|
||||
```jsonc
|
||||
{ "categories": [...], "attributes": [{ name, slug, terms: [...] }],
|
||||
"tags": [...] }
|
||||
"tags": [...],
|
||||
"taxonomy_updated_at": "ISO-8601" }
|
||||
```
|
||||
- `POST /api/v1/admin/taxonomy/sync` → `200` `{ "categories": n, "attributes": n, "tags": n, "synced_at": "..." }`.
|
||||
- Pulls from Woo, rebuilds the cache. `502 woo_error` on failure; previous cache retained.
|
||||
Client re-pulls if `taxonomy_updated_at` is newer than its snapshot. Species/attribute/tag
|
||||
lists here are the **only** legal options for UI pickers and Call 1.
|
||||
- `POST /api/v1/admin/taxonomy/sync` → `200` `{ "categories": n, "attributes": n, "tags": n, "taxonomy_updated_at": "..." }`.
|
||||
- Pulls from Woo, rebuilds the cache. Bump `taxonomy_updated_at` **only** if a stored-subset row changed. `502 woo_error` on failure; previous cache retained.
|
||||
|
||||
### Pricing
|
||||
- `GET /api/v1/admin/pricing` → `[PriceRule]`
|
||||
@@ -216,10 +222,16 @@ Sheet/slider prefs are client localStorage only in POC (not in SettingsView).
|
||||
`woo_consumer_key` / `woo_consumer_secret`.
|
||||
- Secrets re-encrypted server-side. `200` `SettingsView` returned (password
|
||||
never echoed).
|
||||
- `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "..." }`
|
||||
- `POST /api/v1/settings/test-woo` → `200` `{ "ok": bool, "detail": "...", "taxonomy_updated_at": "..." }`
|
||||
- Uses stored username + application password over HTTPS Basic Auth against
|
||||
`WOO_BASE_URL` REST v3. Fails closed if credentials missing or URL is not https.
|
||||
- `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "vision_capable": bool, "detail": "..." }`
|
||||
- On success, **always** run a taxonomy resync (bump anchor only if rows changed).
|
||||
- `POST /api/v1/settings/test-inference` → `200` `{ "ok": bool, "vision_capable": bool, "portable_resend": bool, "stateful_previous_response_id": bool, "detail": "..." }`
|
||||
- Probe whether Call 1→Call 2 can use full-history resend and/or `previous_response_id`.
|
||||
- **Fail closed** (`ok: false`) if neither portable resend nor stateful id works. Call 2 must not run.
|
||||
- `GET /api/v1/validation-module` → `200` `{ "hash": "...", "rules": { ... } }`
|
||||
- Shared FastAPI module. Client caches it. Compare `hash` **on submit only**.
|
||||
- Length rules are hard input caps. Field-exit uses the cache locally.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+7
-2
@@ -85,8 +85,9 @@ Return valid JSON matching this schema:
|
||||
```
|
||||
|
||||
### 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.
|
||||
- The taxonomy list (species, wood categories, edge type, figure, grade, feat-*) is
|
||||
pulled from the **synced Woo cache only**. No hardcoded mill species list. A term
|
||||
not in the cache cannot be suggested or selected.
|
||||
- Brief definitions help the model distinguish ambiguous cases
|
||||
(e.g., "quarter-sawn" vs "cathedral").
|
||||
- Character/inclusions/voids/checks are free-text observations. The server
|
||||
@@ -175,6 +176,10 @@ dimensions as measured. {geo_sentence}`
|
||||
- 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.
|
||||
- **Portable Call 1 context (issue #2):** default is full resend of Call 1 (user
|
||||
prompt + assistant JSON). Label each taxonomy value **confirmed** (user-edited)
|
||||
vs **inferred** (still from Call 1). Stateful `previous_response_id` is opt-in
|
||||
after `test-inference`. Fail closed if neither path works.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Inference, Woo payload, API, and schema live in other docs.
|
||||
1. **Determinism** — same inputs → bit-identical outputs. No randomness, no timestamps in math, no network calls for the math.
|
||||
2. **Manual entry is authoritative** — user-entered length, thickness, SKU, and price always win over any computed value.
|
||||
3. **Fallback chain is explicit** — every computed value has a defined fallback; the system never silently guesses.
|
||||
- BG removal: chroma-key / threshold → user slider / sheet toggle → U2Net → retake.
|
||||
- BG removal: chroma-key / threshold → four knobs (sheet, sensitivity, edge offset, feather) live on capture → retake. U2Net last-resort is deferred from POC (do not delete from architecture).
|
||||
- Length axis: min-area bounding rect → user rotate/confirm → retake if the photo is unusable.
|
||||
- Area: requires a single contiguous slab mask; broken mask → flag and retake.
|
||||
4. **Units** — inches in 1/8" steps; sqft and bdft to 2 decimals; price half-up to 2 decimals. Those rounded values are what get stored. No sqin in the UI, tests, or product copy.
|
||||
@@ -48,8 +48,10 @@ The user must see where the cut will land and be able to fix it. Edge quality is
|
||||
- Show the slab edge clearly — that edge is the measurement boundary.
|
||||
- User confirms the edge before length / area run.
|
||||
|
||||
### 3.3 Persisted settings (reset-to-default available)
|
||||
Factory values live in code. If the user changes them, the new values persist until reset.
|
||||
### 3.3 Capture knobs (inline, live, persisted)
|
||||
Factory values live in code. If the user changes them, the new values persist in localStorage until reset (Settings).
|
||||
|
||||
All four knobs are **on the capture screen next to the overlay**, not in Settings. Changing any knob **immediately re-runs** §3.4 on the current photo and refreshes the overlay on the source image. No apply/save step. No server round trip in POC.
|
||||
|
||||
1. **Sheet** — auto / green / black.
|
||||
2. **Sensitivity** — one slider; tighter vs looser detection (green: HSV range width; black: brightness cutoff).
|
||||
@@ -59,12 +61,12 @@ Factory values live in code. If the user changes them, the new values persist un
|
||||
### 3.4 Algorithms
|
||||
- **Green:** chroma-key in HSV. Border flood-fill so only sheet pixels connected to the image edge are removed (protects green-ish grain inside the slab).
|
||||
- **Black:** grayscale threshold. Same flood-fill (protects dark grain).
|
||||
- **Fallback:** if the edge is still wrong after adjustment, U2Net matting. Last resort.
|
||||
- **Fallback (POC):** retake. U2Net matting is last resort **post-POC** (one photo in, mask out; sliders still apply). Do not implement in Gate C.
|
||||
- Morphological close, then feather per the setting.
|
||||
|
||||
### 3.5 Edge quality
|
||||
- The mask must be one contiguous slab contour.
|
||||
- If the user cannot get a clean edge, retake is the resolution.
|
||||
- If the user cannot get a clean edge with knobs, retake is the POC resolution.
|
||||
|
||||
## 4. Length axis, widths, sqft, bdft
|
||||
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
# SlabUploader UX — POC interaction spec
|
||||
|
||||
Status: SPEC · 2026-09-01 · issue #10 (presentation) · functional rules in #9 / AGENTS
|
||||
This is the UAT-facing interface contract. Wireframes are described as screens and states, not pixels. Implementers and testers use this file plus `AGENTS.md` and `docs/TECH-SPEC-PIPELINE.md`.
|
||||
|
||||
Functional numbers (0.7 threshold, empty vs prefill) live in AGENTS §6. This file only says how those rules look and feel.
|
||||
|
||||
---
|
||||
|
||||
## 0. Principles
|
||||
|
||||
- One-handed phone, outdoor or mill-floor lighting, thumb reach.
|
||||
- The user always sees what is missing **on the field**, not only after Submit.
|
||||
- Manual values always beat inference. Empty is honest; fake prefill is not.
|
||||
- No raw HTTP codes or stack traces. Recovery stays on the same screen as the field.
|
||||
|
||||
Mandatory before **ready** (continue to publishable):
|
||||
|
||||
- exactly **one species**
|
||||
- **at least one wood category**
|
||||
- **at least one figure** (`fig-*` or the Figure attribute — same locked set as DATA-MODEL)
|
||||
|
||||
Issue #9 named species + ≥1 figure as the confidence-gate floor. Wood category is also required to list (original product). Presentation treats all three as mandatory with inline nudges (#10).
|
||||
|
||||
---
|
||||
|
||||
## 1. First-run (calibration trick)
|
||||
|
||||
Not OpenCV ruler detection (that path is deferred). First launch of capture shows a short overlay, dismissible, remembered in localStorage:
|
||||
|
||||
1. Lay the slab on a **green or black sheet**.
|
||||
2. Shoot **top-down**, lens parallel to the broad face.
|
||||
3. Put a tape or known length along the prevailing grain so *you* can type length in inches. The app does not read the tape.
|
||||
4. Take 1–5 photos. Confirm the mask before trusting numbers.
|
||||
|
||||
Skip control: “Don’t show again.” Reset lives in Settings, not on the capture chrome.
|
||||
|
||||
---
|
||||
|
||||
## 2. Capture — mask is the load-bearing step
|
||||
|
||||
Background removal defines the pixel footprint. Length, widths, sqft, and bdft all sit on that mask. A clipped or fat mask silently moves the bounding rectangle and the 6" width samples.
|
||||
|
||||
### Layout (one screen)
|
||||
|
||||
- Full-bleed photo with **mask overlay** on the original (included vs excluded), not only the cropped PNG.
|
||||
- Four knobs **on this screen**, next to the preview — never a trip to Settings:
|
||||
- Sheet: auto / green / black
|
||||
- Sensitivity
|
||||
- Edge offset
|
||||
- Feather
|
||||
- Confirm edge. Then length-axis overlay (rotate / confirm). Then length, thickness, SKU.
|
||||
- Retake photo. POC does **not** show “Try harder” (server U2Net). That control is deferred, not deleted — do not remove it from architecture docs.
|
||||
|
||||
### Live re-run
|
||||
|
||||
Changing any knob re-runs client background removal on the current photo and refreshes the overlay immediately. No save button for knobs. No server round trip for mask tuning in POC.
|
||||
|
||||
### Empty / error states (capture)
|
||||
|
||||
| Situation | What the user sees |
|
||||
|---|---|
|
||||
| Sheet color not auto-detected | Prompt: pick green or black. Stay on this screen. |
|
||||
| Mask not one contiguous slab | Banner on the photo: “Cut is broken — retake or tighten the sliders.” Confirm stays disabled. |
|
||||
| Axis looks wrong | Rotate control; if still wrong, retake. |
|
||||
| After crop, shorter side would be < 1600px | Warn: this photo cannot list until retake. No fake upscale. |
|
||||
| User tries to continue without confirming mask | Confirm is the gate; Continue is disabled until confirm. |
|
||||
|
||||
Knobs persist in localStorage (reset-to-default in Settings). They are not server secrets.
|
||||
|
||||
---
|
||||
|
||||
## 3. Review screen
|
||||
|
||||
After calibrated upload (and Call 1 if inference is on).
|
||||
|
||||
### Field rendering
|
||||
|
||||
| Call 1 confidence | Species, wood categories, edge, figure, grade, feat-* |
|
||||
|---|---|
|
||||
| **≥ 0.7** (default; configurable in server settings later, not a POC slider) | Prefill from Call 1. Every value is editable. Show a quiet “suggested” affordance, not a lock. |
|
||||
| **< 0.7** | Leave **empty**. Do not stash the low-confidence guess in a hidden field. |
|
||||
| Inference OFF | Empty; user fills by hand. Same mandatory set. |
|
||||
|
||||
Pickers for species, wood category, and figure list **only** Woo-synced leaves. No “other”, no hardcoded mill list. A species that appeared in Woo after last sync is invisible until the next successful sync.
|
||||
|
||||
Price: recommendation from species $/bdft × bdft when a rate exists; otherwise empty. Override always allowed.
|
||||
|
||||
Title / description: empty until the user taps **Generate text** (Call 2) or types. Call 2 never auto-runs.
|
||||
|
||||
### Per-field nudges (not submit-only)
|
||||
|
||||
While a mandatory field is empty, that control shows an inline state (mark + short line under the field):
|
||||
|
||||
- Species: “Pick the species from the store list.”
|
||||
- Wood category: “Pick at least one wood category.”
|
||||
- Figure: “Pick at least one figure.”
|
||||
|
||||
Nudges appear as soon as the screen loads with empties, and clear when the field is valid. Submit/Continue to ready stays disabled while any of the three is empty — **and** the user can still see which one without scrolling to a toast after tap.
|
||||
|
||||
Do not use a single post-submit modal as the only signal.
|
||||
|
||||
### Overrides
|
||||
|
||||
Changing a prefilled species/figure is a normal edit. No confirm dialog. Manual value is what publishes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Settings (relevant bits)
|
||||
|
||||
- **Store URL**: read-only text from `WOO_BASE_URL` (compose). Not an input.
|
||||
- Woo username + application password: inputs. Password never round-trips on GET.
|
||||
- Inference endpoint + key + model; enable flags.
|
||||
- Species $/bdft table (seeded from synced species only).
|
||||
- `woo_create_status` draft | publish.
|
||||
- Reset capture knobs to factory.
|
||||
- Manual “Refresh taxonomy” is optional for POC; test-connection always syncs.
|
||||
|
||||
---
|
||||
|
||||
## 5. Validation (field exit and submit)
|
||||
|
||||
Shared module on FastAPI (`docs/OPENAPI.md`). Client caches it; hash checked **on submit**, not on every blur.
|
||||
|
||||
- Blur: local check with cached rules. Required fields block; optional empty is valid; optional with content must pass.
|
||||
- Length: hard cap at the Woo limit (cannot type past it).
|
||||
- Character set: invalid characters rejected on exit.
|
||||
- Submit: full pass + hash check. Mismatch → “Updating rules” → re-validate → retry. No data loss.
|
||||
|
||||
Presentation: errors sit under the field. No top-of-screen dump for field problems.
|
||||
|
||||
---
|
||||
|
||||
## 6. Publish errors (after commit)
|
||||
|
||||
Draft stays on the phone.
|
||||
|
||||
**409 duplicate SKU** — inline on SKU:
|
||||
|
||||
- “This SKU already exists in the store (any status).”
|
||||
- Actions: **Edit SKU** (focus the field) · **Open existing listing** (admin URL if the server returned it; otherwise copy SKU and a one-line “look this SKU up in Woo admin”).
|
||||
- Never show `409` as the message.
|
||||
|
||||
**422 stale value** — inline on the field Woo rejected (category, attribute, price, …):
|
||||
|
||||
- “This value is no longer valid. Pick from the current list.”
|
||||
- Options from the synced cache after the submission-time refresh.
|
||||
- Never show `422` as the message.
|
||||
|
||||
Server 5xx / timeout / auth: one recoverable banner (“Couldn’t reach the store — try again”), draft intact. Not a field nudge.
|
||||
|
||||
---
|
||||
|
||||
## 7. UAT can write cases against
|
||||
|
||||
1. First-run overlay appears once; skip persists.
|
||||
2. Knobs on capture; each slider live-reruns mask overlay on the source photo.
|
||||
3. Confirm disabled until mask is contiguous.
|
||||
4. Review with inference ON and confidence ≥ 0.7: fields filled, editable.
|
||||
5. Review with confidence < 0.7: species/wood category/figure empty; inline nudges; cannot mark ready until all three satisfied.
|
||||
6. Species picker contains only synced names; typing a non-synced name is impossible.
|
||||
7. Duplicate SKU: inline SKU recovery, two actions, no raw code.
|
||||
8. Stale category after sync: 422 copy on that field with current options.
|
||||
|
||||
---
|
||||
|
||||
## 8. Non-goals (this spec)
|
||||
|
||||
- Visual branding, type scale, color tokens.
|
||||
- Server U2Net “Try harder” control (deferred).
|
||||
- Configurable 0.7 slider in the UI.
|
||||
- Offline / airplane-mode chrome.
|
||||
Reference in New Issue
Block a user