← back to Dw Pairs Well
auto-save: 2026-07-12T23:51:20 (1 files) — server.js
f1897392fc668d396dd4df3cce8472210c5ce675 · 2026-07-12 23:51:28 -0700 · Steve Abrams
Files touched
Diff
commit f1897392fc668d396dd4df3cce8472210c5ce675
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 23:51:28 2026 -0700
auto-save: 2026-07-12T23:51:20 (1 files) — server.js
---
server.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 57 insertions(+), 4 deletions(-)
diff --git a/server.js b/server.js
index 98f69b9..dbbdc66 100644
--- a/server.js
+++ b/server.js
@@ -308,7 +308,45 @@ async function clipSimilarTop(source, k) {
}
}
-function pickPairs(source, candidates, k = 24) {
+// CLIP style-world sub-score for PAIRS. A coordinate should share the hero's style
+// world WITHOUT visually competing — so mid-similarity is rewarded and near-duplicates
+// (same pattern re-colored) are penalized. Neutral (0) when the candidate has no
+// vector, so the un-embedded tail is never punished.
+const CLIP_NEAR_DUP = 0.88; // ≥ this = visually the same design → penalize
+const CLIP_STYLE_HI = 0.55; // sweet spot lower bound → +4
+const CLIP_STYLE_LO = 0.45; // weak style kinship → +2
+function clipPairBonus(cos) {
+ if (cos == null || !Number.isFinite(cos)) return 0;
+ if (cos >= CLIP_NEAR_DUP) return -4;
+ if (cos >= CLIP_STYLE_HI) return 4;
+ if (cos >= CLIP_STYLE_LO) return 2;
+ return 0;
+}
+
+// Bulk source-vs-candidates cosine map from the visual-search service.
+// Returns {} on any miss (service down / source not embedded) — blend degrades to
+// the pure tag+palette scorer, never blocks the response.
+async function clipPairwiseMap(source, candidates) {
+ if (!source.dw_sku) return {};
+ try {
+ const skus = candidates.map(c => c.dw_sku).filter(Boolean);
+ if (!skus.length) return {};
+ const r = await fetch(`${VISUAL_SEARCH_URL}/pairwise`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ dw_sku: source.dw_sku, candidates: skus }),
+ signal: AbortSignal.timeout(4000)
+ });
+ if (!r.ok) return {};
+ const d = await r.json();
+ return (d && d.ok && d.cosines) ? d.cosines : {};
+ } catch (e) {
+ console.error('clipPairwiseMap fallback →', e.message);
+ return {};
+ }
+}
+
+function pickPairs(source, candidates, k = 24, clipMap = null) {
const sCls = classify(parseTags(source.tags));
const sourcePalette = paletteOf(source);
@@ -321,15 +359,18 @@ function pickPairs(source, candidates, k = 24) {
// Strong bias: motif is the whole point of this feature.
const motifBonus = motif ? 6 : 0;
- const totalScore = tagScore.score + overlap.score + motifBonus;
+ const clipCos = clipMap ? clipMap[c.dw_sku] : null;
+ const clipBonus = clipPairBonus(clipCos);
+ const totalScore = tagScore.score + overlap.score + motifBonus + clipBonus;
const why = [];
if (motif) why.push(`Coordinating ${motif}`);
if (overlap.pickedUp.length) why.push(`Picks up: ${overlap.pickedUp.slice(0, 4).join(', ')}`);
+ if (clipBonus > 0) why.push(`Same style world (CLIP ${Number(clipCos).toFixed(2)})`);
if (overlap.avgDE != null) why.push(`palette ΔE avg ${overlap.avgDE.toFixed(1)}`);
if (tagScore.why.length) why.push(tagScore.why[0]);
- return { ...c, _score: totalScore, _why: why, _palette: candPalette, _dE: overlap.avgDE, _motif: motif, _pickedUp: overlap.pickedUp, _overlapScore: overlap.score };
+ return { ...c, _score: totalScore, _why: why, _palette: candPalette, _dE: overlap.avgDE, _motif: motif, _pickedUp: overlap.pickedUp, _overlapScore: overlap.score, _clipCos: clipCos };
});
// HARD FILTER per Steve's rule: every coordinate MUST use colors from the source palette.
@@ -1034,7 +1075,18 @@ app.get('/api/pairs', async (req, res) => {
// Limit query: caller can request up to 24 via ?limit=
const requestedK = Math.max(1, Math.min(24, parseInt(req.query.limit, 10) || 24));
const candidates = await loadCandidates(source, 1200);
- const top = pickPairs(source, candidates, requestedK);
+
+ // ?engine=clip blends a CLIP style-world sub-score into the tag+palette scorer
+ // (palette hard gate unchanged). clipPairwiseMap returns {} on any miss, which
+ // makes the blend a no-op — the response never depends on the service being up.
+ let usedEngine = 'tag';
+ let clipMap = null;
+ if (req.query.engine === 'clip') {
+ clipMap = await clipPairwiseMap(source, candidates);
+ if (Object.keys(clipMap).length) usedEngine = 'clip-blend';
+ else clipMap = null;
+ }
+ const top = pickPairs(source, candidates, requestedK, clipMap);
// Collect every hex we need paint matches for: source palette + each pair palette
const sourcePalette = paletteOf(source);
@@ -1063,6 +1115,7 @@ app.get('/api/pairs', async (req, res) => {
res.set('Cache-Control', 'public, max-age=300');
res.json({
ok: true,
+ engine: usedEngine,
source: {
dw_sku: source.dw_sku,
handle: source.handle,
← 399cce5 auto-save: 2026-07-12T23:21:12 (1 files) — server.js
·
back to Dw Pairs Well
·
pairs: PAIRS_CLIP_DEFAULT env flag (default OFF) blends CLIP ce3fda2 →