fix(moodboard): gate starter kits + flats on the garment-visual opt-in

Technical flat sketches generated unconditionally and the Design Starter
Kits section was assembled regardless of what the designer chose — so
picking only "print & fabric swatches" still produced garment flats + a
full starter-kit block they never asked for (wasted image spend on a
garment design they won't use).

Starter kits are a garment-design deliverable, so tie them to the garment
opt-in: wants_garments = generate_visuals AND "concepts" in visual_types
now gates construction-spec drafting, flat-sketch generation, and the
starter-kit assembly. Swatches-only runs skip all of it (frontend already
hides an empty section); "garment concepts"/"everything" get the full kit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ajay Sable
2026-07-28 11:32:25 +05:30
co-authored by Claude Opus 4.8
parent a51bf2b3f4
commit 2a5b95e37e
2 changed files with 41 additions and 27 deletions
+40 -26
View File
@@ -1101,12 +1101,23 @@ async def run_synthesis_job(decode_id: str, embedding_svc: EmbeddingService | No
decode.sapl_grounding = json.dumps(grounding) decode.sapl_grounding = json.dumps(grounding)
db.commit() db.commit()
# Draft per-hero-piece construction specs (deepseek) — always (cheap, high value). # Garment-design artifacts (construction specs, technical flats, concept
try: # renders, starter kits) are only worth producing when the designer opted
specs = await _draft_construction_specs(brief.get("key_silhouettes") or []) # into GARMENT visuals — i.e. picked "garment concepts" or "everything". If
except Exception as e: # they only wanted print/fabric swatches, skip them entirely: generating a
logger.warning(f"Moodboard synthesis {decode_id}: spec drafting failed: {e}") # garment design the designer won't use is wasted effort (and image spend).
specs = [] vt = set(steer.get("visual_types") or ["concepts", "prints", "fabrics"])
wants_garments = bool(steer.get("generate_visuals", True)) and "concepts" in vt
# Draft per-hero-piece construction specs (deepseek) — only when the starter
# kits that consume them will actually be shown.
specs: list = []
if wants_garments:
try:
specs = await _draft_construction_specs(brief.get("key_silhouettes") or [])
except Exception as e:
logger.warning(f"Moodboard synthesis {decode_id}: spec drafting failed: {e}")
specs = []
# Visuals — only if the designer asked for them (gated by the steer). # Visuals — only if the designer asked for them (gated by the steer).
concept_filenames: list = [] concept_filenames: list = []
@@ -1114,7 +1125,6 @@ async def run_synthesis_job(decode_id: str, embedding_svc: EmbeddingService | No
fabric_swatches: list = [] fabric_swatches: list = []
flat_by_index: dict = {} flat_by_index: dict = {}
if steer.get("generate_visuals", True): if steer.get("generate_visuals", True):
vt = set(steer.get("visual_types") or ["concepts", "prints", "fabrics"])
decode.status = "visualising" decode.status = "visualising"
decode.current_page_status = "Visualising concepts, flats & swatches…" decode.current_page_status = "Visualising concepts, flats & swatches…"
db.commit() db.commit()
@@ -1122,9 +1132,10 @@ async def run_synthesis_job(decode_id: str, embedding_svc: EmbeddingService | No
if "concepts" in vt: if "concepts" in vt:
for prompt in _build_concept_image_prompts(brief): for prompt in _build_concept_image_prompts(brief):
image_items.append({"kind": "concept", "prompt": prompt}) image_items.append({"kind": "concept", "prompt": prompt})
# Technical flats for the starter kits (front/back line drawings). # Technical flats for the starter kits (front/back line drawings) —
for fp in _build_flat_sketch_prompts(brief.get("key_silhouettes") or [], specs): # garment artifacts, so tied to the same 'garment concepts' opt-in.
image_items.append({"kind": "flat", "index": fp["index"], "prompt": fp["prompt"]}) for fp in _build_flat_sketch_prompts(brief.get("key_silhouettes") or [], specs):
image_items.append({"kind": "flat", "index": fp["index"], "prompt": fp["prompt"]})
if "prints" in vt: if "prints" in vt:
for swatch in _build_print_swatch_prompts(brief): for swatch in _build_print_swatch_prompts(brief):
image_items.append({"kind": "swatch", "label": swatch["label"], "type": swatch["type"], "prompt": swatch["prompt"]}) image_items.append({"kind": "swatch", "label": swatch["label"], "type": swatch["type"], "prompt": swatch["prompt"]})
@@ -1138,22 +1149,25 @@ async def run_synthesis_job(decode_id: str, embedding_svc: EmbeddingService | No
fabric_swatches = [{"label": g["label"], "category": g["category"], "image": g["image"]} for g in generated if g["kind"] == "fabric"] fabric_swatches = [{"label": g["label"], "category": g["category"], "image": g["image"]} for g in generated if g["kind"] == "fabric"]
flat_by_index = {g["index"]: g["image"] for g in generated if g.get("kind") == "flat" and g.get("image")} flat_by_index = {g["index"]: g["image"] for g in generated if g.get("kind") == "flat" and g.get("image")}
# Assemble the per-hero-piece design starter kits (concept + flat + spec + fabric). # Assemble the per-hero-piece design starter kits (concept + flat + spec +
sils = brief.get("key_silhouettes") or [] # fabric) — a garment-design deliverable, so ONLY when the designer opted into
avail = (grounding.get("have") or []) + (grounding.get("similar") or []) # garment visuals. Swatches-only runs skip it entirely (no empty garment kit).
ground_sils = {s.get("silhouette"): s for s in grounding.get("silhouettes", [])} starter_kits: list = []
starter_kits = [] if wants_garments:
for i, sil in enumerate(sils[:_CONCEPT_IMAGE_COUNT]): sils = brief.get("key_silhouettes") or []
name = (sil.get("name") if isinstance(sil, dict) else str(sil)) or "" avail = (grounding.get("have") or []) + (grounding.get("similar") or [])
starter_kits.append({ ground_sils = {s.get("silhouette"): s for s in grounding.get("silhouettes", [])}
"name": name, for i, sil in enumerate(sils[:_CONCEPT_IMAGE_COUNT]):
"description": (sil.get("description") if isinstance(sil, dict) else "") or "", name = (sil.get("name") if isinstance(sil, dict) else str(sil)) or ""
"spec": specs[i] if i < len(specs) else {f: "—" for f in _SPEC_FIELDS}, starter_kits.append({
"concept_image": concept_filenames[i] if i < len(concept_filenames) else None, "name": name,
"flat_image": flat_by_index.get(i), "description": (sil.get("description") if isinstance(sil, dict) else "") or "",
"suggested_fabric": (avail[i % len(avail)] if avail else None), "spec": specs[i] if i < len(specs) else {f: "—" for f in _SPEC_FIELDS},
"on_dna": bool(ground_sils.get(name, {}).get("on_dna")), "concept_image": concept_filenames[i] if i < len(concept_filenames) else None,
}) "flat_image": flat_by_index.get(i),
"suggested_fabric": (avail[i % len(avail)] if avail else None),
"on_dna": bool(ground_sils.get(name, {}).get("on_dna")),
})
decode.starter_kits = json.dumps(starter_kits) decode.starter_kits = json.dumps(starter_kits)
decode.concept_images = json.dumps(concept_filenames) decode.concept_images = json.dumps(concept_filenames)
+1 -1
View File
@@ -2,7 +2,7 @@
**Last updated:** 2026-07-28 (**OpenCode Go — a shared multi-model layer added alongside Gemini; Steps 1–3b SHIPPED + verified live on UAT.** Next direction after the RBAC/notifications work below: give SAPL a **multi-model brain**. **OpenCode Go** is an OpenAI-compatible gateway to ~23 models across families (minimax / deepseek / glm / kimi / qwen / mimo); we adopt it as an **ADDITIVE second engine — Gemini paths are untouched** (`agent.py`, scraper, fabric extraction, mail all unchanged). **The vision is bigger than the source plan** (`docs/plans/hermes-moa-for-sapl-plan.md`, authored by an external "Diablo/Hermes Agent" — and, notably, written WITHOUT running the API, so its response-parsing was wrong): not just a chatbot MOA endpoint but **per-feature model routing by performance** + a **HERMES orchestrator on top** that routes/manages/learns — Scraper→minimax/deepseek · Moodboard decode→deepseek/glm-5 · Chatbot→gemini/deepseek · **Studio stays on AIBrandSolutions (unchanged)** · HERMES→MOA (parallel refs + aggregator) over all of it. **Architecture correction vs the plan:** the plan buried the client under `services/hermes/`, but since scraper+moodboard also call it, it belongs CENTRALLY → new **`services/llm/`** package (`opencode_client.py` now; `routing.py` + `moa_orchestrator.py` next) so every feature reuses one client. **Build order:** (1) ✅ **foundation — DONE** · (2) ✅ **chatbot `/ask-with-moa` MOA agent — DONE + verified green (2/2 live)** · (2b) ✅ **frontend "Deep Research" toggle — DONE** (`AskHermes.tsx` "Deep" pill next to Ask → hits `/ask-with-moa`; loading copy "consulting several models… ~15–60s"; renders a `model_trace` summary "Deep Research · N models consulted · Ns · fallback used"; `hermesAskMoa` + `HermesModelTrace*` types in `client.ts`; `tsc -b` + build clean) · (3a) ✅ **per-feature routing + scraper→minimax — DONE** · (3b) ✅ **moodboard→deepseek + interactive analyze→ask→tailor flow — DONE** · (4) HERMES-on-top orchestrator (routing brain that learns). **Last updated:** 2026-07-28 (**OpenCode Go — a shared multi-model layer added alongside Gemini; Steps 1–3b SHIPPED + verified live on UAT.** Next direction after the RBAC/notifications work below: give SAPL a **multi-model brain**. **OpenCode Go** is an OpenAI-compatible gateway to ~23 models across families (minimax / deepseek / glm / kimi / qwen / mimo); we adopt it as an **ADDITIVE second engine — Gemini paths are untouched** (`agent.py`, scraper, fabric extraction, mail all unchanged). **The vision is bigger than the source plan** (`docs/plans/hermes-moa-for-sapl-plan.md`, authored by an external "Diablo/Hermes Agent" — and, notably, written WITHOUT running the API, so its response-parsing was wrong): not just a chatbot MOA endpoint but **per-feature model routing by performance** + a **HERMES orchestrator on top** that routes/manages/learns — Scraper→minimax/deepseek · Moodboard decode→deepseek/glm-5 · Chatbot→gemini/deepseek · **Studio stays on AIBrandSolutions (unchanged)** · HERMES→MOA (parallel refs + aggregator) over all of it. **Architecture correction vs the plan:** the plan buried the client under `services/hermes/`, but since scraper+moodboard also call it, it belongs CENTRALLY → new **`services/llm/`** package (`opencode_client.py` now; `routing.py` + `moa_orchestrator.py` next) so every feature reuses one client. **Build order:** (1) ✅ **foundation — DONE** · (2) ✅ **chatbot `/ask-with-moa` MOA agent — DONE + verified green (2/2 live)** · (2b) ✅ **frontend "Deep Research" toggle — DONE** (`AskHermes.tsx` "Deep" pill next to Ask → hits `/ask-with-moa`; loading copy "consulting several models… ~15–60s"; renders a `model_trace` summary "Deep Research · N models consulted · Ns · fallback used"; `hermesAskMoa` + `HermesModelTrace*` types in `client.ts`; `tsc -b` + build clean) · (3a) ✅ **per-feature routing + scraper→minimax — DONE** · (3b) ✅ **moodboard→deepseek + interactive analyze→ask→tailor flow — DONE** · (4) HERMES-on-top orchestrator (routing brain that learns).
**MOODBOARD library-match ACCURACY fix (2026-07-28):** Ajay flagged the "Fabric direction" cards were showing in-house garment matches for fabrics SAPL doesn't stock (satin/suede/tweed) — "make our embedding similarity accurate to the point." **Root-caused via live calibration against the real chromadb + gemini-embedding-2 index:** (1) cosine similarity CANNOT judge fabric availability — a suede query (0 in library) scores 0.69, *higher* than cotton poplin (in library) at 0.66, and pure nonsense scores 0.51; so availability must be a DB fact, not a score. (2) the grounding's fabric matcher split phrases into words and matched generic filler (`blend`→11 hits, `soft`→11, `coloured`→2) against the fabric table's free-text `description`/`construction` → everything read as a match. (3) **a latent batch-embedding bug:** `gemini-embedding-2-preview` returns only ONE embedding for a `contents=[N texts]` call (concatenates them), so `embed_texts` collapsed N per-item queries → 1 vector and a `zip` dropped all but the first item — per-item silhouette/print/fabric matching was silently broken. **Fixes (backend):** new **`_fabric_status`** (vocabulary-driven: extracts real material nouns, drops descriptors, matches STRUCTURED identity fields only — composition/weave/texture/pattern, never free-text) replaces the loose `check_fabric_library` call in `_build_sapl_grounding`; **gap-gate** in `_match_library_per_item` (a sourcing-gap fabric surfaces ZERO in-house garments); **`embed_texts` fixed** to embed per-text (order-aligned) — restoring silhouette/print matching; **calibrated similarity floor** `_enrich_matches(min_sim=…)` (config `moodboard_match_min_similarity=0.58`, env `MOODBOARD_MATCH_MIN_SIM` — nonsense 0.51 vs real 0.58-0.71) on both `_match_library` + per-item. **Frontend:** fabric cards now show a subtle library-status hint (◆ In library / ◈ Similar in library / ◇ Not in library — develop) from `sapl_grounding.fabrics[].status`, so an empty (gap) card doesn't read as broken. Validated live vs real DB: tweed/suede/terry/fleece→gap (no in-house), cotton poplin/twill→have, satin/organza+viscose→similar; per-item now returns fabric/print/silhouette matches correctly; archive grid floor-passes ~0.77-0.79. `tsc -b`+build+py_compile clean. **Note:** SIMILAR fabrics (fibre stocked, not exact construction) still show in-house — flip to have-only if desired. The scraper's `check_fabric_library` has the same loose-match trait (unchanged; scoped to moodboard as asked). **MOODBOARD library-match ACCURACY fix (2026-07-28):** Ajay flagged the "Fabric direction" cards were showing in-house garment matches for fabrics SAPL doesn't stock (satin/suede/tweed) — "make our embedding similarity accurate to the point." **Root-caused via live calibration against the real chromadb + gemini-embedding-2 index:** (1) cosine similarity CANNOT judge fabric availability — a suede query (0 in library) scores 0.69, *higher* than cotton poplin (in library) at 0.66, and pure nonsense scores 0.51; so availability must be a DB fact, not a score. (2) the grounding's fabric matcher split phrases into words and matched generic filler (`blend`→11 hits, `soft`→11, `coloured`→2) against the fabric table's free-text `description`/`construction` → everything read as a match. (3) **a latent batch-embedding bug:** `gemini-embedding-2-preview` returns only ONE embedding for a `contents=[N texts]` call (concatenates them), so `embed_texts` collapsed N per-item queries → 1 vector and a `zip` dropped all but the first item — per-item silhouette/print/fabric matching was silently broken. **Fixes (backend):** new **`_fabric_status`** (vocabulary-driven: extracts real material nouns, drops descriptors, matches STRUCTURED identity fields only — composition/weave/texture/pattern, never free-text) replaces the loose `check_fabric_library` call in `_build_sapl_grounding`; **gap-gate** in `_match_library_per_item` (a sourcing-gap fabric surfaces ZERO in-house garments); **`embed_texts` fixed** to embed per-text (order-aligned) — restoring silhouette/print matching; **calibrated similarity floor** `_enrich_matches(min_sim=…)` (config `moodboard_match_min_similarity=0.58`, env `MOODBOARD_MATCH_MIN_SIM` — nonsense 0.51 vs real 0.58-0.71) on both `_match_library` + per-item. **Frontend:** fabric cards now show a subtle library-status hint (◆ In library / ◈ Similar in library / ◇ Not in library — develop) from `sapl_grounding.fabrics[].status`, so an empty (gap) card doesn't read as broken. Validated live vs real DB: tweed/suede/terry/fleece→gap (no in-house), cotton poplin/twill→have, satin/organza+viscose→similar; per-item now returns fabric/print/silhouette matches correctly; archive grid floor-passes ~0.77-0.79. `tsc -b`+build+py_compile clean. **Note:** SIMILAR fabrics (fibre stocked, not exact construction) still show in-house — flip to have-only if desired. The scraper's `check_fabric_library` has the same loose-match trait (unchanged; scoped to moodboard as asked). **Then fixed a starter-kit gating bug (2026-07-28):** technical flats generated + the whole Design-Starter-Kits section assembled UNCONDITIONALLY, so a designer who picked only "print & fabric swatches" still got garment flats + kits they never asked for. Starter kits are a garment-design deliverable → now gated on the garment-visual opt-in: new `wants_garments = generate_visuals AND "concepts" in visual_types` gates construction-spec drafting, flat-sketch generation, AND starter-kit assembly (empty `starter_kits` → frontend already hides the section). Swatches-only runs now skip all garment-image spend; "garment concepts"/"everything" still get the full kit. py_compile clean.
**OpenCode Go multi-model layer — shipped & on UAT (2026-07-24→25, commit `4c9486d`; iteration detail archived → STATUS-ARCHIVE.md):** **Step 2 — chatbot MOA agent** (`services/hermes/agent_moa.py`, `POST /api/hermes/ask-with-moa`, opt-in "Deep Research" toggle) live-verified (3 parallel refs → aggregator, graceful degrade + `kimi→glm→minimax` aggregator fallback chain). **Step 3a — per-feature routing** (`services/llm/routing.py`, env-overridable) + **scraper reasoning → `minimax-m3`** (Gemini loop is the automatic fallback; vision stays Gemini). **Scraper overhauled:** all-8-brand sweep (`scrape_all_brands`; user picks via UI tabs), **gender-aware** (women/men/kids category URLs), **grounded in SAPL's own fabric+garment libraries** before/after scraping (make-now vs source vs on-DNA), **Pinterest crash root-caused + fixed** (Camoufox driver `pageError.location` bug → `services/playwright_patch.py`, re-applied on boot), **calm bento report** (`BentoReport.tsx` — colour band → trend signals → what-to-make → feasibility scorecard, dense analysis behind a toggle). ⚠ **Next** stays IP-blocked (Akamai; needs a residential proxy — awaiting Ajay's key). Live-proven: Primark AW26 menswear → 53 items across 5 brands + Pinterest, grounded report. **OpenCode Go multi-model layer — shipped & on UAT (2026-07-24→25, commit `4c9486d`; iteration detail archived → STATUS-ARCHIVE.md):** **Step 2 — chatbot MOA agent** (`services/hermes/agent_moa.py`, `POST /api/hermes/ask-with-moa`, opt-in "Deep Research" toggle) live-verified (3 parallel refs → aggregator, graceful degrade + `kimi→glm→minimax` aggregator fallback chain). **Step 3a — per-feature routing** (`services/llm/routing.py`, env-overridable) + **scraper reasoning → `minimax-m3`** (Gemini loop is the automatic fallback; vision stays Gemini). **Scraper overhauled:** all-8-brand sweep (`scrape_all_brands`; user picks via UI tabs), **gender-aware** (women/men/kids category URLs), **grounded in SAPL's own fabric+garment libraries** before/after scraping (make-now vs source vs on-DNA), **Pinterest crash root-caused + fixed** (Camoufox driver `pageError.location` bug → `services/playwright_patch.py`, re-applied on boot), **calm bento report** (`BentoReport.tsx` — colour band → trend signals → what-to-make → feasibility scorecard, dense analysis behind a toggle). ⚠ **Next** stays IP-blocked (Akamai; needs a residential proxy — awaiting Ajay's key). Live-proven: Primark AW26 menswear → 53 items across 5 brands + Pinterest, grounded report.