[object Object]

← 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

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 →