[object Object]

← 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

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 →