← 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
M scripts/settlement_postgen_vision_check.js
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 →