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.
@@ -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
- - Runs a second IC consensus pass when multiple ICs are detected in one image.
17
- - Preserves individual IC marking observations, because the model may read one chip correctly and another incorrectly.
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 consensus behavior
210
+ ## Multiple-IC behavior
211
211
 
212
- For this lab workflow, the program assumes that all ICs visible in one image should be the same part family/marking.
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. First pass: general visible inventory extraction.
217
- 2. If multiple ICs are found, second pass: IC-only consensus verification.
218
- 3. Final output includes:
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 helps expose uncertainty and preserves alternate readings.
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 vision result's IC count. `sighting_count` is the number of image-level sightings that were merged into the BOM row.
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": "consensus result; 4 visible ICs",
419
- "position_hint": "multiple ICs",
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
- "position_hint": "top-right",
429
- "package_marking": "SN74LS283N F 7936",
430
- "marking_confidence": "high"
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
- "first_pass_items": []
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
- - The consensus pass can enforce one shared IC result, but it can still choose the wrong consensus.
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.0",
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": [".pi/extensions/vision-inventory-mcp/index.ts"],
42
- "skills": [".pi/skills"],
43
- "prompts": [".pi/prompts"]
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
- for source, items in (
96
- ("items", result.get("items", [])),
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": 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-level sighting.
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
- The schema's historical field name is count_index, but in this lab workflow
236
- the model often uses it as the count for a grouped consensus item. We sum
237
- matching primary items and fall back to one when no usable count is available.
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
- try:
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 contributes one sighting for its most common candidate.
292
- counts: Dict[str, int] = defaultdict(int)
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
- counts[row["candidate_part"].upper()] += 1
295
- candidate = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[0][0]
296
- enrichment = lookup_enrichment(candidate, cache)
297
- amount = estimate_amount_for_candidate(result, candidate)
298
- observed_markings = sorted({row["observed_marking"] for row in evidence})
299
- observations = "; ".join(
300
- f"{row['position_hint']}: {row['observed_marking']} ({row['marking_confidence']})"
301
- for row in evidence
302
- )
303
- confidence_values = [str(row.get("marking_confidence", "unknown")) for row in evidence]
304
- needs_review = any(row.get("needs_review", True) for row in evidence) or not enrichment.get("verified", False)
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
- rows.append({
307
- "image": image_name,
308
- "candidate_part": candidate,
309
- "normalized_part": enrichment.get("normalized_part", candidate),
310
- "amount": amount,
311
- "description": enrichment.get("description", ""),
312
- "datasheet_url": enrichment.get("datasheet_url", ""),
313
- "manufacturer": enrichment.get("manufacturer", ""),
314
- "verified": bool(enrichment.get("verified", False)),
315
- "vision_confidence": "/".join(sorted(set(confidence_values))),
316
- "needs_review": needs_review,
317
- "observed_markings": " | ".join(observed_markings),
318
- "observations": observations,
319
- "raw_json": entry["raw_json"],
320
- "notes": enrichment.get("notes", "Missing datasheet enrichment"),
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
 
@@ -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
- first_pass = normalize_inventory_result(parsed, image_name)
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()