← back to Designer Wallcoverings
title-fallback.py: synthesize compliant titles for 1580 title-less Lily SKUs (MFR-SKU fallback + Morris LIS map, settlement-flagged, needs-pattern-name reviewable)
f1960768f482a0c9a682cf46c21015625cbd6358 · 2026-07-01 15:04:12 -0700 · Steve
Files touched
A onboarding/sangetsu-lilycolor/scripts/title-fallback.py
Diff
commit f1960768f482a0c9a682cf46c21015625cbd6358
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 1 15:04:12 2026 -0700
title-fallback.py: synthesize compliant titles for 1580 title-less Lily SKUs (MFR-SKU fallback + Morris LIS map, settlement-flagged, needs-pattern-name reviewable)
---
.../sangetsu-lilycolor/scripts/title-fallback.py | 228 +++++++++++++++++++++
1 file changed, 228 insertions(+)
diff --git a/onboarding/sangetsu-lilycolor/scripts/title-fallback.py b/onboarding/sangetsu-lilycolor/scripts/title-fallback.py
new file mode 100644
index 00000000..4c54f79c
--- /dev/null
+++ b/onboarding/sangetsu-lilycolor/scripts/title-fallback.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python3
+"""
+title-fallback.py — synthesize compliant EN titles for the title-less Lilycolor SKUs.
+
+DTD VERDICT A (committed) — for the 1,580 records where title_ja is null (NOT
+recoverable from the website feed, swatch OCR, or the PDF price tables):
+ (a) MFR-SKU-as-pattern-name fallback titler for the ~1,556 bulk (LV/LW/LMT/…);
+ (b) proper Morris & Co name match for the 24 LIS "Import Selection" records
+ (LIS42001 = "Bird & Pomegranate" is known; rest fall back to
+ "Morris & Co Import <MfrSku>"), flagged settlement_required (birds/foliage);
+ (c) an optional vendor re-scrape drafted to pending-approval as a later upgrade.
+
+OFFLINE / non-gated / idempotent / JSONL-only. Reads the unified staging file +
+the local design-enrichment .final.jsonl, and EMITS a separate fallback-title
+file — it NEVER mutates the unified staging and NEVER flips a gate.
+
+Title format per DW rules: "<PatternNameOrMfrSku> <MaterialOrStyleDescriptor> | Lilycolor"
+ - pattern name source order:
+ LIS → known Morris map, else "Morris & Co Import <MfrSku>"
+ else → MFR SKU itself (last-resort pattern name, per CLAUDE.md fallback #4)
+ - descriptor from enrichment material || styles[0] || patterns[0] (Title Case), else omit
+ - HARD guards: Title Case, "Wallcovering" never "Wallpaper", never "Unknown".
+ - every emitted record carries needs_pattern_name=True so these stay reviewable.
+
+Output: staging/lilycolor-titles-fallback-063026A.jsonl
+ one row per title-less SKU:
+ {sku, title_en, pattern_en, descriptor_en, source, needs_pattern_name,
+ settlement_required?}
+
+Cost: $0 (local).
+"""
+import json
+import os
+import re
+import sys
+
+HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+STAGING = os.path.join(HERE, "staging")
+
+UNIFIED = os.path.join(STAGING, "lilycolor-unified-staging.jsonl")
+ENRICH = os.path.join(STAGING, "enrichment-full-063026A.final.jsonl")
+if not os.path.exists(ENRICH):
+ ENRICH = os.path.join(STAGING, "enrichment-full-063026A.jsonl")
+
+OUT = os.path.join(STAGING, "lilycolor-titles-fallback-063026A.jsonl")
+
+BRAND = "Lilycolor"
+
+# Known Morris & Co pattern names for LIS "Import Selection" SKUs.
+# LIS42001 = "Bird & Pomegranate" is confirmed (task + enrichment desc:
+# "a bird perched on branches ... and a pomegranate"). The rest have NO reliable
+# local SKU->pattern crosswalk (Lily's LIS codes are Lilycolor's own catalog
+# numbers, not Morris's product SKUs), so they fall to the branded-import form.
+MORRIS_NAME_MAP = {
+ "LIS42001": "Bird & Pomegranate",
+}
+
+# Words to keep lowercase in Title Case (unless first word).
+_SMALL = {"a", "an", "the", "and", "but", "or", "for", "nor", "on", "at",
+ "to", "by", "in", "of", "with", "as"}
+_BANNED_RE = re.compile(r"\bwallpaper(s)?\b", re.IGNORECASE)
+
+
+def title_case(s):
+ """Title Case with small-word exceptions; first word always capitalized.
+ Preserves already-mixed tokens like 'McKenzie' and ampersand joins."""
+ if not s:
+ return s
+ words = s.split()
+ out = []
+ for i, w in enumerate(words):
+ lw = w.lower()
+ # keep tokens that already have internal caps (McKenzie, BN) intact
+ if w != lw and w != w.lower() and any(c.isupper() for c in w[1:]):
+ out.append(w)
+ continue
+ if i != 0 and lw in _SMALL:
+ out.append(lw)
+ else:
+ out.append(w[:1].upper() + w[1:] if w else w)
+ return " ".join(out)
+
+
+def clean_wallpaper(s):
+ if not s:
+ return s
+ return _BANNED_RE.sub(lambda m: "Wallcovering" + ("s" if m.group(1) else ""), s)
+
+
+def descriptor_from_enrich(e):
+ """material || styles[0] || patterns[0], Title Cased, else None."""
+ if not e:
+ return None
+ for src in (e.get("material"),):
+ if src and str(src).strip():
+ return title_case(str(src).strip())
+ for listkey in ("styles", "patterns"):
+ vals = e.get(listkey)
+ if isinstance(vals, list) and vals:
+ v = str(vals[0]).strip()
+ if v:
+ return title_case(v)
+ return None
+
+
+def load_by_key(path, key):
+ out = {}
+ if not os.path.exists(path):
+ return out
+ with open(path) as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ d = json.loads(line)
+ except json.JSONDecodeError:
+ continue
+ k = d.get(key)
+ if k:
+ out[k] = d
+ return out
+
+
+def assemble_title(pattern_en, descriptor_en):
+ left = pattern_en
+ if descriptor_en:
+ left = f"{pattern_en} {descriptor_en}"
+ title = f"{left} | {BRAND}"
+ title = clean_wallpaper(title)
+ return title
+
+
+def main():
+ enrich = load_by_key(ENRICH, "sku")
+
+ n_in = 0
+ n_out = 0
+ n_lis = 0
+ n_morris_named = 0
+ n_settlement = 0
+ n_with_descriptor = 0
+ seen = set()
+
+ with open(UNIFIED) as fin, open(OUT, "w") as fout:
+ for line in fin:
+ line = line.strip()
+ if not line:
+ continue
+ rec = json.loads(line)
+ n_in += 1
+
+ # Only the title-less universe (title_ja is null) gets a fallback.
+ if rec.get("title_ja") is not None:
+ continue
+
+ sku = rec.get("mfr_sku")
+ if not sku or sku in seen:
+ continue
+ seen.add(sku)
+
+ prefix = rec.get("mfr_prefix") or ""
+ e = enrich.get(sku)
+ descriptor_en = descriptor_from_enrich(e)
+
+ settlement_required = False
+ if prefix == "LIS":
+ n_lis += 1
+ mapped = MORRIS_NAME_MAP.get(sku)
+ if mapped:
+ pattern_en = mapped
+ source = "morris-known-map"
+ n_morris_named += 1
+ else:
+ # branded import fallback — never "Unknown", never bare SKU
+ pattern_en = f"Morris & Co Import {sku}"
+ source = "morris-import-fallback"
+ # LIS = Morris & Co designs: birds / foliage / pomegranate motifs
+ # → the settlement gate MUST run before any publish. Flag it; do
+ # NOT publish here.
+ settlement_required = True
+ n_settlement += 1
+ else:
+ # Bulk (LV/LW/LMT/LL*/…): MFR SKU is the last-resort pattern name
+ # (CLAUDE.md fallback order #4). Never "Unknown".
+ pattern_en = sku
+ source = "mfr-sku-fallback"
+
+ pattern_en = title_case(pattern_en)
+ title_en = assemble_title(pattern_en, descriptor_en)
+
+ # HARD guards — refuse to emit a bad title.
+ low = title_en.lower()
+ assert "unknown" not in low, f"BLOCKED unknown in title for {sku}"
+ assert "wallpaper" not in low, f"BLOCKED wallpaper in title for {sku}"
+
+ if descriptor_en:
+ n_with_descriptor += 1
+
+ row = {
+ "sku": sku,
+ "mfr_prefix": prefix,
+ "title_en": title_en,
+ "pattern_en": pattern_en,
+ "descriptor_en": descriptor_en,
+ "source": source,
+ "needs_pattern_name": True, # reviewable — synthesized, not scraped
+ "_cost": "$0 (local)",
+ }
+ if settlement_required:
+ row["settlement_required"] = True
+
+ fout.write(json.dumps(row, ensure_ascii=False) + "\n")
+ n_out += 1
+
+ print(f"title-less records scanned : {n_in} total unified rows")
+ print(f"fallback titles written : {n_out} -> {os.path.relpath(OUT, HERE)}")
+ print(f" LIS (Morris & Co) : {n_lis} ({n_morris_named} known-named, "
+ f"{n_lis - n_morris_named} branded-import fallback)")
+ print(f" settlement_required flag : {n_settlement} (LIS birds/foliage — GATE before publish)")
+ print(f" with enrichment descriptor: {n_with_descriptor}")
+ print(f" all flagged needs_pattern_name=True (reviewable)")
+ print(f" cost: $0 (local)")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
← 2eb75f4c merge-staging: read canonical enrichment-full-063026A.final.
·
back to Designer Wallcoverings
·
merge-staging: fold DTD-A fallback titles into merged set (t 151d386d →