docs: lock revised capture pipeline and hybrid architecture
Rewrite PRD, tech spec, data model, and OpenAPI around the 2026-08-27 decisions: client-side mask/measure/crop, U2Net on demand, Woo as product source of truth, draft-only local state. Architecture records topology C through the status machine. Drop the old CONTENT-WOO spec until the Call 2 prompt review finishes.
This commit is contained in:
+165
-218
@@ -1,271 +1,218 @@
|
||||
# Technical Spec: Deterministic Image Pipeline & Ruler→Inch Math
|
||||
# Technical Spec: Deterministic Image Pipeline & Area → Bdft
|
||||
|
||||
Status: SPEC (pre-implementation) · 2026-08-24 · TyTech / SlabUploader
|
||||
Status: SPEC · 2026-08-27 · TyTech / SlabUploader
|
||||
Scope: PRD deliverable #1 — algorithms and test vectors for the deterministic core.
|
||||
No inference, no LLM. Everything here is pure functions of (pixels, inputs) → outputs.
|
||||
Every threshold/constant lives in §8 (single source of truth) and is unit-tested.
|
||||
No inference, no LLM. Everything here is a pure function of (pixels, user inputs) → outputs.
|
||||
Inference, Woo payload, API, and schema live in other docs.
|
||||
|
||||
---
|
||||
|
||||
## 1. Invariants
|
||||
|
||||
1. **Determinism** — same inputs → bit-identical outputs. No randomness, no timestamps in math, no network calls.
|
||||
2. **Manual entry is authoritative** — a user-entered value (length, width, thickness, SKU, price) always wins over any computed value.
|
||||
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.
|
||||
4. **All math in inches and board feet** — stored as float64; display rounding per §2.
|
||||
5. **Client vs server split** — client runs the *light* pass (preview, OCR, quick ruler check) for feedback; server runs the *authoritative* pass. Server values are what get stored and published. Client results are hints only.
|
||||
- BG removal: chroma-key / threshold → user slider / sheet toggle → U2Net → retake.
|
||||
- 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.
|
||||
5. **Client is authoritative** — all deterministic processing runs in the browser. The server stores the numbers and files the client sends, and handles Woo sync.
|
||||
|
||||
## 2. Units & rounding
|
||||
## 2. Units and rounding
|
||||
|
||||
| Quantity | Internal | Display | Stored |
|
||||
|---|---|---|---|
|
||||
| Length, widths | float64 inches | 1 decimal | full precision |
|
||||
| Thickness | float64 inches (manual) | 2 decimals (eighths OK in UI) | full precision |
|
||||
| sqft | float64 | 1 decimal | full precision |
|
||||
| bdft | float64 | 1 decimal | full precision |
|
||||
| Price | float64 USD | 2 decimals | 2 decimals (round half-up) |
|
||||
| Quantity | Source | Stored / displayed |
|
||||
|---|---|---|
|
||||
| Length | user, 1/8" steps | nearest 1/8" |
|
||||
| Thickness | user pick, 1/8" steps | that pick (used in math and copy) |
|
||||
| Thickness Woo attribute | same pick, rounded **up** into a Woo range | Woo term, not the 1/8" value |
|
||||
| sqft | computed from mask | 2 decimals |
|
||||
| bdft | sqft × thickness (inches) | 2 decimals |
|
||||
| Price | species $/bdft × bdft, or override | 2 decimals, half-up |
|
||||
|
||||
- bdft is **never rounded up to whole units** (ASSUMPTION — flag to Ty: lumber-trade practice sometimes rounds up; v1 does not).
|
||||
- Price computed from unrounded bdft, then rounded half-up to cents.
|
||||
- Pixel→inch scale is float64 px/inch, never pre-rounded.
|
||||
- bdft is never rounded up to whole board feet.
|
||||
- Pixel→inch scale is an implementation detail. Users never see pixels.
|
||||
|
||||
## 3. Ruler → inch scale
|
||||
## 3. Background removal
|
||||
|
||||
### 3.1 Inputs
|
||||
- `ruler.png` — calibration photo: a standard **12-inch ruler** (Decision 3: always inches), placed flat and parallel to the slab's long axis on the green sheet.
|
||||
- Known: `RULER_INCHES = 12.0` (§8).
|
||||
The user must see where the cut will land and be able to fix it. Edge quality is the foundation of sqft and bdft.
|
||||
|
||||
### 3.2 Server algorithm (authoritative)
|
||||
```
|
||||
1. Grayscale, GaussianBlur (k=5, σ=1.0)
|
||||
2. Detect the ruler:
|
||||
a. Canny (40, 120) → contours → candidate quads (4 corners, area > 5% of frame)
|
||||
b. Score candidates: high straight-line edge density (Hough, minLineLength=10% of ruler width),
|
||||
regular tick spacing (see 3.3), aspect ratio within 12:0.5..12:2 band
|
||||
c. Take highest score; none above TICK_SPACING_TOL → RULER_FAIL
|
||||
3. Perspective-correct the ruler to a top-down rect using its 4 corners
|
||||
→ rect of W_r × H_r px, long axis horizontal
|
||||
4. Tick detection on the corrected rect:
|
||||
a. Column-wise dark-pixel projection over the central 60% band of the rect
|
||||
b. Local minima with prominence > MIN_TICK_PROMINENCE → tick x-positions
|
||||
c. Cluster ticks; compute gaps; identify the modal gap g_mode
|
||||
d. Tick labels: integer inches where cumulative distance from the first
|
||||
labeled tick ≈ k × g_mode for k = 1..12
|
||||
e. Scale candidate: s = (x_{12th tick} − x_{1st tick}) / 11.0 [px per inch]
|
||||
(11 gaps between tick 1 and tick 12)
|
||||
f. Validation: |g_mode − (s/1)| < TICK_SPACING_TOL and tick count ≥ 12
|
||||
5. Cross-check with the ruler rect edges: s_edge = (W_r − 2·margin) / 12.0
|
||||
where margin is the detected pre-"1" and post-"12" padding (mode tick gap).
|
||||
If |s − s_edge| / s < RULER_CROSS_CHECK_TOL → use s.
|
||||
Else → prefer s (tick-based), log warning.
|
||||
6. Emit: scale_px_per_inch = s, ruler_confidence = f(tick_count, tol residuals)
|
||||
```
|
||||
`RULER_FAIL` → the pipeline proceeds via the manual-length path (§5.3).
|
||||
### 3.1 Auto-detect sheet
|
||||
1. Sample a 2–5% strip on all four borders.
|
||||
2. Average HSV per side.
|
||||
3. Hue in [35, 85] and saturation ≥ 40 → green.
|
||||
4. Else value ≤ 30 and saturation ≤ 20 → black.
|
||||
5. Else prompt: pick green or black.
|
||||
|
||||
### 3.3 Client light pass (OpenCV.js, preview only)
|
||||
Canny + largest 4-point contour + tick projection. Emits a *hint* scale for live
|
||||
feedback ("~201 px/in — looks right"). Never stored. If it fails, UI says
|
||||
"ruler unclear — you can enter length manually."
|
||||
### 3.2 Visual feedback (required)
|
||||
- Overlay the mask on the photo (what will be removed vs kept).
|
||||
- Show the slab edge clearly — that edge is the measurement boundary.
|
||||
- User confirms the edge before length / area run.
|
||||
|
||||
### 3.4 SKU OCR (client, Tesseract.js)
|
||||
- Preprocess: grayscale → contrast stretch → upscale ×2 if ruler-width < 800 px → binarize (Otsu)
|
||||
- PSM: 7 (single line), character whitelist: `ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-`
|
||||
- Accept if: confidence ≥ OCR_CONF_MIN AND regex `^[A-Z0-9-]{3,24}$`
|
||||
- Else → manual SKU entry (FR3). Server re-runs OCR on upload as a secondary check; mismatch → surface both, user picks.
|
||||
### 3.3 Persisted settings (reset-to-default available)
|
||||
Factory values live in code. If the user changes them, the new values persist until reset.
|
||||
|
||||
## 4. Top-down photo → pixel geometry
|
||||
1. **Sheet** — auto / green / black.
|
||||
2. **Sensitivity** — one slider; tighter vs looser detection (green: HSV range width; black: brightness cutoff).
|
||||
3. **Edge offset** — pull the mask in or out a few pixels so the cut sits on the wood, not in the sheet or into the grain.
|
||||
4. **Feather** — hard cut vs a couple of pixels of alpha blend.
|
||||
|
||||
The inventory photo used for width sampling must be **top-down** (slab flat on the
|
||||
green sheet, camera above). Selection rule: the first inventory photo whose slab
|
||||
contour aspect ratio matches (L_px : w_px ≈ reported L : expected w) — else the
|
||||
user picks which photo is top-down in the review UI (FR23).
|
||||
### 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.
|
||||
- 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.
|
||||
|
||||
## 4. Length axis, widths, sqft, bdft
|
||||
|
||||
### 4.1 Contour and length axis
|
||||
1. Binary mask M from §3.
|
||||
2. Largest contour → convex hull.
|
||||
3. Minimum-area bounding rectangle on the hull.
|
||||
4. Long side = length axis; short side = width axis.
|
||||
This follows the **prevailing** length, not a diagonal cut on one end.
|
||||
|
||||
### 4.2 Visual feedback (required)
|
||||
- Overlay the bounding rectangle and the length axis.
|
||||
- Width sample lines drawn perpendicular to that axis.
|
||||
- User confirms the axis matches the slab’s prevailing length.
|
||||
- **Rotate axis** if it is skewed; samples and sqft scale update live.
|
||||
- Nearly square slabs: user says which axis is length.
|
||||
|
||||
### 4.3 Scale
|
||||
User length L (inches, 1/8") is authoritative.
|
||||
|
||||
```
|
||||
1. Green-sheet removal (see §7.1) → slab mask M
|
||||
2. Contour of M → C (largest contour)
|
||||
3. Rotate C so its principal axis is horizontal (PCA)
|
||||
4. Length in px: L_px = max extent along the axis
|
||||
5. If ruler scale available: L_in = L_px / s. Cross-check vs any user-entered
|
||||
length (manual is authoritative; if |L_in − L_manual| / L_manual > LEN_MISMATCH_TOL
|
||||
→ warn in UI, use manual).
|
||||
6. Edge profiles: for x ∈ [0, L_px], y_top(x), y_bot(x) from C.
|
||||
Width profile: w(x) = y_bot(x) − y_top(x)
|
||||
scale = longest_axis_px / L
|
||||
```
|
||||
|
||||
## 5. Dimensions → sqft → bdft
|
||||
### 4.4 Widths (listing only, not bdft)
|
||||
- Sample every **6"** along the confirmed length axis, perpendicular to it.
|
||||
- Report min, max, and average width for the description.
|
||||
- Auto-dimension overlay on the photo is **v1.5**, out of this spec.
|
||||
|
||||
### 4.5 sqft
|
||||
Projected top-down area of the mask. Live-edge thickness and the underside are not in the area.
|
||||
|
||||
### 5.1 Width sampling at peaks & troughs (Decision 1)
|
||||
```
|
||||
1. Smooth w(x) with a moving average (window = L_px / 50)
|
||||
2. Find local maxima (peaks) and minima (troughs) of w(x) with
|
||||
prominence > WIDTH_FEATURE_PROM
|
||||
3. Candidate sample x-positions:
|
||||
- x = 0.05·L_px and x = 0.95·L_px (near both ends, always)
|
||||
- all peak/trough x-positions (trimmed to interior)
|
||||
4. Select 3–5 points (WIDTH_SAMPLES_MIN..MAX):
|
||||
always keep the two end points; add interior points ranked by prominence
|
||||
until 5, or stop at 3 if fewer interior features exist.
|
||||
5. w_i = w(x_i) in px → inches via scale (or via manual length, §5.3)
|
||||
6. w_avg = (Σ w_i) / n (Decision 18)
|
||||
sqft = (mask_pixel_count / scale²) / 144
|
||||
```
|
||||
|
||||
### 5.2 Core math
|
||||
Round to 2 decimals.
|
||||
|
||||
### 4.6 bdft
|
||||
|
||||
```
|
||||
sqft = (L_in × w_avg_in) / 144
|
||||
bdft = (L_in × w_avg_in × t_in) / 12 (Decision 18)
|
||||
bdft = sqft × thickness_in
|
||||
```
|
||||
|
||||
### 5.3 Fallbacks (Decisions 2, 19)
|
||||
| Available | Scale source | Length | Widths |
|
||||
|---|---|---|---|
|
||||
| Ruler OK | ruler px/in | computed (cross-checked vs manual) | sampled from top-down photo |
|
||||
| Ruler FAIL, manual L, top-down photo OK | L_manual / L_px | manual (authoritative) | sampled from photo via derived scale |
|
||||
| Ruler FAIL, manual L, NO usable top-down photo | — | manual | **manual width entry** (Decision 19; single width = w_avg) |
|
||||
| No length at all (no ruler, user declines) | — | BLOCK publish — length is required | — |
|
||||
Round to 2 decimals. Identity: 1 sqft at 1" thick = 1 bdft.
|
||||
|
||||
## 6. Pricing (Decisions 4, 5, 13)
|
||||
```
|
||||
rule = pricing_rules[species, character] (seeded from synced Woo categories)
|
||||
recommendation = bdft × rule.price_per_bdft (unrounded bdft)
|
||||
final_price = user override if edited, else recommendation
|
||||
stored_price = round_half_up(final_price, 2)
|
||||
```
|
||||
- No rule for (species, character) → recommendation = null, UI shows "no pricing rule — set price manually". Publish still allowed with a manual price.
|
||||
## 5. Photo normalization
|
||||
|
||||
## 7. Photo normalization (Decisions 8, 20)
|
||||
Applies to every inventory photo of the slab (first photo and the rest).
|
||||
|
||||
### 7.1 Green-sheet background removal (deterministic first)
|
||||
The sheet is a uniform green; chroma-key beats a general matting model here:
|
||||
- Crop aspect: **3:4 portrait** (settings default; matches the current PIP).
|
||||
- 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.
|
||||
- Original + processed kept while draft/retry; both purged after successful publish (server storage; see Data Model).
|
||||
|
||||
## 6. Pricing
|
||||
|
||||
- Settings: sync **species** from Woo; user sets **$/bdft per species**.
|
||||
- Capture: `recommendation = bdft × species.price_per_bdft`, pre-fills price.
|
||||
- User may override. Override sticks for that slab.
|
||||
- No rule for that species → empty price; user types it. Publish still allowed.
|
||||
- Stored price: half-up to 2 decimals.
|
||||
- Figure- or category-weighted price is **v1.5**.
|
||||
|
||||
## 7. Constants
|
||||
|
||||
### 7.1 Code (not in Settings)
|
||||
```
|
||||
1. Convert to HSV (OpenCV 8-bit: H 0–179)
|
||||
2. Sheet mask: H ∈ [GREEN_HUE_LO, GREEN_HUE_HI] AND S ≥ GREEN_SAT_MIN AND V ≥ GREEN_VAL_MIN
|
||||
3. Border flood-fill: keep only sheet pixels connected to the image border
|
||||
(protects greenish wood tones inside the slab)
|
||||
4. Morphological close (kernel 9) then 1–2px feather on the alpha edge
|
||||
5. Fallback: if sheet coverage < SHEET_COV_MIN of frame (wrong background),
|
||||
escalate to U²Net matting for that photo and flag it in the log
|
||||
FILL_TARGET = 0.80
|
||||
ASPECT_W, ASPECT_H = 3, 4
|
||||
OUTPUT_PX_MIN = 1600
|
||||
WIDTH_SAMPLE_IN = 6
|
||||
INCH_STEP = 0.125
|
||||
AREA_DECIMALS = 2
|
||||
BDFT_DECIMALS = 2
|
||||
PRICE_DECIMALS = 2
|
||||
```
|
||||
|
||||
### 7.2 Boundary detection & 1:1 crop (FR20)
|
||||
### 7.2 Factory defaults for persisted BG settings
|
||||
```
|
||||
1. Slab bbox from mask M (after removal)
|
||||
2. Target: slab occupies FILL_TARGET of the final 1:1 frame
|
||||
→ frame side (px) = min(bbox_w, bbox_h) / FILL_TARGET
|
||||
3. Crop window: square of that side, centered on the bbox center
|
||||
(clamped to source bounds; if clamped, re-center on the clamp-constrained side)
|
||||
4. Upscale/crop to 1600×1600 (Decision 20; 1000×1000 is the hard minimum —
|
||||
if source can't supply 1000 px of slab in the short dimension, keep source
|
||||
resolution, flag "low-res" in the review UI)
|
||||
5. Output: 1600×1600 PNG (transparency preserved), plus a JPEG (q=85) copy
|
||||
for the WooCommerce upload
|
||||
SHEET = auto
|
||||
GREEN_HUE_LO/HI = 35, 85
|
||||
GREEN_SAT_MIN = 40
|
||||
GREEN_VAL_MIN = 40
|
||||
BLACK_THRESH = 30
|
||||
```
|
||||
Sensitivity, edge offset, and feather map onto these at runtime. Users do not edit the raw HSV numbers.
|
||||
|
||||
### 7.3 Merchandising filter (FR22, optional, per-slab toggle)
|
||||
Deterministic LUT: contrast +12% (linear stretch), warmth +8 (R/B channel shift
|
||||
toward warm), grain = unsharp mask (radius 2, amount 0.4). Fixed constants in §8 —
|
||||
no per-photo tuning. Applied after §7.2, before encode.
|
||||
## 8. Test vectors
|
||||
|
||||
### 7.4 What gets stored (Decision 12)
|
||||
- While draft / retry: original + normalized, both on disk (Docker volume)
|
||||
- After successful publish: both purged (SQLite rows keep metadata only)
|
||||
Users think in inches, sqft, and bdft. Pixels are only how the computer gets there.
|
||||
|
||||
## 8. Deterministic constants (single source of truth)
|
||||
### TV-1 Green sheet
|
||||
Synthetic: green border, wood in the center. Interior green-ish patch that does not touch the border stays. Coverage enough that U2Net does not run.
|
||||
|
||||
```python
|
||||
RULER_INCHES = 12.0 # known physical ruler length
|
||||
CANNY_LO, CANNY_HI = 40, 120
|
||||
MIN_TICK_PROMINENCE = 0.15 # of projection peak amplitude
|
||||
TICK_SPACING_TOL = 0.05 # 5%
|
||||
RULER_CROSS_CHECK_TOL = 0.03 # 3%
|
||||
OCR_CONF_MIN = 70 # Tesseract 0–100
|
||||
LEN_MISMATCH_TOL = 0.05 # 5%
|
||||
WIDTH_FEATURE_PROM = 0.02 # 2% of L_px, in width-profile prominence
|
||||
WIDTH_SAMPLES_MIN, WIDTH_SAMPLES_MAX = 3, 5
|
||||
END_TRIM = 0.05 # sample at 5% and 95% of length
|
||||
GREEN_HUE_LO, GREEN_HUE_HI = 35, 85
|
||||
GREEN_SAT_MIN = 40
|
||||
GREEN_VAL_MIN = 40
|
||||
SHEET_COV_MIN = 0.25 # sheet must cover ≥25% of frame
|
||||
FILL_TARGET = 0.80 # slab fill of final frame
|
||||
OUTPUT_PX = 1600 # 1:1 output size (Decision 20)
|
||||
OUTPUT_PX_MIN = 1000 # hard minimum
|
||||
JPEG_QUALITY = 85
|
||||
FILTER_CONTRAST = 0.12
|
||||
FILTER_WARMTH = 8
|
||||
FILTER_GRAIN_RADIUS = 2
|
||||
FILTER_GRAIN_AMOUNT = 0.4
|
||||
```
|
||||
All of these are importable module-level constants (`pipeline/constants.py` when
|
||||
implemented) and each test vector in §9 pins at least one of them.
|
||||
### TV-2 Black sheet
|
||||
Same layout, black border, mid-tone wood. Dark interior wood that does not touch the border stays.
|
||||
|
||||
## 9. Test vectors
|
||||
### TV-3 Scale, sqft, bdft
|
||||
Slab **96" long**, **1.5" thick**. Mask measures **0.64 sqft**.
|
||||
bdft = 0.64 × 1.5 = **0.96**.
|
||||
|
||||
### TV-1: Ruler scale (pure math)
|
||||
Ruler rect corrected; ticks detected at px [100, 300, 500, …, 2300] (12 ticks, 200 px apart).
|
||||
- scale = (2300 − 100) / 11 = **200.0 px/in**
|
||||
- s_edge with margin 100 px each side, W_r = 2500: (2500 − 200) / 12 = 191.67 → Δ = 4.2% > 3% → use tick scale, log warning. ✔ expected branch.
|
||||
### TV-4 Width samples
|
||||
96" length axis, stations every 6". Widths perpendicular to the confirmed axis. Listing gets min, max, avg.
|
||||
|
||||
### TV-2: Full happy path
|
||||
- scale = 200 px/in (from TV-1)
|
||||
- L_px = 19,200 → L = 96.0 in
|
||||
- widths (px) at x = 960, 4800, 9600, 14400, 18240: [3500, 3640, 3800, 3560, 3700]
|
||||
→ inches: [17.5, 18.2, 19.0, 17.8, 18.5] → w_avg = **18.2 in**
|
||||
- t = 1.5 in (manual)
|
||||
- sqft = (96 × 18.2) / 144 = **12.13**
|
||||
- bdft = (96 × 18.2 × 1.5) / 12 = **218.40**
|
||||
- rule: (Black Walnut, Cathedral) = $12.50/bdft
|
||||
- recommendation = 218.40 × 12.50 = **$2,730.00**
|
||||
### TV-5 Angled end
|
||||
One square end, one 45° cut. Length axis follows prevailing length, not the diagonal. Widths stay perpendicular after user confirm.
|
||||
|
||||
### TV-3: Manual length, derived scale
|
||||
- RULER_FAIL; manual L = 96.0 in; L_px = 19,200 → scale = 200.0 px/in (derived)
|
||||
- Same width px as TV-2 → identical downstream results (18.2 in avg, 218.40 bdft).
|
||||
- Asserts the fallback chain is value-equivalent when inputs match.
|
||||
### TV-6 3:4 crop, 80% at extremes
|
||||
Slab centered in a 3:4 frame. At the widest and tallest points, fill = 0.80. PNG with alpha. Shorter side ≥ 1600 when the source allows.
|
||||
|
||||
### TV-4: No top-down photo (Decision 19)
|
||||
- Manual L = 96.0, manual width = 18.0, t = 1.5
|
||||
- w_avg = 18.0 → bdft = (96 × 18 × 1.5) / 12 = **216.00**
|
||||
- No photo used in math; publish allowed.
|
||||
### TV-7 Too-small source
|
||||
After 80% crop, shorter side would be below 1600 and the source cannot supply it. Warn. Do not upscale from nothing. That photo is not publishable until retake.
|
||||
|
||||
### TV-5: Pricing edge cases
|
||||
- bdft = 218.4, rule $12.34 → rec = 2695.056 → stored **$2,695.06** (half-up)
|
||||
- bdft = 0 (degenerate) → rec = $0.00, UI blocks publish (slab must have positive dims)
|
||||
- No rule → rec = null; manual price $999.99 stored as-is.
|
||||
### TV-8 Pricing
|
||||
bdft 0.96, species $12.50/bdft → rec **12.00**.
|
||||
Override **150.00** stored as 150.00.
|
||||
No rule → rec empty; manual 99.99 stored as 99.99.
|
||||
|
||||
### TV-6: Normalization geometry
|
||||
- Source 4000×3000; slab bbox (px): x 400..3600, y 600..2400 → w=3200, h=1800
|
||||
- frame side = 1800 / 0.80 = 2250
|
||||
- crop: 2250×2250 centered on (2000, 1500) → x 875..3125, y 275..2525 (in bounds ✔)
|
||||
- slab fill check: 1800 / 2250 = **0.80** ✔
|
||||
- Output: 1600×1600. Slab in output: 1800·(1600/2250) = 1280 px → 80% of 1600 ✔
|
||||
- Clamp case: bbox center (1000, 1000), side 2250 → clamp x to 0..2250, y to 0..2250; re-center rule applies; expected crop x 0..2250, y 0..2250 with slab offset left/up — fill still 0.80.
|
||||
### TV-9 Thickness → Woo range
|
||||
User pick **1"** stored as 1". Attribute bucket is the Woo range that 1" rounds **up** into (e.g. `1"–1 1/2"`). Exact range labels come from the synced Woo attribute, not this file.
|
||||
|
||||
### TV-7: Green-sheet key
|
||||
- Synthetic: 1000×1000, H=60 S=120 V=120 background; center 400×200 rect H=20 S=60 V=80 (wood)
|
||||
- Expected: mask covers background minus the wood rect; border-flood removes any
|
||||
interior sheet-colored islands; sheet coverage = 60% ≥ 25% ✔ (no U²Net escalation)
|
||||
- Wood rect with a green-ish tone (H=45 S=50 V=100) touching no border → NOT removed (border flood) ✔
|
||||
### TV-10 Rounding
|
||||
Length 96.04" → **96"** (nearest 1/8").
|
||||
sqft 0.641 → **0.64**.
|
||||
bdft 11.525 → **11.53**.
|
||||
Price 12.345 → **12.35**.
|
||||
|
||||
### TV-8: Width sampler selection
|
||||
- w(x) profile: ends 100, peak 140 at x=0.3L, trough 90 at x=0.6L, end 105
|
||||
(prominences: peak 40 ≥ 2% of L_px-scale ✔, trough 50 ✔)
|
||||
- Expected sample points: {0.05L, 0.95L, 0.3L(peak), 0.6L(trough)} = 4 points (≤5 ✔)
|
||||
- n=4 → w_avg = (100+105+140+90)/4 = 108.75 (px) → inches via scale.
|
||||
## 9. Assumptions
|
||||
|
||||
### TV-9: OCR acceptance
|
||||
- "BW-0042" conf 85, regex ✔ → accept
|
||||
- "BW-004?" conf 78 → regex ✘ → manual entry
|
||||
- "BW-0042" conf 65 → conf < 70 → manual entry
|
||||
1. The first photo is top-down, lens parallel to the broad face. Tilted shots make sqft/bdft wrong; retake is the fix.
|
||||
2. User length is the prevailing length of that face, in 1/8". The bounding-box axis is confirmed in the UI.
|
||||
3. Thickness is the declared 1/8" pick, not measured from any photo.
|
||||
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.
|
||||
8. Client does all of this math. Server stores the numbers the client sends.
|
||||
|
||||
## 10. Assumptions (flagged for Ty)
|
||||
## 10. Out of scope here (covered in other docs)
|
||||
|
||||
1. **bdft not rounded up** to whole units (§2). Say the word and it's a one-line change.
|
||||
2. **12-inch ruler** with inch ticks is the standard calibration tool (Decision 3 implies; spec assumes a 1-ft rule with ticks at every inch).
|
||||
3. **Top-down photo selection** = best aspect-ratio match, user-overridable (FR23). No inference used for this.
|
||||
4. **Ruler parallel to long axis** — if the user places it diagonally, tick scale is still valid (it's a linear scale), only the cross-check margin logic degrades; acceptable for v1.
|
||||
5. Green sheet is the standard bright craft green. A different sheet color → U²Net fallback path (§7.1 step 5), not a config item in v1.
|
||||
|
||||
## 11. Out of scope for this spec (next: OpenAPI contract, deliverable #2)
|
||||
- Endpoint shapes, request/response schemas, error taxonomy
|
||||
- SQLite schema beyond the pricing-rule shape (PRD)
|
||||
- WooCommerce product payload mapping
|
||||
- Content generation templates (FR16) — deterministic template design is a separate short doc
|
||||
| Topic | Doc |
|
||||
|---|---|
|
||||
| Call 1 / Call 2 prompts, taxonomy mapping | Content / Woo |
|
||||
| Woo payload, publish, duplicate SKU | Content / Woo, OpenAPI |
|
||||
| SQLite tables, status machine | Data Model |
|
||||
| HTTP contract | OpenAPI |
|
||||
| Client vs server topology | Architecture |
|
||||
| Auto-dimension overlay, figure-weighted price | Impl plan (v1.5) |
|
||||
| Offline capture | PRD non-goal |
|
||||
|
||||
Reference in New Issue
Block a user