← 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
M DW-Programming/ImportNewSkufromURL/lib/extract.tsM DW-Programming/ImportNewSkufromURL/scripts/ensure-import-app.sh
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 →