[object Object]

← back to Wallco Ai

settlement vision: free local ollama fallback (gemma3:12b) when Gemini capped + clamp local-violation→NEEDS REVIEW (never auto-unpublish on local model)

4e65df7f5c611ae81ad6f4de229592603b9c083f · 2026-06-09 14:04:03 -0700 · Steve

Files touched

Diff

commit 4e65df7f5c611ae81ad6f4de229592603b9c083f
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 9 14:04:03 2026 -0700

    settlement vision: free local ollama fallback (gemma3:12b) when Gemini capped + clamp local-violation→NEEDS REVIEW (never auto-unpublish on local model)
---
 scripts/settlement_postgen_vision_check.js | 102 +++++++++++++++++++++++------
 1 file changed, 83 insertions(+), 19 deletions(-)

diff --git a/scripts/settlement_postgen_vision_check.js b/scripts/settlement_postgen_vision_check.js
index 1362976..84f9d9f 100644
--- a/scripts/settlement_postgen_vision_check.js
+++ b/scripts/settlement_postgen_vision_check.js
@@ -73,6 +73,45 @@ Respond in this EXACT JSON format (no preamble, no markdown):
   "needsReview": true|false
 }`;
 
+// --- loose JSON extraction (local models wrap output in prose/markdown) ------
+function parseJsonLoose(text) {
+  if (!text) return null;
+  try { return JSON.parse(text); } catch {}
+  const m = text.match(/\{[\s\S]*\}/);
+  if (m) { try { return JSON.parse(m[0]); } catch {} }
+  return null;
+}
+
+// --- FREE LOCAL FALLBACK: ollama vision (llava/moondream/gemma3) -------------
+// Used when Gemini is unavailable (429 spending-cap, 5xx, network). Same
+// settlement prompt + image, local + unmetered — so a Gemini outage no longer
+// fail-closes the gate into mass-unpublishing clean designs (the 2026-06-09
+// cascade: cap → "vision unavailable" → guard unpublished 153 clean designs).
+async function ollamaVision(b64, prompt) {
+  const model = process.env.OLLAMA_VISION_MODEL || 'gemma3:12b';
+  const host  = process.env.OLLAMA_HOST || 'http://127.0.0.1:11434';
+  const ac = new AbortController();
+  const to = setTimeout(() => ac.abort(), 75_000);
+  try {
+    const r = await fetch(`${host}/api/generate`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        model,
+        prompt: prompt + '\n\nReturn ONLY the JSON object, no other text.',
+        images: [b64],
+        stream: false,
+        options: { temperature: 0 },
+      }),
+      signal: ac.signal,
+    });
+    if (!r.ok) return null;
+    const j = await r.json();
+    return parseJsonLoose(j.response || '');
+  } catch { return null; }
+  finally { clearTimeout(to); }
+}
+
 (async () => {
   const body = {
     contents: [{
@@ -89,32 +128,57 @@ Respond in this EXACT JSON format (no preamble, no markdown):
   const GEMINI_MODEL = process.env.GEMINI_VISION_MODEL || 'gemini-2.5-flash';
   const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${KEY}`;
   const t0 = Date.now();
-  const r = await fetch(url, {
-    method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify(body),
-  });
-  if (!r.ok) { console.error(`HTTP ${r.status}: ${(await r.text()).slice(0, 500)}`); process.exit(1); }
-  const j = await r.json();
+  let result = null, source = 'gemini';
+  // 1) PRIMARY — Gemini (most accurate). Falls through to local on any failure.
   try {
-    const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
-    logGemini(j, { app: 'wallco-ai', note: 'settlement-postgen-vision', model: GEMINI_MODEL });
-  } catch {}
-  const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
+    const r = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(body),
+    });
+    if (r.ok) {
+      const j = await r.json();
+      try {
+        const { logGemini } = require(require('os').homedir() + '/.claude/skills/cost-tracker/scripts/log-gemini.js');
+        logGemini(j, { app: 'wallco-ai', note: 'settlement-postgen-vision', model: GEMINI_MODEL });
+      } catch {}
+      const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text;
+      result = parseJsonLoose(text);
+    } else {
+      console.error(`Gemini HTTP ${r.status}: ${(await r.text()).slice(0, 200)} — falling back to local ollama vision`);
+    }
+  } catch (e) {
+    console.error(`Gemini error: ${e.message} — falling back to local ollama vision`);
+  }
+
+  // 2) FALLBACK — free local ollama vision (no Gemini-cap dependency).
+  if (!result) {
+    source = 'ollama:' + (process.env.OLLAMA_VISION_MODEL || 'gemma3:12b');
+    console.error(`[vision fallback] using ${source}`);
+    result = await ollamaVision(B64, PROMPT);
+  }
+
+  // 3) LAST RESORT — both vision systems unavailable → fail closed (exit 1 →
+  // guard treats as ERROR). Preserves the original conservative posture only
+  // when there is genuinely no way to verify the image.
   const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
-  if (!text) { console.error('no text in response:', JSON.stringify(j).slice(0, 400)); process.exit(1); }
-
-  let result;
-  try { result = JSON.parse(text); }
-  catch (e) {
-    const m = text.match(/\{[\s\S]*\}/);
-    if (!m) { console.error('no JSON in response:', text.slice(0, 400)); process.exit(1); }
-    result = JSON.parse(m[0]);
+  if (!result) { console.error('both Gemini and local ollama vision unavailable — fail-closed'); process.exit(1); }
+
+  // CLAMP: a LOCAL-model "violation" is advisory, not authoritative. Local
+  // vision (gemma3/llava) is good enough to CLEAR a clean design (OK) but can
+  // err on positives — so never let it drive a destructive BLOCK/unpublish.
+  // Downgrade a local-fallback violation to NEEDS REVIEW (sweep leaves those
+  // published + flagged); only Gemini can produce a sweep-triggering BLOCK.
+  if (source.startsWith('ollama') && result && result.violates) {
+    result.violates = false;
+    result.needsReview = true;
+    result.reasoning = '[local-vision fallback flagged a possible violation — downgraded to NEEDS REVIEW pending authoritative Gemini re-check] ' + (result.reasoning || '');
   }
 
   console.log('\n=== SETTLEMENT POST-GEN VISION CHECK ===');
   console.log(`Image:  ${IMAGE_PATH}`);
   console.log(`Title:  ${TITLE}`);
+  console.log(`Source: ${source}`);
   console.log(`Elapsed: ${elapsed}s\n`);
   console.log(`A1 (directional leaves): ${result.a1_directional_leaves}`);
   console.log(`A2 (open space):         ${result.a2_open_space}`);

← eedbd24 purge-orphan-pngs: reclaim gen-time orphan PNGs (no DB row)  ·  back to Wallco Ai  ·  geometric pilot: 19/20 generated (57711-57750), settlement O a259466 →