← back to Hollywood Import
momentum LOCAL enrichment runner (qwen2.5vl:7b, $0): ai_color_name/description/tags, resumable, no hallucinated hex
d793c5d81fdb6a128129d09f864fc87583c732fa · 2026-07-08 12:43:07 -0700 · steve@designerwallcoverings.com
Files touched
A momentum-feed/enrich.mjs
Diff
commit d793c5d81fdb6a128129d09f864fc87583c732fa
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Wed Jul 8 12:43:07 2026 -0700
momentum LOCAL enrichment runner (qwen2.5vl:7b, $0): ai_color_name/description/tags, resumable, no hallucinated hex
---
momentum-feed/enrich.mjs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/momentum-feed/enrich.mjs b/momentum-feed/enrich.mjs
new file mode 100644
index 0000000..c6c2b8e
--- /dev/null
+++ b/momentum-feed/enrich.mjs
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+// Momentum → Hollywood LOCAL enrichment ($0). Fills ai_color_name / description / tags
+// for momentum_colorways rows missing them, using local Ollama qwen2.5vl:7b vision.
+// Resumable (only touches rows WHERE ai_color_name IS NULL AND image_url IS NOT NULL).
+// Does NOT write ai_color_hex — per dw-local-color-enrichment rule, hex comes from pixel
+// analysis (LLMs hallucinate hex), left to the PIL enricher. NEVER touches dw_sku/shopify_*.
+// Run: NODE_PATH=.../AbramsOS/node_modules node enrich.mjs [--limit N] [--category Acoustic]
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+const { Pool } = require('pg');
+
+const OLLAMA = process.env.OLLAMA_HOST || 'http://localhost:11434';
+const MODEL = process.env.OLLAMA_VL || 'qwen2.5vl:7b';
+const pool = new Pool({ connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified' });
+const argLimit = (() => { const i = process.argv.indexOf('--limit'); return i > -1 ? parseInt(process.argv[i + 1], 10) : null; })();
+const argCat = (() => { const i = process.argv.indexOf('--category'); return i > -1 ? process.argv[i + 1] : null; })();
+
+const PROMPT = `You are a contract interior-design cataloger. Look at this commercial wallcovering/acoustic-panel swatch image and return ONLY compact JSON, no prose:
+{"color_name":"<designer color name, e.g. Harvest Tan, Alabaster, Celadon>","description":"<one commercial sentence about the texture/finish/use, NO brand names>","tags":["<3-5 style/color tags>"]}`;
+
+async function imgB64(url) {
+ const r = await fetch(url, { signal: AbortSignal.timeout(20000) });
+ if (!r.ok) throw new Error(`img ${r.status}`);
+ return Buffer.from(await r.arrayBuffer()).toString('base64');
+}
+async function vision(b64) {
+ const r = await fetch(`${OLLAMA}/api/generate`, {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ model: MODEL, prompt: PROMPT, images: [b64], stream: false, format: 'json',
+ options: { temperature: 0.2 } }),
+ signal: AbortSignal.timeout(120000),
+ });
+ if (!r.ok) throw new Error(`ollama ${r.status}`);
+ const j = await r.json();
+ return JSON.parse(j.response);
+}
+
+async function main() {
+ const where = ['ai_color_name IS NULL', "image_url IS NOT NULL", "image_url <> ''"];
+ if (argCat) where.push(`category = '${argCat.replace(/'/g, "''")}'`);
+ const q = `SELECT id, pattern_name, color_name, image_url FROM momentum_colorways
+ WHERE ${where.join(' AND ')} ORDER BY category, id ${argLimit ? `LIMIT ${argLimit}` : ''}`;
+ const { rows } = await pool.query(q);
+ console.log(`[enrich] ${rows.length} rows to enrich${argCat ? ` (category=${argCat})` : ''}. model=${MODEL} $0 local`);
+ let ok = 0, fail = 0;
+ for (const r of rows) {
+ try {
+ const out = await vision(await imgB64(r.image_url));
+ const name = (out.color_name || '').toString().slice(0, 80) || null;
+ const desc = (out.description || '').toString().slice(0, 500) || null;
+ const tags = Array.isArray(out.tags) ? out.tags.slice(0, 6).map(String) : [];
+ await pool.query(
+ `UPDATE momentum_colorways SET ai_color_name=$1, description=COALESCE(description,$2),
+ tags = CASE WHEN (tags IS NULL OR tags='[]' OR tags='') THEN $3 ELSE tags END, updated_at=now()
+ WHERE id=$4 AND ai_color_name IS NULL`,
+ [name, desc, JSON.stringify(tags), r.id]);
+ ok++;
+ if (ok % 25 === 0) process.stdout.write(`\r enriched ${ok} ok / ${fail} fail `);
+ } catch (e) { fail++; if (fail <= 5) console.error(`\n [skip id=${r.id}] ${e.message}`); }
+ }
+ console.log(`\n[enrich] done — ${ok} enriched, ${fail} failed. $0 (local qwen2.5vl).`);
+ await pool.end();
+}
+main().catch(e => { console.error(e); process.exit(1); });
← e5c026f momentum feed-first refresh (Meilisearch redesign-colors): +
·
back to Hollywood Import
·
refresh: add Meilisearch 20k-cap truncation alarm (contraria c1db41b →