giva-discovery / src /designed_spec.py
Gautham98's picture
GIVA Discovery: full app + vectors
73d02ec verified
Raw
History Blame Contribute Delete
11.7 kB
"""
designed_spec.py — the DESIGNED piece's bill of materials (ported from
sketch-to-quote src/lib/spec.js).
The catalogue match (nearest gold SKU by YOUR CLIP embedding) is a factory-reality
ANCHOR: it proves the build is manufacturable and gives a weight scale. But the
quantities belong to the design that was drawn — from the best source available:
'design' — a vision pass counted/sized the stones,
'brief' — the customer's words carried a count,
'anchor' — nothing better, so the closest costed stack stands in.
Note vs the source app: server-side there is no browser pixel-measurement of the
ring opening, so stone sizes fall back to category-typical spans (labelled an
estimate) rather than a measured scale. Ring-size weight scaling is fully ported.
"""
import re
from sieve import SIEVE_TABLE, sieve_by_name, sieve_for_mm, sieve_for_ct
from quote import normalize_lines, total_ct, total_pcs
from market_ref import market_ct_band, typical_span
from ringsize import size_weight_factor, band_share_for, is_ring, REF_SIZE, inner_diameter_mm
from bom import base_sku
WORD_NUM = {"one": 1, "single": 1, "a": 1, "an": 1, "two": 2, "three": 3, "four": 4,
"five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10,
"eleven": 11, "twelve": 12, "fifteen": 15, "sixteen": 16, "twenty": 20}
CENTRE_CT_CAP = 1.5
MAX_CT_PER_STONE = 3
def parse_brief(text, category):
"""Pull an explicit stone count out of the customer's words (conservative)."""
t = " " + re.sub(r"[,.]", " ", str(text or "").lower()) + " "
count = None
centre = False
num = r"(\d+|" + "|".join(WORD_NUM.keys()) + r")"
m = re.search(num + r"\s*(?:tiny |small |round |brilliant |lab[- ]grown |white )*"
r"(?:diamonds?|stones?|solitaires?|cz|gems?)", t)
if m:
count = int(m.group(1)) if m.group(1).isdigit() else WORD_NUM.get(m.group(1))
if re.search(r"\b(solitaire|single stone|one stone|centre stone|center stone)\b", t):
centre = True
if count is None:
count = 1
if re.search(r"\b(pave|pavé|studded|encrusted|cluster|halo|eternity)\b", t) and count is None:
count = None
each = bool(re.search(r"\b(each|both|every)\s+(wing|side|end|petal|leaf|star|corner|link|ear)", t))
if each and count is not None:
count *= 2
pair_words = bool(re.search(r"\b(pair|studs?|earrings?|jhumkas?|hoops?)\b", t))
is_pair = category == "Earrings" or pair_words
return {"count": count, "centre": centre, "isPair": is_pair, "perPieceDoubled": each}
def stone_mm_from_fraction(span_fraction, span_mm):
f = float(span_fraction or 0)
s = float(span_mm or 0)
if not (f > 0) or not (s > 0):
return None
return round(min(0.9, f) * s, 2)
def _dominant_sieve(lines):
by = sorted(normalize_lines(lines), key=lambda l: -l["pcs"])
return by[0]["sieve"] if by else "+1.5-2"
def _largest_sieve(lines):
by = sorted(normalize_lines(lines), key=lambda l: -l["ctPerStone"])
return by[0]["sieve"] if by else "+9-9.5"
def _line_for(sieve, pcs):
s = sieve_by_name(sieve) or SIEVE_TABLE[2]
pcs = max(0, round(pcs))
return {"sieve": s["sieve"], "mm": s["mm"], "ctPerStone": s["ct"],
"pcs": pcs, "ct": round(max(0, pcs) * s["ct"], 3)}
def _anchor_lines(bom):
out = []
for d in (bom.get("diamonds") if bom else []) or []:
pcs = float(d.get("pcs") or 0)
cps = (float(d["ct"]) / pcs) if pcs > 0 else (float(d.get("ct") or 0) or 0.01)
s = sieve_by_name(d.get("sieve")) or sieve_for_ct(cps)
out.append({"sieve": s["sieve"], "mm": s["mm"], "ctPerStone": s["ct"], "pcs": pcs})
return normalize_lines(out)
def _vision_lines(groups, multiplier, anchor_lines, scale, category):
anchor_cts = [l["ctPerStone"] for l in anchor_lines if l["ctPerStone"] > 0]
hi = max(anchor_cts) * 2 if anchor_cts else 0.05
lo = min(anchor_cts) * 0.5 if anchor_cts else 0.003
band = market_ct_band(category)
if band:
hi = min(hi, band["hi"])
lo = min(lo, band["lo"])
trust_size = bool(scale and scale.get("measured"))
merged = {}
for g in groups or []:
pcs = round(float(g.get("count") or 0) * multiplier)
if pcs <= 0:
continue
mm = stone_mm_from_fraction(g.get("span_fraction"), scale.get("spanMm") if scale else None)
s = sieve_for_mm(mm) if mm and mm > 0 else sieve_for_ct(
(band or {}).get("median") or SIEVE_TABLE[2]["ct"])
centre = bool(re.search(r"centre|center|solitaire", str(g.get("role") or ""), re.I))
if trust_size:
if s["ct"] > MAX_CT_PER_STONE:
s = sieve_for_ct(MAX_CT_PER_STONE)
else:
ceiling = max(hi, CENTRE_CT_CAP) if centre else hi
if s["ct"] > ceiling:
s = sieve_for_ct(ceiling)
if s["ct"] < lo:
s = sieve_for_ct(lo)
merged[s["sieve"]] = merged.get(s["sieve"], 0) + pcs
return normalize_lines([_line_for(sieve, pcs) for sieve, pcs in merged.items()])
def build_designed_spec(matches, boms, vision, brief, category, kt, color,
scale=None, ring_size=None, anchor_sku=None):
"""matches: [{sku,title,similarity}]; boms: {base_sku: bom}."""
covered = []
for m in (matches or []):
b = base_sku(m.get("sku"))
bom = (boms or {}).get(b)
if bom:
covered.append({**m, "base": b, "bom": bom})
if not covered:
return None
# Default scale: no server-side pixel measurement, so category-typical span.
if scale is None:
scale = {"spanMm": typical_span(category), "measured": False,
"source": f"typical {str(category).lower()} span"}
pinned = next((m for m in covered if m["base"] == anchor_sku), None) if anchor_sku else None
anchor = pinned or next((m for m in covered if m["bom"].get("diamonds")), None) or covered[0]
a_lines = _anchor_lines(anchor["bom"])
with_gold = [m for m in covered if float(m["bom"].get("gold_gm") or 0) > 0]
wsum = sum(m["similarity"] for m in with_gold)
blend_gm = (sum(m["similarity"] * float(m["bom"]["gold_gm"]) for m in with_gold) / wsum
if wsum else float(anchor["bom"].get("gold_gm") or 0) or None)
prior_gm = (float(pinned["bom"]["gold_gm"]) if pinned and float(pinned["bom"].get("gold_gm") or 0) > 0
else blend_gm)
if not prior_gm:
return None
parsed = parse_brief(brief, category)
is_pair = vision.get("is_pair") if vision and "is_pair" in vision else parsed["isPair"]
# ---- stone stack ----
lines = a_lines
stone_source = "anchor"
dense = False
count_conf = str((vision or {}).get("count_confidence", "high")).lower()
count_trusted = count_conf == "high"
if vision and vision.get("stone_groups") and (count_trusted or parsed["count"] is None):
mult = 2 if is_pair and (float(vision.get("pieces_visible") or 2) < 2) else 1
vl = _vision_lines(vision["stone_groups"], mult, a_lines, scale, category)
if total_pcs(vl) > 0:
lines = vl
stone_source = "design"
if not count_trusted:
dense = True
elif parsed["count"] is not None:
want = parsed["count"] * (2 if is_pair and not parsed["perPieceDoubled"] else 1)
band = market_ct_band(category)
small = bool(re.search(r"\b(small|tiny|delicate|dainty|micro|petite)\b", str(brief or ""), re.I))
def accent(sieve):
s = sieve_by_name(sieve) or SIEVE_TABLE[2]
if band and small:
s = sieve_for_ct(band["median"])
elif band and s["ct"] > band["hi"]:
s = sieve_for_ct(band["hi"])
return s["sieve"]
if parsed["centre"] and want > 1:
lines = normalize_lines([_line_for(_largest_sieve(a_lines), 1),
_line_for(accent(_dominant_sieve(a_lines)), want - 1)])
elif parsed["centre"]:
lines = normalize_lines([_line_for(_largest_sieve(a_lines), want)])
else:
lines = normalize_lines([_line_for(accent(_dominant_sieve(a_lines)), want)])
stone_source = "brief"
# ---- gold weight ----
gold_gm = prior_gm
weight_source = "neighbours"
ratio = float((vision or {}).get("metal_ratio") or 0)
if ratio > 0 and float(anchor["bom"].get("gold_gm") or 0) > 0:
scaled = float(anchor["bom"]["gold_gm"]) * ratio
blended = (scaled * prior_gm) ** 0.5
gold_gm = min(max(blended, prior_gm * 0.6), prior_gm * 1.8)
weight_source = "design"
# ---- ring size ----
size_info = None
if is_ring(category) and ring_size:
b_share = band_share_for({"lines": lines})
thickness_mm = 1.4
vm = (vision or {}).get("metal", {})
if float(vm.get("thickness_fraction") or 0) > 0 and scale.get("spanMm"):
thickness_mm = min(3, max(0.6, vm["thickness_fraction"] * scale["spanMm"]))
factor = size_weight_factor(ring_size, REF_SIZE, b_share, thickness_mm)
gold_gm *= factor
size_info = {"size": ring_size, "refSize": REF_SIZE, "factor": round(factor, 3),
"innerMm": inner_diameter_mm(ring_size), "bandShare": b_share,
"thicknessMm": round(thickness_mm, 1)}
gold_gm = round(gold_gm, 2)
notes = []
if stone_source == "anchor" and not vision:
notes.append("Stone count taken from the closest costed design — say how many stones you want, or edit the stack.")
if dense:
notes.append("Dense stone work — the count is an estimate; check against the drawing.")
if stone_source == "design" and scale and not scale.get("measured") and total_pcs(lines) > 0:
notes.append(f"Stone sizes are scaled off a {scale.get('source')}, not a measurement"
+ (" — pick a ring size to measure them exactly" if is_ring(category) else ""))
if (vision or {}).get("structural_defects"):
notes.append("Design flagged: " + "; ".join(vision["structural_defects"]))
return {
"category": (vision or {}).get("category") or (anchor["bom"].get("category") if category == "Any" else category) or category,
"units": 2 if is_pair else 1,
"goldGm": gold_gm,
"goldKt": kt,
"goldColor": color,
"lines": lines,
"ctTotal": total_ct(lines),
"pcsTotal": total_pcs(lines),
"stoneSource": stone_source,
"stoneEstimated": dense,
"sizeInfo": size_info,
"scale": scale,
"weightSource": weight_source,
"notes": notes,
"anchor": {
"sku": anchor["base"], "title": anchor.get("title"),
"sim": anchor["similarity"], "goldGm": float(anchor["bom"].get("gold_gm") or 0) or None,
"kt": anchor["bom"].get("gold_kt"), "color": anchor["bom"].get("gold_color"),
"lines": a_lines, "ct": total_ct(a_lines), "pcs": total_pcs(a_lines),
"pinned": bool(pinned),
},
"candidates": [{
"sku": m["base"], "title": m.get("title"), "sim": m["similarity"],
"goldGm": float(m["bom"].get("gold_gm") or 0) or None,
"pcs": total_pcs(_anchor_lines(m["bom"])), "ct": total_ct(_anchor_lines(m["bom"])),
} for m in covered],
"coveredCount": len(covered),
}
SOURCE_LABEL = {
"design": "from your design", "brief": "from your brief", "anchor": "from closest match",
"neighbours": "averaged from lookalikes", "edited": "edited by you",
}