← back to Marketing Command Center
Re-route Make-it-ours off Gemini: settlement vision → Claude, room render → Replicate SDXL
7064e7c1c71b81e8dd3445f7ef393beb27566593 · 2026-08-25 10:51:43 -0700 · Steve Abrams
Files touched
M lib/room-render.jsM lib/settlement-gate.jsM server.js
Diff
commit 7064e7c1c71b81e8dd3445f7ef393beb27566593
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 25 10:51:43 2026 -0700
Re-route Make-it-ours off Gemini: settlement vision → Claude, room render → Replicate SDXL
---
lib/room-render.js | 198 +++++++++++++++++++++++++++++++++++++++----------
lib/settlement-gate.js | 110 ++++++++++++++++++---------
server.js | 8 +-
3 files changed, 238 insertions(+), 78 deletions(-)
diff --git a/lib/room-render.js b/lib/room-render.js
index c7d2a55..5d8d0db 100644
--- a/lib/room-render.js
+++ b/lib/room-render.js
@@ -1,66 +1,184 @@
'use strict';
-// Thin client for the EXISTING DW room-setting-generator pipeline (Treatment 2).
+// Room-setting renderer (Treatment 2) for the #vendors "Make it ours" system.
//
-// REUSE — this does NOT reinvent a renderer. It calls the canonical DW Room Setting
-// App documented in ~/.claude/skills/room-setting-generator/SKILL.md:
-// POST http://127.0.0.1:8106/api/generate-room
-// body: { patternBase64, roomType, angle, cameraDistance, patternWidth, patternHeight }
-// -> { success:true, image:<base64 jpeg> } (engine = gemini-2.5-flash-image)
+// MODEL RE-ROUTE (2026-08-25, TK-10845): the actual IMAGE GENERATION now runs on
+// REPLICATE SDXL instead of the Gemini room-setting-generator (:8106 /
+// gemini-2.5-flash-image). Per Steve's model-routing rule "image gen -> SDXL"
+// and the directive "use other model unless for vision" — a room render is
+// GENERATION (not vision), so it moves off the depleted Gemini credits onto
+// Replicate SDXL (~1¢/img). The generated room image STILL runs through the
+// Change-1 Claude settlement gate (in server.js) before it is saved/shown.
//
-// COST: the room app bills ~$0.01–$0.04 per render (Gemini 2.5 flash image). The
-// caller surfaces + logs that. This module returns the render bytes; the server
-// endpoint then runs the settlement post-gen-vision gate before saving/showing.
+// SHAPE PRESERVED — the server endpoint's consumer contract is unchanged:
+// generateRoom(patternBase64, opts) -> { ok, buffer?, mime?, cost, error? }
+// roomAppUp() -> Promise<boolean> (now = "is Replicate reachable / configured")
+// ROOM_RENDER_COST, ROOM_APP exports kept.
+//
+// COST: SDXL ~1¢/image on Replicate (a single 1024x1024 render, ~25 steps). The
+// caller surfaces + logs it. The cost is real once the prediction succeeds.
+
+const REPLICATE_API = 'https://api.replicate.com/v1';
+// stability-ai/sdxl pinned version (resolved 2026-08-25); overridable via env.
+const SDXL_VERSION =
+ process.env.SDXL_VERSION || '7762fd07cf82c948538e41f63f77d685e02b063e37e496e96eefd46c929f9bdc';
+// Kept for back-compat with the endpoint's cost label / 503 copy.
+const ROOM_APP = process.env.ROOM_APP_URL || 'https://api.replicate.com';
+// SDXL on Replicate is ~1¢/render.
+const ROOM_RENDER_COST = 0.01;
-const ROOM_APP = process.env.ROOM_APP_URL || 'http://127.0.0.1:8106';
-const GEN_ROOM_URL = ROOM_APP + '/api/generate-room';
-// Ballpark per-render cost for the cost line (Gemini 2.5 flash image, per skill doc).
-const ROOM_RENDER_COST = 0.03;
+const REPLICATE_KEY_NAMES = ['REPLICATE_API_TOKEN', 'REPLICATE_API_KEY'];
+function replicateToken() {
+ for (const n of REPLICATE_KEY_NAMES) if (process.env[n]) return process.env[n];
+ try {
+ const fs = require('fs');
+ const os = require('os');
+ const path = require('path');
+ const p = path.join(os.homedir(), 'Projects', 'secrets-manager', '.env');
+ const s = fs.readFileSync(p, 'utf8');
+ for (const n of REPLICATE_KEY_NAMES) {
+ const m = s.match(new RegExp('^' + n + '=(.*)$', 'm'));
+ if (m) return m[1].replace(/^["']|["']$/g, '').trim();
+ }
+ } catch { /* no secrets file */ }
+ return '';
+}
-// Is the room app reachable? (fast HEAD/GET so the UI can show a helpful message
-// instead of hanging when the pipeline app isn't running.)
+// "Is the renderer available?" — now means: is a Replicate token configured?
+// (Kept async + same name so the endpoint's `if (!(await roomAppUp()))` guard
+// still short-circuits with a helpful message instead of 502-ing mid-render.)
async function roomAppUp() {
+ const tok = replicateToken();
+ if (!tok) return false;
try {
const c = new AbortController();
- const t = setTimeout(() => c.abort(), 2500);
- const r = await fetch(ROOM_APP + '/', { signal: c.signal });
+ const t = setTimeout(() => c.abort(), 4000);
+ const r = await fetch(REPLICATE_API + '/account', {
+ headers: { Authorization: 'Bearer ' + tok },
+ signal: c.signal,
+ });
clearTimeout(t);
- return r.ok || r.status === 401 || r.status === 200;
+ // 200 = reachable+authed; 401 still means "reachable" (endpoint offline is the
+ // failure we guard against). Any 2xx/401 => proceed.
+ return r.ok || r.status === 401;
} catch { return false; }
}
-// Generate one room setting from a base64 pattern.
+// Build a room-setting prompt for a given room type (SDXL is txt2img/img2img; we
+// describe the scene and use the pattern as an img2img reference so the actual
+// wallcovering appears on the wall).
+function roomPrompt(roomType) {
+ const rooms = {
+ living_room: 'an elegant modern living room, feature wall covered in this wallcovering pattern, styled sofa and decor',
+ bedroom: 'a serene luxury bedroom, accent wall covered in this wallcovering pattern, styled bed and nightstands',
+ dining_room: 'a refined dining room, feature wall covered in this wallcovering pattern, dining table and chairs',
+ office: 'a sophisticated home office, feature wall covered in this wallcovering pattern, desk and shelving',
+ bathroom: 'a designer bathroom, feature wall covered in this wallcovering pattern, vanity and mirror',
+ };
+ const scene = rooms[roomType] || rooms.living_room;
+ return (
+ `Interior design photograph of ${scene}. Photorealistic, natural daylight, ` +
+ `professional architectural photography, the wallcovering pattern rendered ` +
+ `crisply and true to the reference on the wall, tasteful furnishings, high detail, 4k.`
+ );
+}
+
+// Poll a Replicate prediction to a terminal state.
+async function pollPrediction(token, id, deadlineMs) {
+ while (Date.now() < deadlineMs) {
+ const r = await fetch(REPLICATE_API + '/predictions/' + id, {
+ headers: { Authorization: 'Bearer ' + token },
+ });
+ const j = await r.json();
+ if (j.status === 'succeeded') return { ok: true, output: j.output };
+ if (j.status === 'failed' || j.status === 'canceled') {
+ return { ok: false, error: String(j.error || j.status).slice(0, 160) };
+ }
+ await new Promise((res) => setTimeout(res, 2000));
+ }
+ return { ok: false, error: 'render timed out' };
+}
+
+// Generate one room setting from a base64 pattern via Replicate SDXL.
// @returns {Promise<{ok:boolean, buffer?:Buffer, mime?:string, cost:number, error?:string}>}
async function generateRoom(patternBase64, opts = {}) {
- const body = {
- patternBase64: String(patternBase64 || ''),
- roomType: opts.roomType || 'living_room',
- angle: opts.angle || 'straight_on',
- cameraDistance: Number(opts.cameraDistance) || 8,
- patternWidth: Number(opts.patternWidth) || 27,
- patternHeight: Number(opts.patternHeight) || 27,
+ const pattern = String(patternBase64 || '');
+ if (!pattern) return { ok: false, cost: 0, error: 'no pattern image' };
+ const token = replicateToken();
+ if (!token) return { ok: false, cost: 0, error: 'REPLICATE_API_TOKEN not set' };
+
+ const roomType = opts.roomType || 'living_room';
+ // SDXL accepts a data: URL for the img2img `image` input.
+ const dataUrl = pattern.startsWith('data:') ? pattern : 'data:image/png;base64,' + pattern;
+
+ const input = {
+ prompt: roomPrompt(roomType),
+ negative_prompt:
+ 'blurry, distorted pattern, warped wallcovering, low quality, watermark, text, logo, deformed, ugly',
+ image: dataUrl, // img2img reference so the pattern shows on the wall
+ prompt_strength: 0.72, // keep enough of the scene while honoring the pattern
+ width: 1024,
+ height: 1024,
+ num_inference_steps: 30,
+ guidance_scale: 7.5,
+ scheduler: 'K_EULER',
+ num_outputs: 1,
+ refine: 'expert_ensemble_refiner',
+ apply_watermark: false,
};
- if (!body.patternBase64) return { ok: false, cost: 0, error: 'no pattern image' };
- let json;
+
+ let createJson;
try {
const c = new AbortController();
- const t = setTimeout(() => c.abort(), 120000); // renders can take ~30-60s
- const res = await fetch(GEN_ROOM_URL, {
+ const t = setTimeout(() => c.abort(), 15000);
+ const res = await fetch(REPLICATE_API + '/predictions', {
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
+ headers: {
+ Authorization: 'Bearer ' + token,
+ 'Content-Type': 'application/json',
+ Prefer: 'wait=5', // let Replicate hold briefly; we still poll below
+ },
+ body: JSON.stringify({ version: SDXL_VERSION, input }),
signal: c.signal,
});
clearTimeout(t);
- json = await res.json();
+ createJson = await res.json();
} catch (e) {
- return { ok: false, cost: 0, error: 'room app: ' + String((e && e.message) || e).slice(0, 120) };
+ return { ok: false, cost: 0, error: 'replicate create: ' + String((e && e.message) || e).slice(0, 120) };
}
- if (!json || !json.success || !json.image) {
- return { ok: false, cost: 0, error: (json && json.error) ? String(json.error).slice(0, 160) : 'render failed' };
+ if (!createJson || createJson.error || !createJson.id) {
+ return { ok: false, cost: 0, error: (createJson && createJson.detail) ? String(createJson.detail).slice(0, 160) : 'replicate create failed' };
}
- // The render happened -> the app billed for it, even if we later reject on the
- // settlement gate. So the cost is real once success:true comes back.
- return { ok: true, buffer: Buffer.from(json.image, 'base64'), mime: 'image/jpeg', cost: ROOM_RENDER_COST };
+
+ // Resolve output (either already-terminal from Prefer:wait, or poll).
+ let output = createJson.output;
+ let status = createJson.status;
+ if (status !== 'succeeded') {
+ const polled = await pollPrediction(token, createJson.id, Date.now() + 120000);
+ if (!polled.ok) return { ok: false, cost: 0, error: polled.error };
+ output = polled.output;
+ }
+
+ // SDXL returns an array of image URLs (num_outputs:1 => one URL).
+ const url = Array.isArray(output) ? output[0] : output;
+ if (!url || typeof url !== 'string') return { ok: false, cost: 0, error: 'no image URL from SDXL' };
+
+ let buffer;
+ let mime = 'image/png';
+ try {
+ const c = new AbortController();
+ const t = setTimeout(() => c.abort(), 30000);
+ const imgRes = await fetch(url, { signal: c.signal });
+ clearTimeout(t);
+ if (!imgRes.ok) return { ok: false, cost: 0, error: 'fetch render ' + imgRes.status };
+ mime = imgRes.headers.get('content-type') || (url.endsWith('.jpg') || url.endsWith('.jpeg') ? 'image/jpeg' : 'image/png');
+ buffer = Buffer.from(await imgRes.arrayBuffer());
+ } catch (e) {
+ return { ok: false, cost: 0, error: 'download render: ' + String((e && e.message) || e).slice(0, 120) };
+ }
+ if (!buffer || !buffer.length) return { ok: false, cost: 0, error: 'empty render' };
+
+ // The prediction succeeded -> Replicate billed for it (~1¢).
+ return { ok: true, buffer, mime, cost: ROOM_RENDER_COST };
}
-module.exports = { generateRoom, roomAppUp, ROOM_RENDER_COST, ROOM_APP };
+module.exports = { generateRoom, roomAppUp, ROOM_RENDER_COST, ROOM_APP, SDXL_VERSION };
diff --git a/lib/settlement-gate.js b/lib/settlement-gate.js
index 8cead8a..58b722d 100644
--- a/lib/settlement-gate.js
+++ b/lib/settlement-gate.js
@@ -5,24 +5,45 @@
// ~/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/lib/settlement-checker.ts
// (checkSettlementCompliance) — the reference implementation the
// `settlement-post-gen-vision` skill defers to. We do NOT reinvent the rule;
-// we run the SAME Gemini-vision post-gen check the skill prescribes, so any
+// we run the SAME post-gen VISION check the skill prescribes, so any
// AI-generated OR composited DW-original asset is verified against the executed
// Settlement Agreement (Part A all-three AND Part B, no acceptable carve-out)
// BEFORE it can be staged/shown as final.
//
-// HARD RULES honored from settlement-post-gen-vision/SKILL.md:
-// - Gemini vision is GROUND TRUTH (text anti-prompts don't bind a generator).
-// - FAIL-SAFE: any API/parse/fetch error returns NEEDS_REVIEW, never an auto-pass.
+// MODEL RE-ROUTE (2026-08-25, TK-10845): the vision call now runs on ANTHROPIC
+// CLAUDE vision (Messages API, image block) instead of Gemini. Per Steve's
+// directive "use other model unless for vision" — this IS the genuine vision
+// task (visually verifying a rendered image against a legal gate), so it keeps
+// a VISION-capable model; we just move it off the depleted Gemini credits onto
+// the strongest available Claude vision model. Gemini image credits being
+// depleted was blocking BOTH this legal gate (so even the $0 branded card
+// couldn't SAVE) and the room render.
+//
+// HARD RULES honored from settlement-post-gen-vision/SKILL.md (UNCHANGED — only
+// the model call changed; the prompt text, combination logic, return shape, and
+// fail-CLOSED behavior are IDENTICAL to the prior Gemini port):
+// - Vision is GROUND TRUTH (text anti-prompts don't bind a generator).
+// - FAIL-SAFE / fail-CLOSED: any API/parse/fetch error or uncertainty returns
+// NEEDS_REVIEW, never an auto-pass. This is a LEGAL compliance gate.
// - Verdict vocabulary is BLOCK / NEEDS_REVIEW / OK.
//
-// COST: one Gemini vision call per gate (~$0.0006/image, gemini flash). The caller
-// logs it via the cost-tracker skill. A plain branded-card composite (Treatment 1)
-// is not AI-generated, but we still post-gen-vision-check it per the task spec
-// ("run it past settlement post-gen-vision to be safe").
+// COST: one Claude vision call per gate (~$0.003–0.01/image on Claude vision;
+// a small image + short JSON reply). The caller logs it via the cost-tracker
+// skill. A plain branded-card composite (Treatment 1) is not AI-generated, but
+// we still post-gen-vision-check it per the task spec ("run it past settlement
+// post-gen-vision to be safe").
+
+// Strongest available Claude vision model for a LEGAL gate. Overridable via env
+// if the account is repointed. claude-opus-4-8 is the flagship vision model on
+// this box; if it's ever unavailable the fetch simply fails-CLOSED to NEEDS_REVIEW.
+const CLAUDE_VISION_MODEL = process.env.SETTLEMENT_CLAUDE_MODEL || 'claude-opus-4-8';
+const ANTHROPIC_VERSION = '2023-06-01';
+// ~$0.003–0.01 per image on Claude vision (small image tokens + short JSON out).
+const CLAUDE_VISION_COST = 0.006;
-const GEMINI_KEY_NAMES = ['GEMINI_API_KEY', 'GOOGLE_API_KEY'];
-function geminiKey() {
- for (const n of GEMINI_KEY_NAMES) if (process.env[n]) return process.env[n];
+const ANTHROPIC_KEY_NAMES = ['ANTHROPIC_API_KEY'];
+function anthropicKey() {
+ for (const n of ANTHROPIC_KEY_NAMES) if (process.env[n]) return process.env[n];
// Best-effort: read the secrets-manager master .env so the MCC doesn't need the
// key duplicated into its own .env (read-only, last-4 never logged).
try {
@@ -31,11 +52,11 @@ function geminiKey() {
const path = require('path');
const p = path.join(os.homedir(), 'Projects', 'secrets-manager', '.env');
const s = fs.readFileSync(p, 'utf8');
- for (const n of GEMINI_KEY_NAMES) {
+ for (const n of ANTHROPIC_KEY_NAMES) {
const m = s.match(new RegExp('^' + n + '=(.*)$', 'm'));
if (m) return m[1].replace(/^["']|["']$/g, '').trim();
}
- } catch { /* no secrets file — gate will fail-safe to NEEDS_REVIEW */ }
+ } catch { /* no secrets file — gate will fail-CLOSED to NEEDS_REVIEW */ }
return '';
}
@@ -52,11 +73,13 @@ async function settlementGateBuffer(buf, mime, title) {
detail: { failSafe: true },
});
- const key = geminiKey();
- if (!key) return failSafe('GEMINI_API_KEY not set');
+ const key = anthropicKey();
+ if (!key) return failSafe('ANTHROPIC_API_KEY not set');
if (!Buffer.isBuffer(buf) || !buf.length) return failSafe('empty image buffer');
const b64 = buf.toString('base64');
+ // CANONICAL PROMPT TEXT — preserved VERBATIM from the prior Gemini port so the
+ // legal gate's semantics are byte-identical; only the transport (Claude) changed.
const prompt = `You are performing a strict LEGAL compliance check on a wallcovering design image for "${String(title || '').slice(0, 120)}".
Answer ONLY with valid JSON, no markdown:
{
@@ -71,27 +94,46 @@ Answer ONLY with valid JSON, no markdown:
}
Be precise and conservative: only answer true when the visual evidence is clear.`;
+ // Claude vision wants a media_type from a fixed set; normalize jpg->jpeg and
+ // fall back to png for anything unexpected (the buffer is still sent as-is).
+ let mediaType = String(mime || 'image/png').toLowerCase();
+ if (mediaType === 'image/jpg') mediaType = 'image/jpeg';
+ if (!['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(mediaType)) mediaType = 'image/png';
+
let json;
try {
- const res = await fetch(
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`,
- {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- contents: [{ role: 'user', parts: [{ inline_data: { mime_type: mime || 'image/png', data: b64 } }, { text: prompt }] }],
- generationConfig: { maxOutputTokens: 800, thinkingConfig: { thinkingBudget: 0 } },
- }),
- }
- );
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
+ method: 'POST',
+ headers: {
+ 'x-api-key': key,
+ 'anthropic-version': ANTHROPIC_VERSION,
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: CLAUDE_VISION_MODEL,
+ max_tokens: 800,
+ messages: [
+ {
+ role: 'user',
+ content: [
+ { type: 'image', source: { type: 'base64', media_type: mediaType, data: b64 } },
+ { type: 'text', text: prompt },
+ ],
+ },
+ ],
+ }),
+ });
json = await res.json();
} catch (e) {
return failSafe('fetch ' + String((e && e.message) || e).slice(0, 100));
}
- if (!json || json.error) return failSafe('Gemini: ' + String((json && json.error && json.error.message) || 'no response').slice(0, 120));
+ if (!json || json.error) {
+ return failSafe('Claude: ' + String((json && json.error && json.error.message) || 'no response').slice(0, 120));
+ }
- const text = (((json.candidates || [])[0] || {}).content || {}).parts
- ? json.candidates[0].content.parts.map((p) => p.text || '').join('')
+ // Claude Messages returns content: [{type:'text', text:'...'}]
+ const text = Array.isArray(json.content)
+ ? json.content.map((p) => (p && p.type === 'text' ? p.text : '') || '').join('')
: '';
let v;
try {
@@ -100,7 +142,7 @@ Be precise and conservative: only answer true when the visual evidence is clear.
return failSafe('unparseable vision verdict');
}
- // Defendant-favorable combination (identical to the canonical checker):
+ // Defendant-favorable combination (IDENTICAL to the canonical checker):
// VIOLATION requires FULL Part A (all three) AND Part B, no acceptable carve-out.
const partA = !!(v.a1_directional_leaves && v.a2_open_space && v.a3_multiple_colors);
const partB = !!v.b_prohibited_elements;
@@ -109,14 +151,14 @@ Be precise and conservative: only answer true when the visual evidence is clear.
const needsReview = !violates && ((partA && !carveout) || (partB && !!v.is_tropical_foliage_design));
const reason =
- `Gemini vision: A1=${!!v.a1_directional_leaves}, A2=${!!v.a2_open_space}, A3=${!!v.a3_multiple_colors} ` +
+ `Claude vision: A1=${!!v.a1_directional_leaves}, A2=${!!v.a2_open_space}, A3=${!!v.a3_multiple_colors} ` +
`(Part A ${partA ? 'MET' : 'not met'}); Part B=${partB}` +
`${(v.b_elements_found || []).length ? ` [${(v.b_elements_found || []).join(', ')}]` : ''}; ` +
`carve-out=${carveout}. ${v.visual_summary || ''}`.trim();
const verdict = violates ? 'BLOCK' : (needsReview ? 'NEEDS_REVIEW' : 'OK');
- // ~$0.0006/image is the Gemini flash vision ballpark (per Steve's cost rule).
- return { verdict, reason, cost: 0.0006, detail: { partA, partB, carveout, raw: v } };
+ // ~$0.003–0.01/image is the Claude vision ballpark (per Steve's cost rule).
+ return { verdict, reason, cost: CLAUDE_VISION_COST, detail: { partA, partB, carveout, raw: v } };
}
-module.exports = { settlementGateBuffer, geminiKey };
+module.exports = { settlementGateBuffer, anthropicKey, CLAUDE_VISION_MODEL, CLAUDE_VISION_COST };
diff --git a/server.js b/server.js
index 30faf35..a106846 100644
--- a/server.js
+++ b/server.js
@@ -241,7 +241,7 @@ const logCost = (units, note) => {
const { spawn } = require('child_process');
const script = path.join(os.homedir(), '.claude', 'skills', 'cost-tracker', 'scripts', 'log.js');
if (!fs.existsSync(script)) return;
- spawn('node', [script, '--api', 'gemini_2_0_flash', '--units', units, '--app', 'marketing-command-center', '--note', note || 'vendor-amplify make-it-ours'], { detached: true, stdio: 'ignore' }).unref();
+ spawn('node', [script, '--api', 'replicate_sdxl', '--units', units, '--app', 'marketing-command-center', '--note', note || 'vendor-amplify make-it-ours'], { detached: true, stdio: 'ignore' }).unref();
} catch { /* cost log is best-effort */ }
};
@@ -326,7 +326,7 @@ app.post('/api/vendor-amplify-room', async (req, res) => {
if (!patternBase64) return res.status(400).json({ ok: false, error: 'no pattern image' });
if (!(await roomAppUp())) {
- return res.status(503).json({ ok: false, error: 'room renderer offline — start the Room Setting App on :8106', costLabel: `$0 (not billed)` });
+ return res.status(503).json({ ok: false, error: 'room renderer unavailable — REPLICATE_API_TOKEN not set/reachable', costLabel: `$0 (not billed)` });
}
// (2) generate — the room app bills ~$0.01–0.04 once success comes back.
@@ -335,8 +335,8 @@ app.post('/api/vendor-amplify-room', async (req, res) => {
patternWidth: b.patternWidth, patternHeight: b.patternHeight,
});
if (!gen.ok) return res.status(502).json({ ok: false, error: gen.error || 'render failed', cost: gen.cost || 0, costLabel: '$0 (not billed)' });
- logCost('1:image', `room render (${b.roomType || 'living_room'}) for ${vendor}`);
- const costLabel = `$${gen.cost.toFixed(2)} (Gemini 2.5 flash image room render)`;
+ logCost('1:image', `room render SDXL (${b.roomType || 'living_room'}) for ${vendor}`);
+ const costLabel = `$${gen.cost.toFixed(2)} (Replicate SDXL room render)`;
// (1) SETTLEMENT gate on the AI-generated render — MANDATORY.
let gate;
← 834187d auto-data-snapshot: 2026-08-25T10:46:10 (1 data files) — pub
·
back to Marketing Command Center
·
Owned·DW Fleet: apply contrarian fixes (sort actionability + 88aa02b →