[object Object]

← back to Designer Wallcoverings

extraction goes LOCAL-FIRST (EXTRACT_PREFER_LOCAL=1, hermes3:8b) — bulk drain no longer chews Max-plan quota; fix dead qwen2.5 fallback default (never installed); ensure-import-app heals browsers via symlink not install (install prunes cache dirs + memoized checks need app restart)

b20f45ae788477dba3244d95e5b37b58b89a7530 · 2026-07-13 10:26:25 -0700 · Steve

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit b20f45ae788477dba3244d95e5b37b58b89a7530
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 13 10:26:25 2026 -0700

    extraction goes LOCAL-FIRST (EXTRACT_PREFER_LOCAL=1, hermes3:8b) — bulk drain no longer chews Max-plan quota; fix dead qwen2.5 fallback default (never installed); ensure-import-app heals browsers via symlink not install (install prunes cache dirs + memoized checks need app restart)
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 DW-Programming/ImportNewSkufromURL/lib/extract.ts  | 29 +++++++++++++++++++++-
 .../scripts/ensure-import-app.sh                   | 23 ++++++++++++-----
 2 files changed, 45 insertions(+), 7 deletions(-)

diff --git a/DW-Programming/ImportNewSkufromURL/lib/extract.ts b/DW-Programming/ImportNewSkufromURL/lib/extract.ts
index 9b4c360b..2f7d467a 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/extract.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/extract.ts
@@ -57,7 +57,15 @@ function claudeExtract(systemPrompt: string, userPrompt: string, timeoutMs = 900
 // when the Max-subscription CLI is rate-limited under concurrent/bulk load it
 // fails over here instead of stalling. format=json forces clean JSON (no fences).
 const OLLAMA_URL = process.env.OLLAMA_URL || 'http://127.0.0.1:11434';
-const OLLAMA_MODEL = process.env.OLLAMA_EXTRACT_MODEL || 'qwen2.5:latest';
+// hermes3:8b — the fleet's proven local JSON extractor and actually installed on
+// Mac2 (the old default qwen2.5:latest was never pulled, so this fallback 404'd
+// silently until 2026-07-13). Override via OLLAMA_EXTRACT_MODEL.
+const OLLAMA_MODEL = process.env.OLLAMA_EXTRACT_MODEL || 'hermes3:8b';
+// EXTRACT_PREFER_LOCAL=1 → try local Ollama BEFORE the Claude CLI (Steve's
+// local-first rule; bulk drains shouldn't chew Max-plan quota that interactive
+// sessions share). CLI remains the quality fallback when local output fails
+// validation.
+const PREFER_LOCAL = process.env.EXTRACT_PREFER_LOCAL === '1';
 async function qwenExtract(systemPrompt: string, userPrompt: string, timeoutMs = 60000): Promise<string> {
   const ctrl = new AbortController();
   const t = setTimeout(() => ctrl.abort(), timeoutMs);
@@ -572,6 +580,25 @@ Extract the product data as JSON following the exact schema above.`;
   // 1 claude-cli attempt, then fail straight over to local qwen — under bulk/
   // concurrent load the Max CLI rate-limits, and waiting on 30s backoffs stalls
   // the whole queue. qwen (unmetered, no rate limit) is the better next tier.
+  // LOCAL-FIRST: unmetered hermes3 takes the first shot; anything that fails
+  // JSON validation falls through to the Claude CLI path below unchanged.
+  if (PREFER_LOCAL) {
+    try {
+      let qt = await qwenExtract(SYSTEM_PROMPT, userPrompt);
+      if (qt.startsWith('```')) qt = qt.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '');
+      const qjson = qt.match(/[\{\[][\s\S]*[\}\]]/);
+      if (qjson) qt = qjson[0];
+      const qvalidated = parseOrRescue(JSON.parse(qt));
+      if (qvalidated) {
+        console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_success', provider: 'qwen-local-first', model: OLLAMA_MODEL, imageCount: qvalidated.images.length, tagCount: qvalidated.tags.length, mfrSku: qvalidated.specs?.sku || 'none' }));
+        return qvalidated;
+      }
+      console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_local_first_invalid', model: OLLAMA_MODEL, note: 'falling through to claude-cli' }));
+    } catch (qErr) {
+      console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_local_first_error', model: OLLAMA_MODEL, error: qErr instanceof Error ? qErr.message : String(qErr) }));
+    }
+  }
+
   const MAX_ANTHROPIC_ATTEMPTS = 1;
   for (let attempt = 1; attempt <= MAX_ANTHROPIC_ATTEMPTS; attempt++) {
     try {
diff --git a/DW-Programming/ImportNewSkufromURL/scripts/ensure-import-app.sh b/DW-Programming/ImportNewSkufromURL/scripts/ensure-import-app.sh
index 4cb48899..ee3ef08c 100755
--- a/DW-Programming/ImportNewSkufromURL/scripts/ensure-import-app.sh
+++ b/DW-Programming/ImportNewSkufromURL/scripts/ensure-import-app.sh
@@ -21,12 +21,23 @@ alive() {
 
 # ENV HEAL: scrapes die with "Browser launch failed: Executable doesn't exist"
 # when the app's playwright-core lacks its pinned browser revision (bit us
-# 2026-07-13). MUST use the APP-LOCAL cli — `npx playwright` can resolve a
-# different playwright build whose browsers.json pins different revisions,
-# installing binaries the app never asks for (that's how shell-1228 landed
-# while the app wanted shell-1200). The local cli reads the local browsers.json
-# and is a fast no-op when the pinned revision is already present.
-(cd "$APP_DIR" && node node_modules/playwright/cli.js install chromium chromium-headless-shell >/dev/null 2>&1)
+# 2026-07-13). The app's browsers.json pins headless-shell rev 1200, but every
+# installable playwright build ships 1228 — so the fix is a SYMLINK 1200→1228,
+# not an install. Do NOT run `playwright install` here: it PRUNES cache dirs
+# (incl. our symlink target's siblings) that aren't in its own registry.
+PWCACHE="$HOME/Library/Caches/ms-playwright"
+if [ ! -e "$PWCACHE/chromium_headless_shell-1200/chrome-headless-shell-mac-arm64/chrome-headless-shell" ]; then
+  real=$(ls -d "$PWCACHE"/chromium_headless_shell-1[2-9][0-9][0-9] 2>/dev/null | grep -v -- '-1200$' | tail -1)
+  if [ -n "$real" ]; then
+    echo "[ensure-import-app] shell-1200 missing — symlinking to $(basename "$real")"
+    ln -sfn "$(basename "$real")" "$PWCACHE/chromium_headless_shell-1200"
+  else
+    echo "[ensure-import-app] no headless-shell present — installing via app-local cli"
+    (cd "$APP_DIR" && node node_modules/playwright/cli.js install chromium-headless-shell >/dev/null 2>&1)
+  fi
+  # playwright memoizes executable checks per process — force a fresh app
+  $PM2 restart "$NAME" --update-env >/dev/null 2>&1
+fi
 
 alive && exit 0
 

← d44bb4b1 ensure-import-app: install browsers via APP-LOCAL playwright  ·  back to Designer Wallcoverings  ·  fix: launchd bare-node PATH bug in two DW scheduled jobs e107ff3e →