vision-electronic-indexing-pi 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/skills/vision-inventory-workflow/SKILL.md +1 -1
- package/README.md +24 -28
- package/package.json +10 -4
- package/scripts/inventory_folder_to_csv.py +44 -52
- package/vision_inventory_mcp.py +1 -105
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vision-inventory-workflow
|
|
3
|
-
description: Run the Vision Electronic Indexing workflow for electronics/PCB photos: process images, create parts_to_lookup.json, verify datasheets with web search, fill datasheet_cache.json, regenerate CSV, and summarize uncertainties.
|
|
3
|
+
description: "Run the Vision Electronic Indexing workflow for electronics/PCB photos: process images, create parts_to_lookup.json, verify datasheets with web search, fill datasheet_cache.json, regenerate CSV, and summarize uncertainties."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Vision Inventory Workflow
|
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ The core vision step is a local Python MCP server that sends images to Cloudflar
|
|
|
13
13
|
|
|
14
14
|
- Processes one electronics image or a folder of images.
|
|
15
15
|
- Extracts visible IC/package markings, confidence, position hints, and review flags.
|
|
16
|
-
-
|
|
17
|
-
- Preserves individual IC marking observations
|
|
16
|
+
- Supports multiple different ICs in the same image.
|
|
17
|
+
- Preserves individual IC/package marking observations for audit and review.
|
|
18
18
|
- Saves raw JSON for auditability.
|
|
19
19
|
- Creates `parts_to_lookup.json` for datasheet enrichment.
|
|
20
20
|
- Produces a final CSV, using `datasheet_cache.json` when enrichment is available.
|
|
@@ -207,21 +207,17 @@ max_side: 4000
|
|
|
207
207
|
jpeg_quality: 96
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
## IC
|
|
210
|
+
## Multiple-IC behavior
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
Images may contain one IC or many different ICs. The vision step does not force all visible ICs in an image to share one marking or part family.
|
|
213
213
|
|
|
214
214
|
The processing flow is:
|
|
215
215
|
|
|
216
|
-
1.
|
|
217
|
-
2.
|
|
218
|
-
3.
|
|
219
|
-
- `items`: one consensus IC item when possible.
|
|
220
|
-
- `ic_marking_observations`: per-chip marking observations.
|
|
221
|
-
- `first_pass_items`: original first-pass IC candidates.
|
|
222
|
-
- `warnings`: notes about consensus verification.
|
|
216
|
+
1. General visible inventory extraction from the image.
|
|
217
|
+
2. Each visible IC/package marking is kept as its own candidate when the model returns it separately.
|
|
218
|
+
3. The batch workflow builds one evidence row per image/candidate part, so a single photo can contribute multiple different BOM rows.
|
|
223
219
|
|
|
224
|
-
This does not guarantee correct OCR. It
|
|
220
|
+
This does not guarantee correct OCR. It preserves alternate readings and marks uncertain candidates for review.
|
|
225
221
|
|
|
226
222
|
## Main batch workflow
|
|
227
223
|
|
|
@@ -339,7 +335,7 @@ output/inventory.csv
|
|
|
339
335
|
|
|
340
336
|
## Final CSV columns
|
|
341
337
|
|
|
342
|
-
By default, `inventory.csv` is deduplicated by normalized part number. Multiple images with the same IC become one BOM row with `sighting_count` and an `images` list.
|
|
338
|
+
By default, `inventory.csv` is deduplicated by normalized part number. Multiple images, or multiple candidates from the same image, with the same IC become one BOM row with `sighting_count` and an `images` list.
|
|
343
339
|
|
|
344
340
|
```text
|
|
345
341
|
normalized_part
|
|
@@ -364,9 +360,9 @@ Example BOM row:
|
|
|
364
360
|
SN74LS283N,SN74LS283N,8,2,74ls (4 bit) adder low power schottky ttl 5v DIP,https://www.ti.com/lit/ds/symlink/sn74ls283.pdf,Texas Instruments,true,high/low,true,"image_001.jpeg | image_002.jpeg","SN74LS283N | SN74S283N","output/raw/image_001.json | output/raw/image_002.json","Verified against TI datasheet"
|
|
365
361
|
```
|
|
366
362
|
|
|
367
|
-
The script also writes `inventory_evidence.csv`, which keeps the non-deduplicated per-image rows used to build the BOM. It includes the same per-sighting `amount` estimate before aggregation.
|
|
363
|
+
The script also writes `inventory_evidence.csv`, which keeps the non-deduplicated per-image/candidate rows used to build the BOM. It includes the same per-sighting `amount` estimate before aggregation.
|
|
368
364
|
|
|
369
|
-
`amount` is estimated from the
|
|
365
|
+
`amount` is estimated from the number of matching IC candidate items/evidence rows for that candidate. `sighting_count` is the number of evidence rows that were merged into the BOM row.
|
|
370
366
|
|
|
371
367
|
## MCP server usage
|
|
372
368
|
|
|
@@ -415,22 +411,22 @@ The server uses MCP `stdio` transport, so it is meant to be launched by an MCP-c
|
|
|
415
411
|
"package_marking": "SN74LS283N",
|
|
416
412
|
"marking_confidence": "medium",
|
|
417
413
|
"likely_part": "SN74LS283N",
|
|
418
|
-
"description": "
|
|
419
|
-
"position_hint": "
|
|
414
|
+
"description": "visible DIP IC marking",
|
|
415
|
+
"position_hint": "top-right",
|
|
420
416
|
"needs_review": true
|
|
421
|
-
}
|
|
422
|
-
],
|
|
423
|
-
"warnings": [
|
|
424
|
-
"Multi-pass IC consensus verification applied."
|
|
425
|
-
],
|
|
426
|
-
"ic_marking_observations": [
|
|
417
|
+
},
|
|
427
418
|
{
|
|
428
|
-
"
|
|
429
|
-
"
|
|
430
|
-
"
|
|
419
|
+
"item_type": "IC",
|
|
420
|
+
"count_index": 2,
|
|
421
|
+
"package_marking": "MAX232N",
|
|
422
|
+
"marking_confidence": "high",
|
|
423
|
+
"likely_part": "MAX232N",
|
|
424
|
+
"description": "visible DIP IC marking",
|
|
425
|
+
"position_hint": "bottom-left",
|
|
426
|
+
"needs_review": false
|
|
431
427
|
}
|
|
432
428
|
],
|
|
433
|
-
"
|
|
429
|
+
"warnings": []
|
|
434
430
|
}
|
|
435
431
|
```
|
|
436
432
|
|
|
@@ -461,7 +457,7 @@ Handled cases include:
|
|
|
461
457
|
- Vision models can misread small or blurry IC markings.
|
|
462
458
|
- A higher-resolution or closer photo usually helps more than prompt changes.
|
|
463
459
|
- Full-board photos are useful for context; cropped IC close-ups are better for marking OCR.
|
|
464
|
-
-
|
|
460
|
+
- Multiple ICs in one image can still be missed or merged by the vision model if markings are small or blurry.
|
|
465
461
|
- Datasheet enrichment should be verified against official sources.
|
|
466
462
|
- The script does not deduplicate the same physical part across multiple images unless you handle that in the enrichment/review step.
|
|
467
463
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vision-electronic-indexing-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Pi package for agent-assisted electronics/PCB image inventory with Cloudflare Workers AI vision and datasheet enrichment.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,14 @@
|
|
|
38
38
|
"typebox": "*"
|
|
39
39
|
},
|
|
40
40
|
"pi": {
|
|
41
|
-
"extensions": [
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
"extensions": [
|
|
42
|
+
".pi/extensions/vision-inventory-mcp/index.ts"
|
|
43
|
+
],
|
|
44
|
+
"skills": [
|
|
45
|
+
".pi/skills"
|
|
46
|
+
],
|
|
47
|
+
"prompts": [
|
|
48
|
+
".pi/prompts"
|
|
49
|
+
]
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -92,12 +92,8 @@ def candidate_from_item(item: Dict[str, Any]) -> str:
|
|
|
92
92
|
def extract_part_evidence(image_name: str, result: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
93
93
|
evidence: List[Dict[str, Any]] = []
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
("first_pass_items", result.get("first_pass_items", [])),
|
|
98
|
-
):
|
|
99
|
-
if not isinstance(items, list):
|
|
100
|
-
continue
|
|
95
|
+
items = result.get("items", [])
|
|
96
|
+
if isinstance(items, list):
|
|
101
97
|
for item in items:
|
|
102
98
|
if not isinstance(item, dict):
|
|
103
99
|
continue
|
|
@@ -108,7 +104,7 @@ def extract_part_evidence(image_name: str, result: Dict[str, Any]) -> List[Dict[
|
|
|
108
104
|
continue
|
|
109
105
|
evidence.append({
|
|
110
106
|
"image": image_name,
|
|
111
|
-
"source":
|
|
107
|
+
"source": "items",
|
|
112
108
|
"position_hint": item.get("position_hint", "unknown"),
|
|
113
109
|
"observed_marking": marking,
|
|
114
110
|
"candidate_part": candidate_from_item(item),
|
|
@@ -229,18 +225,17 @@ def lookup_enrichment(part: str, cache: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
229
225
|
return {}
|
|
230
226
|
|
|
231
227
|
|
|
232
|
-
def estimate_amount_for_candidate(result: Dict[str, Any], candidate: str) -> int:
|
|
233
|
-
"""Estimate physical IC quantity for one image
|
|
228
|
+
def estimate_amount_for_candidate(result: Dict[str, Any], candidate: str, evidence_count: int = 1) -> int:
|
|
229
|
+
"""Estimate physical IC quantity for one candidate in one image.
|
|
234
230
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
Count separate matching IC items. The schema field count_index is treated as
|
|
232
|
+
an ordinal/index, not a quantity. Fall back to the number of candidate
|
|
233
|
+
evidence rows when only observations are available.
|
|
238
234
|
"""
|
|
239
235
|
items = result.get("items", [])
|
|
240
236
|
if not isinstance(items, list):
|
|
241
|
-
return 1
|
|
237
|
+
return max(1, evidence_count)
|
|
242
238
|
|
|
243
|
-
amount = 0
|
|
244
239
|
matched = 0
|
|
245
240
|
for item in items:
|
|
246
241
|
if not isinstance(item, dict):
|
|
@@ -250,17 +245,10 @@ def estimate_amount_for_candidate(result: Dict[str, Any], candidate: str) -> int
|
|
|
250
245
|
if candidate_from_item(item).upper() != candidate.upper():
|
|
251
246
|
continue
|
|
252
247
|
matched += 1
|
|
253
|
-
|
|
254
|
-
count = int(item.get("count_index", 1))
|
|
255
|
-
except Exception:
|
|
256
|
-
count = 1
|
|
257
|
-
amount += max(1, count)
|
|
258
|
-
|
|
259
|
-
if amount > 0:
|
|
260
|
-
return amount
|
|
248
|
+
|
|
261
249
|
if matched > 0:
|
|
262
250
|
return matched
|
|
263
|
-
return 1
|
|
251
|
+
return max(1, evidence_count)
|
|
264
252
|
|
|
265
253
|
|
|
266
254
|
def image_part_rows(results: List[Dict[str, Any]], cache: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
@@ -288,37 +276,41 @@ def image_part_rows(results: List[Dict[str, Any]], cache: Dict[str, Any]) -> Lis
|
|
|
288
276
|
})
|
|
289
277
|
continue
|
|
290
278
|
|
|
291
|
-
# One image
|
|
292
|
-
|
|
279
|
+
# One image may contain multiple different IC candidates. Emit one
|
|
280
|
+
# evidence row per candidate instead of forcing a single image-level part.
|
|
281
|
+
evidence_by_candidate: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
293
282
|
for row in evidence:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
for row in
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
283
|
+
candidate = row["candidate_part"].upper()
|
|
284
|
+
if candidate and candidate.lower() not in UNKNOWN_MARKINGS:
|
|
285
|
+
evidence_by_candidate[candidate].append(row)
|
|
286
|
+
|
|
287
|
+
for candidate, candidate_evidence in sorted(evidence_by_candidate.items()):
|
|
288
|
+
enrichment = lookup_enrichment(candidate, cache)
|
|
289
|
+
amount = estimate_amount_for_candidate(result, candidate, evidence_count=len(candidate_evidence))
|
|
290
|
+
observed_markings = sorted({row["observed_marking"] for row in candidate_evidence})
|
|
291
|
+
observations = "; ".join(
|
|
292
|
+
f"{row['position_hint']}: {row['observed_marking']} ({row['marking_confidence']})"
|
|
293
|
+
for row in candidate_evidence
|
|
294
|
+
)
|
|
295
|
+
confidence_values = [str(row.get("marking_confidence", "unknown")) for row in candidate_evidence]
|
|
296
|
+
needs_review = any(row.get("needs_review", True) for row in candidate_evidence) or not enrichment.get("verified", False)
|
|
305
297
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
298
|
+
rows.append({
|
|
299
|
+
"image": image_name,
|
|
300
|
+
"candidate_part": candidate,
|
|
301
|
+
"normalized_part": enrichment.get("normalized_part", candidate),
|
|
302
|
+
"amount": amount,
|
|
303
|
+
"description": enrichment.get("description", ""),
|
|
304
|
+
"datasheet_url": enrichment.get("datasheet_url", ""),
|
|
305
|
+
"manufacturer": enrichment.get("manufacturer", ""),
|
|
306
|
+
"verified": bool(enrichment.get("verified", False)),
|
|
307
|
+
"vision_confidence": "/".join(sorted(set(confidence_values))),
|
|
308
|
+
"needs_review": needs_review,
|
|
309
|
+
"observed_markings": " | ".join(observed_markings),
|
|
310
|
+
"observations": observations,
|
|
311
|
+
"raw_json": entry["raw_json"],
|
|
312
|
+
"notes": enrichment.get("notes", "Missing datasheet enrichment"),
|
|
313
|
+
})
|
|
322
314
|
return rows
|
|
323
315
|
|
|
324
316
|
|
package/vision_inventory_mcp.py
CHANGED
|
@@ -127,59 +127,6 @@ Rules:
|
|
|
127
127
|
""".strip()
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
def build_ic_consensus_prompt(image_name: str, first_pass: Dict[str, Any]) -> str:
|
|
131
|
-
first_pass_json = json.dumps(first_pass, indent=2, ensure_ascii=False)
|
|
132
|
-
return f"""
|
|
133
|
-
Analyze this electronics image again, but focus ONLY on the IC package top markings.
|
|
134
|
-
|
|
135
|
-
Image filename: {image_name}
|
|
136
|
-
|
|
137
|
-
Important known constraint for this dataset:
|
|
138
|
-
- All ICs visible in one image should have the same part marking.
|
|
139
|
-
- The previous pass may contain OCR mistakes or hallucinated similar-looking 74LS part numbers.
|
|
140
|
-
- Do not trust the previous pass. Use it only as a list of candidates to verify against the image.
|
|
141
|
-
- Return ONE consensus IC inventory item for the shared marking, not separate items with different markings.
|
|
142
|
-
- If any character is unclear, use [?] in that character position and set needs_review=true.
|
|
143
|
-
- Prefer low confidence with [?] over a confident but guessed part number.
|
|
144
|
-
- Do not use web lookup.
|
|
145
|
-
|
|
146
|
-
Previous pass to verify:
|
|
147
|
-
{first_pass_json}
|
|
148
|
-
|
|
149
|
-
Return only valid JSON using this schema:
|
|
150
|
-
|
|
151
|
-
{{
|
|
152
|
-
"image": "{image_name}",
|
|
153
|
-
"items": [
|
|
154
|
-
{{
|
|
155
|
-
"item_type": "IC",
|
|
156
|
-
"count_index": 1,
|
|
157
|
-
"package_marking": "one consensus visible marking shared by all ICs, or [?]-marked partial text",
|
|
158
|
-
"marking_confidence": "high | medium | low | unreadable",
|
|
159
|
-
"likely_part": "same as package_marking if visible, or unknown",
|
|
160
|
-
"description": "consensus result; include the visible IC count if clear",
|
|
161
|
-
"position_hint": "multiple ICs / board-wide IC group / etc.",
|
|
162
|
-
"needs_review": true
|
|
163
|
-
}}
|
|
164
|
-
],
|
|
165
|
-
"warnings": [],
|
|
166
|
-
"ic_marking_observations": [
|
|
167
|
-
{{
|
|
168
|
-
"position_hint": "where this individual IC appears",
|
|
169
|
-
"package_marking": "best visible marking for this individual IC, or [?]-marked partial text",
|
|
170
|
-
"marking_confidence": "high | medium | low | unreadable"
|
|
171
|
-
}}
|
|
172
|
-
]
|
|
173
|
-
}}
|
|
174
|
-
|
|
175
|
-
Rules:
|
|
176
|
-
- Return JSON only.
|
|
177
|
-
- Return exactly one consensus IC item if ICs are visible.
|
|
178
|
-
- Also list each visible IC's individual marking observation in ic_marking_observations.
|
|
179
|
-
- Individual observations may disagree; do not force them to match. Use [?] for unclear characters.
|
|
180
|
-
- Do not return multiple different consensus IC items.
|
|
181
|
-
""".strip()
|
|
182
|
-
|
|
183
130
|
|
|
184
131
|
mcp = FastMCP("Vision Inventory")
|
|
185
132
|
|
|
@@ -521,50 +468,6 @@ def visible_ic_items(result: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
|
521
468
|
]
|
|
522
469
|
|
|
523
470
|
|
|
524
|
-
def verify_ic_consensus_pass(
|
|
525
|
-
image_data_url: str,
|
|
526
|
-
image_name: str,
|
|
527
|
-
first_pass: Dict[str, Any],
|
|
528
|
-
account_id: str,
|
|
529
|
-
api_token: str,
|
|
530
|
-
) -> Dict[str, Any]:
|
|
531
|
-
"""Run a second vision pass that enforces the project rule that all ICs match."""
|
|
532
|
-
ic_items = visible_ic_items(first_pass)
|
|
533
|
-
if len(ic_items) < 2:
|
|
534
|
-
return first_pass
|
|
535
|
-
|
|
536
|
-
response_text, cloudflare_error = call_workers_ai(
|
|
537
|
-
image_data_url=image_data_url,
|
|
538
|
-
image_name=image_name,
|
|
539
|
-
user_prompt=build_ic_consensus_prompt(image_name, first_pass),
|
|
540
|
-
account_id=account_id,
|
|
541
|
-
api_token=api_token,
|
|
542
|
-
model=DEFAULT_MODEL,
|
|
543
|
-
)
|
|
544
|
-
if cloudflare_error:
|
|
545
|
-
first_pass.setdefault("warnings", []).append(
|
|
546
|
-
f"IC consensus verification failed: {cloudflare_error.get('message', 'unknown error')}"
|
|
547
|
-
)
|
|
548
|
-
return first_pass
|
|
549
|
-
assert response_text is not None
|
|
550
|
-
|
|
551
|
-
parsed, parse_error = extract_json_object(response_text)
|
|
552
|
-
if parse_error or parsed is None:
|
|
553
|
-
first_pass.setdefault("warnings", []).append(
|
|
554
|
-
f"IC consensus verification returned invalid JSON: {parse_error or 'unknown parse error'}"
|
|
555
|
-
)
|
|
556
|
-
first_pass["ic_consensus_raw_response"] = response_text
|
|
557
|
-
return first_pass
|
|
558
|
-
|
|
559
|
-
consensus = normalize_inventory_result(parsed, image_name)
|
|
560
|
-
previous_markings = [str(item.get("package_marking", "unknown")) for item in ic_items]
|
|
561
|
-
consensus.setdefault("warnings", []).append("Multi-pass IC consensus verification applied.")
|
|
562
|
-
consensus.setdefault("warnings", []).append(
|
|
563
|
-
"First pass IC marking candidates: " + ", ".join(previous_markings)
|
|
564
|
-
)
|
|
565
|
-
consensus["first_pass_items"] = first_pass.get("items", [])
|
|
566
|
-
return consensus
|
|
567
|
-
|
|
568
471
|
|
|
569
472
|
def process_image_impl(
|
|
570
473
|
image_path: str,
|
|
@@ -615,14 +518,7 @@ def process_image_impl(
|
|
|
615
518
|
"raw_response": response_text,
|
|
616
519
|
}
|
|
617
520
|
|
|
618
|
-
|
|
619
|
-
return verify_ic_consensus_pass(
|
|
620
|
-
image_data_url=image_data_url,
|
|
621
|
-
image_name=image_name,
|
|
622
|
-
first_pass=first_pass,
|
|
623
|
-
account_id=account_id,
|
|
624
|
-
api_token=api_token,
|
|
625
|
-
)
|
|
521
|
+
return normalize_inventory_result(parsed, image_name)
|
|
626
522
|
|
|
627
523
|
|
|
628
524
|
@mcp.tool()
|