원클릭으로
image-extraction-debug
Debug Gemini/GPT vision API issues in puzzle image extraction
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Debug Gemini/GPT vision API issues in puzzle image extraction
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Complete Claude Code hooks reference - input/output schemas, registration, testing patterns
How to use AI APIs like OpenAI, ChatGPT, Elevenlabs, etc. When a user asks you to make an app that requires an AI API, use this skill to understand how to use the API or how to respond to the user.
Constraint satisfaction patterns for the Pips puzzle solver
Create git commits with user approval and no Claude attribution
Create or update continuity ledger for state preservation across clears
Create handoff document for transferring work to another session
| name | image-extraction-debug |
| description | Debug Gemini/GPT vision API issues in puzzle image extraction |
| allowed-tools | ["Read","Edit","Grep","Bash"] |
Debug and fix issues with AI-powered puzzle image extraction (GPT-5.2 via OpenRouter).
File: src/lib/services/gemini.ts
The extraction uses a dual-image approach:
User crops image into two parts (DualImageCropper component):
Domino extraction (line 117-211):
openai/gpt-5.2[(3,5), (0,0), (2,6), ...]Grid extraction (line 260-400):
openai/gpt-5.2Symptom: AI extracts 5×5 grid but puzzle is 7×9
Cause: AI guesses dimensions from visible cells only, ignoring holes
Solution: User provides size hints via GridSizeHintModal
Code location: src/lib/services/gemini.ts line 268-282
if (sizeHint) {
const expectedCells = sizeHint.dominoCount * 2;
const totalGridCells = sizeHint.cols * sizeHint.rows;
const expectedHoles = totalGridCells - expectedCells;
prompt = `${GRID_EXTRACTION_PROMPT}
IMPORTANT SIZE HINTS (from user):
- Grid dimensions: ${sizeHint.cols} columns × ${sizeHint.rows} rows
- Expected valid cells: ${expectedCells}
- Expected holes: ${expectedHoles}`;
}
Test: Verify size hints are passed through:
handleSizeHintConfirm() stores hints (index.tsx line 283)extractDualPuzzle() passes to API (line 315-319)Symptom: "Invalid puzzle: 18 cells but 12 dominoes (need 24 cells)"
Root causes:
Debug steps:
console.log('[AI] GPT-5.2 domino response:', text);
console.log('[AI] Parsed dominoes:', dominoes.map(d => d.pips));
Verify parsing (line 174-210):
Test with expectedCount hint (line 123-125):
const prompt = expectedCount
? `${DOMINO_EXTRACTION_PROMPT}\n\nThere should be EXACTLY ${expectedCount} dominoes in the image.`
: DOMINO_EXTRACTION_PROMPT;
Fix: Add domino count hint to dual extraction:
// In extractDualPuzzle() function (add parameter)
async function extractDualPuzzle(
dominoImageUri: string,
gridImageUri: string,
sizeHint?: GridSizeHint // Already has this
) {
// Pass expectedCount to domino extraction
const dominoes = await extractDominoesFromImage(
dominoBase64,
sizeHint?.dominoCount // NEW: pass count hint
);
}
Symptom: AI reads [3,5] domino as [3,6] or [4,5]
Causes:
Debug: Check raw AI response (line 171):
console.log('[AI] GPT-5.2 domino response:', text);
Solutions:
A) Improve prompt specificity (line 103-110):
HOW TO COUNT PIPS (dots) ON EACH HALF:
- 0: blank/empty (no dots at all)
- 1: one center dot
- 2: two dots (diagonal)
- 3: three dots (diagonal line)
- 4: four dots (corners)
- 5: five dots (corners + center)
- 6: six dots (2 columns of 3)
Add examples:
COMMON MISTAKES TO AVOID:
- Don't count shadows as dots
- 5 pips = 4 corners + 1 center (NOT 6)
- 3 pips = diagonal line (NOT triangle)
B) Lower temperature (line 159):
temperature: 0.1, // Already low, could go to 0.0
C) Request confidence scores:
Output format:
[
{"pips": [3,5], "confidence": "high"},
{"pips": [2,6], "confidence": "medium"},
...
]
Symptom: "Σ10" badge read as "equal" or "any"
Cause: OCR errors, ambiguous badges
Debug location: Grid extraction response parsing (line 380-450)
Check console:
console.log('[AI] GPT-5.2 grid response:', text);
Fix prompt (line 239-245):
Constraint badge meanings:
- "Σ" + number or just a number = sum constraint (type: "sum", value: N)
- "=" = equal (all same value) (type: "equal")
- "≠" = different (all unique) (type: "different")
- ">" + number = greater than (type: "greater", value: N)
- "<" + number = less than (type: "less", value: N)
- No badge = any constraint (type: "any")
Add visual examples:
EXAMPLES:
- Badge shows "10" or "Σ10" → {"constraint_type": "sum", "constraint_value": 10}
- Badge shows "=" → {"constraint_type": "equal"}
- Badge shows "≠" → {"constraint_type": "different"}
- No badge visible → {"constraint_type": "any"}
Symptom: "Region A has non-contiguous cells"
Cause: AI grouped cells by color instead of dashed boundaries
Code location: checkRegionContiguity() function (line 60-87)
This validates that all cells in a region are connected by edges (not diagonals).
Debug:
Fix prompt (line 233-235):
Rules:
- Do not merge regions by color — only by dashed boundaries.
- Each dashed boundary encloses ONE region.
Make more explicit:
CRITICAL: Dashed boundaries define regions, NOT colors.
- Two cells of the same color but separated by a dashed line = TWO different regions
- Cells can only be in the same region if connected WITHOUT crossing a dashed boundary
Symptom: "GPT-5.2 API error: 401" or timeout
Causes:
Debug (line 163-167):
if (!response.ok) {
const errorText = await response.text();
console.error('[AI] GPT-5.2 API error:', errorText);
throw new Error(`GPT-5.2 API error: ${response.status}`);
}
Solutions:
A) Check API key (line 14):
const OPENROUTER_API_KEY = process.env.EXPO_PUBLIC_VIBECODE_OPENROUTER_API_KEY;
Verify in .env file or Vibecode ENV tab.
B) Add retry logic:
async function extractWithRetry(fn: () => Promise<any>, retries = 3): Promise<any> {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
C) Add timeout (currently none):
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch(OPENROUTER_ENDPOINT, {
signal: controller.signal,
// ... other options
});
clearTimeout(timeoutId);
File: src/lib/services/gemini.ts
createSamplePuzzle()createLShapedPuzzle()These bypass AI extraction - useful for testing solver independently.
Add this to gemini.ts:
export async function debugExtraction(
imageUri: string,
sizeHint?: GridSizeHint
): Promise<{
rawDominoResponse: string;
rawGridResponse: string;
parsedDominoes: ExtractedDomino[];
parsedGrid: GridExtractionResponse;
}> {
// Same extraction but returns raw + parsed data
// ... implementation
}
Call from app:
const debug = await debugExtraction(imageUri, sizeHint);
console.log('Raw responses:', debug);
Add to verify images aren't too large:
console.log('[AI] Image size:', Math.round(base64Image.length / 1024), 'KB');
if (base64Image.length > 1024 * 1024) { // 1MB
console.warn('[AI] Image very large, may cause issues');
}
Current prompt (line 92-115) is good but can improve:
Add:
DETECTION RULES:
1. Each domino is a rectangular tile divided into two square halves
2. Tiles may be rotated but are always rectangular
3. Read ALL tiles visible in image, even if partially cut off
4. Count each unique tile exactly once
OUTPUT VERIFICATION:
Before submitting, verify your count matches the number of distinct rectangular tiles visible.
Current prompt (line 216-258) is comprehensive but could add:
Add:
COMMON MISTAKES TO AVOID:
1. Don't assume grid is square (rows ≠ cols)
2. Don't skip holes - mark every # explicitly
3. Don't merge adjacent regions unless boundary is absent
4. Don't guess constraint values if badge is unclear
SELF-CHECK:
- Total cells (regions + holes) = width × height
- Every region ID appears in ascii_grid
- Every coordinate uses 0-indexed (R0,C0 is top-left)
src/lib/services/gemini.ts - AI extraction logic
src/components/DualImageCropper.tsx - Image cropping UI
src/components/GridSizeHintModal.tsx - Size hint input
src/app/index.tsx - Extraction flow orchestration
Problem: Grid extracted as 5×5 but should be 7×9 with holes
Steps:
# Look for:
[AI] GPT-5.2 grid response: {"width": 5, "height": 5, ...}
// Add log in extractGridFromImage (line 267)
console.log('[DEBUG] Size hint:', sizeHint);
// Should see in console:
IMPORTANT SIZE HINTS (from user):
- Grid dimensions: 7 columns × 9 rows
// index.tsx handleSizeHintConfirm
console.log('[DEBUG] Hint confirmed:', cols, rows, dominoCount);
// index.tsx handleDualCropperComplete
console.log('[DEBUG] Passing hint to extraction:', pendingSizeHint);
After AI returns data, run these checks:
// 1. Cell count validation
const totalCells = grid.regions.reduce((sum, r) => sum + r.cells.length, 0);
const expectedCells = sizeHint.dominoCount * 2;
if (totalCells !== expectedCells) {
console.error(`Cell count mismatch: got ${totalCells}, expected ${expectedCells}`);
}
// 2. Grid dimension validation
if (sizeHint && (grid.width !== sizeHint.cols || grid.height !== sizeHint.rows)) {
console.error(`Dimension mismatch: got ${grid.width}×${grid.height}, expected ${sizeHint.cols}×${sizeHint.rows}`);
}
// 3. Domino count validation
if (dominoes.length !== sizeHint.dominoCount) {
console.error(`Domino count mismatch: got ${dominoes.length}, expected ${sizeHint.dominoCount}`);
}
// 4. Constraint value sanity check
for (const region of grid.regions) {
if (region.constraint_type === 'sum' && region.constraint_value) {
const maxPossible = region.size * 6; // Max pip value is 6
if (region.constraint_value > maxPossible) {
console.warn(`Impossible sum: region size ${region.size} can't sum to ${region.constraint_value}`);
}
}
}
Large images = slow API calls + high token costs.
// Add image compression before base64 conversion
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
async function compressImage(uri: string): Promise<string> {
const compressed = await manipulateAsync(
uri,
[{ resize: { width: 1024 } }], // Max width 1024px
{ compress: 0.8, format: SaveFormat.JPEG }
);
return compressed.uri;
}
Use before extraction:
const compressedUri = await compressImage(imageUri);
const base64 = await FileSystem.readAsStringAsync(compressedUri, {
encoding: FileSystem.EncodingType.Base64,
});
If user re-extracts same image:
const extractionCache = new Map<string, PuzzleData>();
// Key by image hash
const imageHash = await crypto.digest('SHA-256', base64Image);
if (extractionCache.has(imageHash)) {
console.log('[Cache] Returning cached extraction');
return extractionCache.get(imageHash)!;
}
// ... do extraction ...
extractionCache.set(imageHash, result);
Always provide size hints when possible
Use dual image cropping
Log everything during debug
Test with known puzzles first
createSamplePuzzle() to verify solver worksHandle API errors gracefully
Validate aggressively