← back to Wallco Ai
fix(ghost-detector): kill ~36% false-positive flag rate on full-corpus scan
47f7a9c05ad3a749c84d8289cfcfa07624959243 · 2026-05-23 23:14:39 -0700 · Steve Abrams
Two coupled fixes after the debugger traced a 17,463-design scan that
flagged 6,305 as having a "ghost layer" (translucent duplicate-of-scene
overlay). Verified false-positive on damask IDs 21/34/42/58 — clean
two-tone designs whose classical engraved internal shading was reading
as a ghost layer to single-call Gemini.
1. scripts/scan-ghost-layer.js — swap analyzeGhostLayer (single Gemini
call, ~20-30% flip rate per detector docs) → analyzeGhostLayerStable
(3-vote majority). Also write back the votes[] + unanimous fields to
the JSONL so split-vote borderline cases are visible in the output.
Schema note: `confidence` in scan output now encodes consensus
strength (1.0 unanimous, 0.67 split) — comment added inline.
2. lib/ghost-detector.js — add PROMPT_TOILE that explicitly exempts
engraved internal shading from the ghost-layer definition, and route
damask / chinoiserie / toile / face-skull-damask /
throttled-plaster-roses through it via pickPrompt(). PROMPT_TILE
stays the default for everything else; SCENIC_CATEGORIES still wins
first when both could match.
Generator-side bug (make_seamless.py blending semi-transparent motif
copies at tile seams for dense-motif categories — designer-zoo-calm,
drunk-animals, drunk-monkeys-v2) is real but lives in the Python image
pipeline; out of scope for this commit.
Validated: both files node --check clean; module exports unchanged
(analyzeGhostLayer / analyzeGhostLayerStable / verifyNoGhostLayer /
GEMINI_MODEL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M lib/ghost-detector.jsM scripts/scan-ghost-layer.js
Diff
commit 47f7a9c05ad3a749c84d8289cfcfa07624959243
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat May 23 23:14:39 2026 -0700
fix(ghost-detector): kill ~36% false-positive flag rate on full-corpus scan
Two coupled fixes after the debugger traced a 17,463-design scan that
flagged 6,305 as having a "ghost layer" (translucent duplicate-of-scene
overlay). Verified false-positive on damask IDs 21/34/42/58 — clean
two-tone designs whose classical engraved internal shading was reading
as a ghost layer to single-call Gemini.
1. scripts/scan-ghost-layer.js — swap analyzeGhostLayer (single Gemini
call, ~20-30% flip rate per detector docs) → analyzeGhostLayerStable
(3-vote majority). Also write back the votes[] + unanimous fields to
the JSONL so split-vote borderline cases are visible in the output.
Schema note: `confidence` in scan output now encodes consensus
strength (1.0 unanimous, 0.67 split) — comment added inline.
2. lib/ghost-detector.js — add PROMPT_TOILE that explicitly exempts
engraved internal shading from the ghost-layer definition, and route
damask / chinoiserie / toile / face-skull-damask /
throttled-plaster-roses through it via pickPrompt(). PROMPT_TILE
stays the default for everything else; SCENIC_CATEGORIES still wins
first when both could match.
Generator-side bug (make_seamless.py blending semi-transparent motif
copies at tile seams for dense-motif categories — designer-zoo-calm,
drunk-animals, drunk-monkeys-v2) is real but lives in the Python image
pipeline; out of scope for this commit.
Validated: both files node --check clean; module exports unchanged
(analyzeGhostLayer / analyzeGhostLayerStable / verifyNoGhostLayer /
GEMINI_MODEL).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
lib/ghost-detector.js | 32 +++++++++++++++++++++++++++++++-
scripts/scan-ghost-layer.js | 8 ++++++--
2 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/lib/ghost-detector.js b/lib/ghost-detector.js
index 67cf5dd..dc9fadf 100644
--- a/lib/ghost-detector.js
+++ b/lib/ghost-detector.js
@@ -70,6 +70,26 @@ reason: ONE short sentence (≤25 words)
Reply with ONLY a JSON object (no markdown, no prose):
{"hasGhostLayer": true|false, "confidence": <number>, "reason": "<short sentence>"}`;
+const PROMPT_TOILE = `You are inspecting a CLASSICAL DAMASK / TOILE / CHINOISERIE pattern for ghost layers.
+
+These patterns INTENTIONALLY use engraved internal shading — fine linework, hatching, cross-hatching, or dark detail lines drawn INSIDE a single motif shape. That is the historical engraved-print aesthetic — NOT a ghost layer.
+
+hasGhostLayer: TRUE only if the SAME MOTIF (flower, leaf, urn, bird) appears as TWO SEPARATE COPIES at different opacities — one fully opaque + one faded sitting beside or behind it as a distinct second silhouette.
+
+hasGhostLayer: FALSE if:
+ - The pattern is a single-color motif (or two-tone tone-on-tone) with internal linework / engraving / hatching detail on a contrasting ground
+ - The detail lines are INSIDE the motif boundary, not a second copy of the whole motif
+ - It looks like a classically-engraved damask/toile/chinoiserie print (expected aesthetic — fine darker linework within a solid motif is correct)
+ - Motifs share the same outline but no faded duplicate silhouette is visible behind/beside them
+
+DO NOT confuse engraved internal shading with ghost layers. Engraving = detail INSIDE one motif. Ghost layer = TWO copies of the WHOLE motif at different opacities.
+
+confidence: 0.0-1.0
+reason: ONE short sentence (≤25 words)
+
+Reply with ONLY a JSON object (no markdown, no prose):
+{"hasGhostLayer": true|false, "confidence": <number>, "reason": "<short sentence>"}`;
+
// scenic categories — atmospheric perspective is intended, use the lenient prompt
const SCENIC_CATEGORIES = new Set([
'cactus-pine-scenic', 'cactus-11ft-mural', 'tree-mural',
@@ -77,8 +97,18 @@ const SCENIC_CATEGORIES = new Set([
'mural-scenic', 'designer-scenic',
]);
+// toile categories — classical engraved aesthetic, internal linework is NOT a ghost layer.
+// Fix added 2026-05-23 after PROMPT_TILE single-call mis-flagged ~1,700 clean damask designs.
+const TOILE_CATEGORIES = new Set([
+ 'damask', 'chinoiserie', 'toile', 'face-skull-damask',
+ 'throttled-plaster-roses',
+]);
+
function pickPrompt(category) {
- if (category && SCENIC_CATEGORIES.has(category.toLowerCase())) return PROMPT_SCENIC;
+ if (!category) return PROMPT_TILE;
+ const c = category.toLowerCase();
+ if (SCENIC_CATEGORIES.has(c)) return PROMPT_SCENIC;
+ if (TOILE_CATEGORIES.has(c)) return PROMPT_TOILE;
return PROMPT_TILE;
}
diff --git a/scripts/scan-ghost-layer.js b/scripts/scan-ghost-layer.js
index cbe525d..042052a 100644
--- a/scripts/scan-ghost-layer.js
+++ b/scripts/scan-ghost-layer.js
@@ -20,7 +20,7 @@
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
-const { analyzeGhostLayer } = require('../lib/ghost-detector');
+const { analyzeGhostLayerStable } = require('../lib/ghost-detector');
// ── args ────────────────────────────────────────────────────────────────
const ARGS = process.argv.slice(2);
@@ -165,12 +165,16 @@ async function processOne(row) {
return { id: row.id, ts: new Date().toISOString(), error: 'file_missing', category: row.category, source: row.source };
}
try {
- const r = await analyzeGhostLayer(row.local_path, { category: row.category });
+ // Stable variant — 3 parallel Gemini calls + majority vote. Confidence here
+ // is consensus strength (1.0 unanimous, 0.67 split), NOT single-call confidence.
+ const r = await analyzeGhostLayerStable(row.local_path, { category: row.category });
return {
id: row.id, ts: new Date().toISOString(),
hasGhostLayer: r.hasGhostLayer,
confidence: r.confidence,
reason: r.reason,
+ unanimous: r.unanimous,
+ votes: r.votes,
category: row.category,
source: row.source,
image_url: `/designs/img/by-id/${row.id}`,
← 7adcd3c ghost-review: auto-suggest dotted regions + tolerance slider
·
back to Wallco Ai
·
ghost-review: Fix / Remove+Fill / Reverse-Regenerate action 725d763 →