ZainabEman's picture
Upload 152 files
546c207 verified
|
Raw History Blame Contribute Delete
55.5 kB
# Project Specification Document (PSD) **Project:** Spare-Parts Diagram Locator for the BQ188 Warp Tying Machine catalogue **Source document:** `1.pdf` (project root) **Status:** Phase 1 โ€” specification complete, implementation not started **Last updated:** 2026-08-30 > This document is the permanent source of truth for the project. Every statement in > section 4 ("PDF / document structure discovered") was verified programmatically > against `1.pdf`. Nothing in that section is assumed. --- ## 1. Project overview A small, entirely local desktop tool. The user types a machine spare-part number (e.g. `30642`). The tool finds the catalogue page that contains that part, renders the exploded-view diagram from that page, and draws a **red rectangle around the physical component** the number refers to โ€” not merely around the printed number โ€” with the part number and name shown next to it. The catalogue is a fixed, known, 55-page PDF. The document never changes at runtime. Therefore all expensive work (text extraction, OCR, geometry) happens **once, offline, in a build step**. The interactive application only performs a dictionary lookup and draws an overlay. Lookup is deterministic and instant. This is a **document-processing / computer-vision** project, not a retrieval or question-answering project. See section 19 for what is explicitly out of scope. --- ## 2. Exact problem being solved Given a 5-digit part number `P`: | Step | Requirement | |---|---| | R1 | Determine every catalogue page on which `P` appears. | | R2 | Retrieve the part's name (German / English / French) and the section it belongs to. | | R3 | Render that page's exploded-view diagram as an image. | | R4 | Find every callout label reading `P` inside that diagram. | | R5 | For each label, resolve the **component** the label points to, and produce a bounding box around that component's drawn geometry. | | R6 | Draw a red rectangle on the component, and print `P` (plus name) next to it. | | R7 | Report page number, section number, section title, and part name. | **The hard requirement is R5.** R4 (finding the printed text) is easy and already proven. R5 requires following the drawing's leader line from the label to the part. ### Why this is not a text-search problem The number `30642` appears twice on page 34: once as a row in the machine-readable parts table on the left half of the page, and once as a raster callout inside the diagram on the right half. Boxing either of those occurrences boxes *text*. The component itself โ€” a worm gear (`Schnecke`) โ€” carries no text at all. It is identified only by a thin line drawn from the callout to the part outline. --- ## 3. User workflow ``` 1. User launches the app โ†’ streamlit run app.py 2. User types a part number โ†’ "30642" 3. App validates / autocompletes โ†’ suggests from 705 known numbers 4. App shows result header โ†’ 30642 ยท Schnecke,kpl. / worm,cpl. / vis sans fin,cpl Section 07 "Motor-Antrieb / Motor-drive" ยท PDF page 34 5. App shows the diagram โ†’ full-page diagram image ยท RED rectangle on the physical component ยท part number drawn next to the rectangle ยท thin blue rectangle on the printed callout label (so the user can verify) ยท confidence badge: "located" / "approximate" / "label only" 6. If the part appears on several pages, a page selector appears (61 parts do). 7. If the part has several instances on one page, all instances are boxed. 8. User can zoom / download the annotated PNG. ``` Failure is always visible, never silent: if the component cannot be resolved, the app still shows the page and boxes the label, and says so. --- ## 4. PDF / document structure discovered All facts below were extracted from `1.pdf` with PyMuPDF 1.28.2. ### 4.1 File-level | Property | Value | |---|---| | Pages | 55 | | Page size | 1190 ร— 842 pt (A4 landscape), rotation 0, uniform on every page | | PDF version | 1.7 | | Producer / Creator | Microsoftยฎ Excelยฎ 2019 | | Created | 2022-08-21 | | Encrypted | No | | Bookmarks / outline | None (0 entries) | | File size | ~39.8 MB | | Machine | BQ188 Warp Tying Machine โ€” Jingong Industrial Co., Ltd | The PDF was exported from an Excel workbook. This matters: the parts tables are **real vector text in a real table grid**, while every diagram is a **flat embedded JPEG**. There is no vector geometry for the drawings. ### 4.2 Page map | PDF pages | Content | Text layer | Images | |---|---|---|---| | 1 | Title page (BQ188, 4 languages, company address) | Yes (376 chars) | 3 | | 2 โ€“ 23 | Scanned **operation manual** โ€” prose + photographs | No real text; only the running footer (98 chars/page) | 2 per page | | 24 | `NEEDLE TABLE` โ€” yarn-count / needle-number table | Yes (vector text, 74 vector drawings) | 0 | | 25 | **Spare-parts catalogue cover** โ€” ordering instructions in DE/EN/FR/ES | Yes | 2 | | 26 โ€“ 55 | **Spare-parts catalogue โ€” 30 section pages** | Yes (table) | 1 visible diagram + decoys | **The spare-parts catalogue begins at PDF page 25 (cover); the first section page with parts and a diagram is PDF page 26.** Pages 1โ€“24 contain no part numbers and are out of scope for indexing (they may still be viewable in the UI). ### 4.3 Section structure (pages 26โ€“55) Sections are numbered `01`โ€“`27` with three sub-sections. The mapping is complete and was read from the first table row of each page: | Page | Sec | German | English | Models | |---|---|---|---|---| | 26 | 01 | Abdeckhaube | Cover | PU-ELA-Z | | 27 | 02 | Lampe+Geblรคse Kurze Knoten | Lamp+fan short knot | | | 28 | 03 | Abdeckblech,hinten | Cover plate,rear | | | 29 | 04 | Abdeckblech,links+AbschluรŸblende Kurbeldeckel | Cover plate,left+Crank end cover cap | | | 30 | 04-1 | Abdeckblech,link. fรผr kurze knoten | Cover plate,left+for short knot | | | 31 | 04-2 | Kurze Knoten | Short knot | | | 32 | 05 | Gehรคuseplatte,oben,unten | Housing plate,above,below | | | 33 | 06 | Handantrieb+Fadenabstreifer+Schutzblech | Hand drive+Thread wipers+Fender | PU-ELA/PU-ELA-Z/PU-S/PU-DK/PU-DKU | | 34 | 07 | Motor-Antrieb | Motor-drive | | | 35 | 08 | Tastfรผhler+Tastfรผhler steuerung | Feeler+Feeler control | PU | | 36 | 09 | Fadenschieber+Bรผrstenhalter | Thread spool+Brush holder | PU | | 37 | 10 | Kreuzhebel | Cross lever | PU-ELA/PU-ELA-Z/PU-DK/PU-C/PU-CC/PU-S | | 38 | 11 | Fรผhrungsplatte | Guide plate | PU | | 39 | 12 | Nadelanschlag+Nadelhebelwelle | Needle+Needle stop lever shaft | | | 40 | 13 | Mittelgruppe-Kipper | Central group-tipper | PU-ELA | | 41 | 13-1 | Mittelgruppe Kipper | Middle group tipper | PU-ELA | | 42 | 14 | Kurze Knoten Schere | Short knot scissors | | | 43 | 15 | Fadenklemme+Klemmenhebellager | Thread clamp+clamping lever bearing | | | 44 | 16 | Knotenhelfer+Knรผpferlager+Knรผpfernadel | Lever hinge+Knotter support+Knotter needle | | | 45 | 17 | Fadenhalter+Knotenauszieher+Fadenkanal | Thread holder+Knot extractor+Thread channel | | | 46 | 18 | Antriebslager+Transport | Drive bearing+Transport | | | 47 | 19 | Kipperwelle | Tipper Shaft | PU-ELA/PU-ELA-Z/PU-S/PU-DK/PU-DKU | | 48 | 20 | Gehรคuseplatte,re. li. | Housing plate,left,right | | | 49 | 21 | Antriebs+Mittel+Untere+Welle | Drive+Medium+Low+Shaft | | | 50 | 22 | Kreuzkurvenscheibe | Cross cam | PU | | 51 | 23 | Knรผpfrahmen | Knotting frame | PU | | 52 | 24 | Zubehรถr fรผr Knรผpfrahmen | Accessories for knotting frame | | | 53 | 25 | Innentraverse-Knรผpfrahmen | Knotting frame inner traverse | | | 54 | 26 | Maschinenwagen | Machine cart | | | 55 | 27 | ร–lwanne | Oil pan | | ### 4.4 Layout of a catalogue page Every one of the 30 pages has the identical two-column layout: ``` (0,0) (1190,0) +-------------------------------+-------------------------------+ | PARTS TABLE (vector text) | EXPLODED DIAGRAM (JPEG) | | x ~ 17 ... 607 | x ~ 607 ... 1140 | | y ~ 28 ... (varies) | y ~ 28 ... 800 | | | | | row 0 : section header | header: model codes, sec no | | row 1 : Art.Nr | Bezeichnung | body: line-art drawing | | | Description | + 5-digit callouts | | | Denomination | + leader lines | | rows 2+: one part per row | footer: drawing path, Seite | +-------------------------------+-------------------------------+ page footer: company / URL (1190,842) ``` ### 4.5 The parts table (left column) โ€” machine-readable * `page.find_tables()` returns exactly **one** table on every catalogue page, always with **4 columns**, and the `Art. Nr.` header is always at row index 1 (row 0 is the section header). This was verified on all 30 pages. * Columns: `Art. Nr.` | `Bezeichnung` (DE) | `Description` (EN) | `Dรฉnomination` (FR). * **861 part rows** in total, **705 distinct part numbers**. * Part numbers are **exactly 5 digits**, no letters, no separators. Verified length distribution: `{5 digits: 860, 6 digits: 1, 2 digits: 27}`. The 27 two-digit values are the section-header cells (`01`โ€ฆ`27`). The single 6-digit value is a typo in the source document: page 47, `351159 / Kipperhebel / tipper / culbuteur` โ€” and the diagram on that same page prints it as `35159`, confirming the table cell is wrong. * Observed number prefixes: `73` (281), `74` (239), `75` (73), `30` (48), `33` (44), `70`, `76`, `78`, `77`, `32`, `35`, `72`. * **61 part numbers appear on more than one page** (62 have more than one occurrence; the extra one is `33275`, listed twice on page 43 โ€” see below). Examples: `73290` on 9 pages (31, 35, 36, 37, 39, 42, 45, 47, 49); `73289` on 5 pages; `70507` on 4. A lookup therefore returns a *set* of pages, not one page. * Only one within-page duplicate table row exists: `33275` on page 43. * **Non-part rows must be filtered.** Pages 42, 51 and 52 end with footnote rows whose `Art. Nr.` cell is empty or contains `*`, `**`, `***`: `* = "nur bei Ausfรผhrung kurze Knoten" (only for short knot)`, `** = "Lรคnge angeben" (indicate length)`, `*** = "Lรคnge und Durchmesser (18 oder 17) angeben"`, `S = Sonderzubehรถr (special accessories)`. Some rows appear *after* the footnotes and are still real parts (e.g. page 52 `30343`โ€“`30346`, page 51 `73013`), so filtering must be per-row, not "stop at first footnote". * The description columns already contain OCR-era typos inherited from whoever built the Excel file: `lsolierplatte` (lowercase L for I), `Kreuzsohnur` for `Kreuzschnur`, `Acoesories`, and full-width `๏ผŽ` instead of `.`. **These are cosmetic. The `Art. Nr.` column is clean.** Descriptions are displayed as-is; they are never used for matching. * A leading `*`, `**` or `***` inside the `Bezeichnung` cell is a footnote marker (e.g. `*Handkamm`, `**Kluppe`) and should be parsed off into a `footnote` field. ### 4.6 The diagram (right column) โ€” raster only * Each catalogue page has **exactly one visible diagram image**, embedded as a **DCTDecode (JPEG), DeviceRGB** object. * Native resolution is **1689 px wide ร— 2248โ€“2367 px tall** on all 30 pages (1689 ร— 2352 for page 34). Placed at โ‰ˆ 533 ร— 770 pt โ†’ **effective โ‰ˆ 228 dpi**. Rendering the *page* at 300 dpi would upsample past native; the correct move is to **extract the embedded image at native resolution** and OCR that. * Despite the RGB container, content is essentially bilevel line art: dark-pixel fraction (`gray < 128`) is **2.2 % โ€“ 10.6 %** per page, mid-tones 0.8 % โ€“ 3.0 %. There are **no halftone photographs** in the catalogue section (unlike pages 2โ€“23 of the manual). * **Decoy images.** Excel left extra image objects on some pages whose placement rectangles fall partly or wholly **outside** the page box โ€” e.g. page 26 has a second image at `y = 802 โ€ฆ 1576` (page height is 842), pages 29 and 54 have one at negative `y`. Additionally the **same xref is reused across consecutive pages** (xref 152 is the second image of page 36 and the visible diagram of page 37). โ†’ **The diagram must be selected as the image rect with the largest intersection area with `page.rect`**, and cached per *page*, not per xref. ### 4.7 How part numbers are represented inside the diagram Verified by rendering and by OCR: * Callouts are the **actual 5-digit part numbers** (`30642`, `74506`, `73631`), drawn in a clean ISO-style technical font, roughly 90 px wide ร— 30 px tall at native resolution. There is **no separate item-number scheme** โ€” no "item 1, 2, 3" that would need a second lookup table. This is the single most favourable property of this document. * Some callouts carry a leading **asterisk** (`*30638`, `*74012`, `*76352`). The asterisk is the footnote marker described in 4.5 and must be stripped before matching. * Some callouts are **enclosed in a drawn rectangle** (`31269`, `32357`, `33333`, `35037`, `36962`, `30907`, `34983`, `30168`, `30169`). Cross-checking these against the table shows they are consistently the **`kpl.` / `cpl.` (complete assembly)** entries โ€” e.g. `31269 Abdeckhaube,kpl. / covering hood cpl.`, `33333 Mittelgruppe,kpl. / middle group cpl.`. A boxed callout therefore denotes a **sub-assembly**, and its "component" is the whole group shown, not one item. The box also means the label glyphs are *not* isolated components โ€” the drawn rectangle around them must be recognised and excluded, or it will be mistaken for the leader line. * The same part number can appear **several times on one page** when the part is used in several places โ€” page 26 has `73060` ร—2, `74014` ร—3, `74025` ร—2, `79038` ร—2; page 34 has `74506` ร—4. Every instance must be indexed and boxed. * Non-part text also appears in the diagram and must be rejected: * top-left: model codes โ€” `PU-ELA`, `PU-ELA-Z`, `PU-JET` * top-right: section number โ€” `07`, `K 13` * bottom-left: drawing path โ€” `D / PU-S-12 / PU-ELA / ET / ET- 07 Motor-Antrieb` * bottom-right: the manufacturer's internal page number โ€” `Seite 18` (this is **not** the PDF page number: PDF page 34 is "Seite 18") * small revision/date codes such as `061101`, `051201`, `110702` ### 4.8 How labels relate spatially to components โ€” leader lines This was investigated directly on the pixel data of page 34. * Callouts sit in white space and are joined to their component by a **thin solid leader line**, typically 3โ€“5 px wide at native resolution. Most are a single straight segment; some are kinked polylines; a few are **dashed** (page 40: `73158`, `71058`). * The line starts a few pixels away from the label's bounding box (it does **not** touch the glyphs โ€” verified: the digits of `30642` are their own connected components, distinct from the line) and **terminates touching the outline of the target component**. * **Consequence, and the key finding of Phase 1:** because the leader line touches its target and nothing else, the 8-connected component containing the leader line *is* the target component. Running `cv2.connectedComponentsWithStats` on `gray < 150` for page 34 gave 586 components; the component containing the `30642` leader line has bbox `x[310:520] y[1472:1671]`, which is an exact, tight fit around the worm gear. No template matching, no segmentation model, no training data required. * **Some callouts have no leader line at all** and simply sit adjacent to their part (visible on page 40). These need a nearest-component fallback. * Nothing in the diagram uses arrowheads in the catalogue section (arrowheads do appear in the *manual*, pages 2โ€“23, which is out of scope). So the relationship is confirmed as: ``` part number (table) -> identical printed callout (raster) -> leader line -> component outline ``` ### 4.9 What makes localization difficult (measured, not assumed) Running the prototype over all 34 validated labels on page 34 gave roughly 80 % tight, correct component boxes. The failures are of four kinds: 1. **Leader-line bleed.** If a leader line *crosses* a large neighbouring outline on its way to a small part, the line, the neighbour and the target all merge into one connected component and the box balloons. On page 34, labels `74506`, `74903` and `79105` all resolved to the large gearbox housing component (347 ร— 533 px, 4.7 % of the image) instead of their small rings. 2. **Frame merge.** Any component that touches the diagram's outer border merges with it. Page 34's border component has a bbox covering 64 % of the image. A size guard is mandatory. 3. **Nearest-neighbour capture.** When another part's outline happens to be closer to the label than the label's own leader line, the wrong component wins. On page 34, `74025` (a screw) captured the bracket that belongs to `76352`. 4. **Assembly callouts.** A boxed `kpl.` callout points at a whole group; the "right" answer is a group of components, and any single-component box is arguably wrong. None of these need machine learning to fix โ€” they need a leader-line-aware tracing step (section 10) plus a small manual-override file. ### 4.10 OCR feasibility โ€” measured OCR was actually run, not assumed. Engine: **RapidOCR (ONNX Runtime, PP-OCRv4 weights)**, CPU only, on the natively extracted JPEG, for all 30 catalogue pages. * Runtime โ‰ˆ **11 s per diagram** on this machine; โ‰ˆ **6 minutes for all 30 pages**, once, offline. * Page 34: 39 text boxes returned, **every one of the 34 real callouts recognised**, confidence โ‰ฅ 0.92 for all of them and 1.00 for most. Only two character-level errors, both trivially repaired by stripping non-digits: `*7.4012` โ†’ `74012` (spurious dot), `-74506` โ†’ `74506` (leader-line dash absorbed). * **Full-catalogue recall** โ€” distinct part numbers OCR'd from each diagram, matched against that page's own table, summed over all 30 pages: | | | |---|---| | Table part rows, pages 26โ€“55 | **861** | | Matched by OCR in the diagram | **817** | | **Recall** | **94.9 %** | | Pages at 100 % | **9 of 30** | | Worst page | **39 โ†’ 40/47 = 85 %** | | Unrecognised tokens, all 30 pages | **69 total (~2.3/page)** | Per-page examples: p28 14/14, p30 19/19, p31 27/27, p36 21/21, p41 25/25, p45 37/37, p50 15/15, p26 33/36, p34 27/29, p40 59/61. The residual "missing" entries are largely parts that are genuinely **listed in the table but not drawn** in that diagram, not OCR misses โ€” the catalogue does this deliberately for hardware variants. **94.9 % is therefore a lower bound on true OCR recall.** * **The reverse case also exists:** callouts appear in a diagram that are absent from that page's table โ€” page 40 shows `73182`, `73636`, `74167`; page 36 shows `33134`, `33141`; page 33 shows `73007`, `70507`. So validating OCR output against the *page's* table alone would silently drop real labels. Validation must be against the **global** 705-number set, with the page's own table used only as a confidence bonus. * The 69 unrecognised tokens are almost entirely the known furniture from 4.7 โ€” `061101`-style revision codes and the `Seite NN` + section-number pair merging into things like `1207` โ€” plus a handful of truncated reads (`4639`, `7497`, `4401`) that recover under edit-distance-1 snapping. **Conclusion: OCR works directly on this document and needs no deskewing, no binarization tuning and no custom training.** Pages do not need to be rendered โ€” the embedded image is extracted at native resolution instead, which is both faster and sharper. --- ## 5. Core technical approach **Build once, look up instantly.** 1. **Offline indexer** (`python -m spareparts.build_index`) processes `1.pdf` end to end and writes a SQLite database plus a PNG cache. Runs in ~10 minutes. Re-runnable, idempotent, and each stage is independently re-runnable so OCR is not repeated when only the localization tuning changes. 2. **Streamlit app** reads the database, does a dictionary lookup, loads the cached PNG, draws the overlay with OpenCV, and displays it. No OCR, no PDF parsing at runtime. Three deliberately deterministic decisions: * **The parts table is the authority.** Names, sections and the canonical part list all come from the PDF's real text layer, never from OCR. * **OCR only ever produces coordinates**, never facts. Every OCR token is snapped to a known part number or discarded. * **Localization is classical CV** โ€” binarize, connected components, leader-line trace. No detector is trained, no model is fine-tuned, nothing is learned from data. --- ## 6. Proposed architecture ``` +-------------------------- BUILD TIME (offline, once) --------------------------+ | | | 1.pdf | | | | | +--> pdf_loader.py classify pages, pick the visible diagram per page, | | | extract it at native resolution -> data/pages/p##.png| | | | | +--> table_parser.py find_tables() -> section header + part rows | | | -> parts, occurrences | | | | | +--> ocr.py RapidOCR on each diagram -> raw text boxes | | | | | +--> matcher.py normalize, reject non-part text, snap to the 705- | | | number set (exact -> edit-distance-1) -> labels | | | | | +--> localizer.py binarize -> connected components -> leader-line trace| | | -> component box + method + confidence -> components | | | | | +--> overrides.json hand corrections, applied last, always win | | | | | +--> index_db.py -> data/catalog.db (SQLite) | | | +--------------------------------------------------------------------------------+ | +---------------------------- RUN TIME (Streamlit) ------------------------------+ | | | app.py --> lookup.py (SQLite queries) --> overlay.py (OpenCV drawing) | | | +--------------------------------------------------------------------------------+ ``` There is no server, no API, no queue, no container. Two entry points: one script, one Streamlit app. --- ## 7. Data flow ### Build ``` 1.pdf +- for page in 26..55: diagram_rect = argmax over image rects of area(rect โˆฉ page.rect) # 4.6 png = extract_image(xref) -> data/pages/p34.png table = page.find_tables()[0].extract() row0 -> section code / DE / EN / model row1 -> header, skipped rows2.. -> part rows, footnote rows filtered -> parts, occurrences ocr_boxes = RapidOCR(png) strip non-digits; drop len != 5 drop boxes in header/footer strips (top 4 %, bottom 6 %) snap to global part set (exact, then edit-distance 1) -> labels bw = (gray(png) < 150) cc = connectedComponentsWithStats(bw, 8) glyph_ids = components fully inside any label box (+ their enclosing box, if boxed) for each label: anchor = trace_leader_line(label, bw, cc, glyph_ids) box, method, conf = resolve_component(anchor, cc) -> components +- apply overrides.json +- write catalog.db ``` ### Query ``` "30642" +- SELECT part -> name_de/en/fr +- SELECT occurrences -> [page 34, section 07 "Motor-Antrieb"] +- SELECT labels + components -> [{label_box, component_box, conf}] +- load data/pages/p34.png +- draw red rect + caption per instance, blue rect on label +- st.image(annotated) ``` --- ## 8. OCR strategy **Engine: RapidOCR (`rapidocr-onnxruntime`).** Rationale, and why *not* the stack originally guessed: * **PaddleOCR** uses the same PP-OCR models but drags in `paddlepaddle`, which is a large, frequently painful wheel on Windows and pins specific Python versions. RapidOCR ships the identical PP-OCRv4 detection + recognition models as ONNX and runs on `onnxruntime` CPU. It installed cleanly here in one `pip install` with no system dependencies, and was **verified working on this exact document at 94.9 % recall**. If PaddleOCR is later preferred, `ocr.py` exposes one function and the engine is swappable. * **Tesseract** was not chosen: it needs a system binary outside pip, and it is weaker on sparse, isolated numerals scattered on a mostly-empty canvas, which is precisely this document's layout. Settings and rules: * OCR the **natively extracted JPEG** (1689 px wide, ~228 dpi effective), **never** a re-rendered page. No upscaling โ€” measured accuracy is already โ‰ˆ 100 % on callouts. * No deskew, no rotation correction: every diagram is upright and axis-aligned. * Run **once per page** at build time; results are cached to `data/ocr_cache/p##.json` so that re-tuning localization never re-runs OCR. * Post-processing pipeline per detected box: 1. `re.sub(r'\D', '', text)` โ€” removes the `*` footnote marker, the spurious `.` from `*7.4012`, and the leading `-` from `-74506`. 2. Reject if length โ‰  5. 3. Reject if the box lies in the header strip (top 4 % of image height) or footer strip (bottom 6 %) โ€” this removes `Seite 18`, drawing paths and revision codes. 4. Snap to the global part set (section 9). * Confidence from the engine is stored per label and surfaced in the UI. --- ## 9. Part-number extraction strategy Two independent sources, with clearly separated authority: **A. Canonical list โ€” from the PDF text layer (authoritative).** `page.find_tables()` on pages 26โ€“55. Per row: `Art. Nr.` must match `^\d{5}$` after stripping whitespace; rows failing that (empty, `*`, `**`, `***`) are recorded as footnotes and excluded from the part list. The `Bezeichnung` cell's leading `*`s are split off into a `footnote` field. This yields **705 distinct part numbers / 861 occurrences**, and is the *only* source of names and sections. One known data defect to carry as-is with a flag: page 47's `351159` is 6 digits and the diagram prints `35159`, so it is a source typo. It is stored, marked `is_malformed = 1`, and excluded from OCR snapping. **B. Label positions โ€” from OCR (positional only).** Every OCR token is snapped to the canonical list before it is allowed to become a label: ``` 1. exact match against the 705-number set -> accept, conf = ocr_conf 2. else Levenshtein distance 1 to exactly one number -> accept, conf = ocr_conf * 0.8 (single candidate only โ€” ambiguous ties are dropped) 3. else -> discard, log to build report ``` A token that also appears in **its own page's table** gets a confidence bonus; a token that appears only in the global list is still accepted, because section 4.10 proved that diagrams legitimately show parts absent from their page's table. Because part numbers are exactly 5 digits with no letters, no checksum and no separators, no other normalization is needed. --- ## 10. Component localization strategy This is the heart of the project. Four tiers, tried in order, each recording which one fired so the UI can be honest about it. ### Preparation (once per page) ``` gray = cv2.cvtColor(img, COLOR_BGR2GRAY) bw = (gray < 150).astype(uint8) # verified threshold; art is bilevel n, cc, stats, _ = cv2.connectedComponentsWithStats(bw, 8) glyph_ids = components whose bbox lies inside a label bbox (+ small pad) frame_ids = components whose bbox area > 25 % of the image (border, page frame) box_ids = for boxed assembly callouts: the thin rectangle enclosing the glyphs ``` ### Tier A โ€” leader-line trace (primary) 1. Cast rays from the label's bounding box outward in 5ยฐ increments, from 4 px to ~40 px away from the box edge, looking for dark pixels that belong to a component that is neither a glyph nor the frame. This finds the leader line's near end. 2. Verify the candidate is *line-like*: skeletonize the local neighbourhood and check the stroke width is โ‰ค ~6 px and locally elongated. This is what distinguishes the real leader line from a neighbouring part's outline that happens to be closer (failure mode 3 in 4.9). 3. **Walk the line.** Follow the stroke pixel by pixel away from the label, allowing direction changes for kinks and a small pixel gap for dashed lines, until the stroke either ends or opens out into a wide (non-line-like) region. That transition point is the **anchor**. 4. Take the connected component containing the anchor. Its bbox is the component box. `method = "leader"`. ### Tier B โ€” bounded fallback (when Tier A's component is implausible) Triggered when the Tier A component's bbox exceeds ~8 % of the image area (leader-line bleed / frame merge, failure modes 1 and 2 in 4.9), or when no line-like stroke is found within 40 px. * If an anchor exists: emit a fixed-radius box centred on the anchor, sized from the page's median component size (roughly 120 ร— 120 px at native resolution). * If no anchor exists: take the nearest non-glyph, non-frame component to the label, with the same size cap. * `method = "anchor_radius"` / `"nearest"`, and confidence is lowered. The UI shows these as **"approximate"**. ### Tier C โ€” assembly callouts A callout enclosed in a drawn rectangle (`kpl.` / `cpl.` parts, section 4.7) points at a whole group. For these: trace the leader line as in Tier A, then take the **union** of all non-glyph components whose bbox centre lies within a generous radius of the anchor, capped at 30 % of the image. `method = "assembly"`. The UI labels these **"assembly (group)"**. ### Tier D โ€” manual override `data/overrides.json`, keyed `"<pdf_page>:<part_no>:<instance_index>"`, holding a box in native-image pixel coordinates plus a note. Applied after everything else and always wins. This is the pragmatic escape hatch for the residual ~10โ€“15 % โ€” a small build-time review tool (section 18, Phase 5) renders every auto box so they can be eyeballed and corrected quickly. ### If a label has no resolvable component at all The record is kept with `component_box = NULL`. The app still shows the page and boxes the **label** in blue, stating "label located, component not resolved". The tool is never allowed to draw a red box it cannot justify. ### Why not other approaches * **Hough transform for leader lines** โ€” considered and rejected as the *primary* method: it finds line segments globally but does not tell you which end is the label end, handles kinked polylines badly, and still needs the component-lookup step. It remains a reasonable Tier-A alternative if pixel-walking proves fragile. * **Training an object detector** โ€” needs annotated data that does not exist, and would be non-deterministic and unexplainable for a 30-page fixed document. * **Full manual annotation of all 861 occurrences** โ€” reliable but ~15 hours of work and defeats the point. The tiered approach automates the ~85 % and leaves a reviewable remainder. --- ## 11. Data / schema design SQLite, single file `data/catalog.db`. **All geometry is stored in native diagram-image pixel coordinates** (origin top-left of the extracted JPEG). PDF-page coordinates are derivable from `pages.rect_*` when needed; keeping one coordinate system avoids an entire class of bugs. ```sql CREATE TABLE pages ( pdf_page INTEGER PRIMARY KEY, -- 1-based, as the user sees it section_code TEXT, -- '07', '13-1' section_de TEXT, section_en TEXT, models TEXT, -- 'PU-ELA/PU-ELA-Z/...' img_xref INTEGER, img_w INTEGER, -- native px, e.g. 1689 img_h INTEGER, -- native px, e.g. 2352 rect_x0 REAL, rect_y0 REAL, -- placement on the PDF page, in points rect_x1 REAL, rect_y1 REAL, png_path TEXT -- 'data/pages/p34.png' ); CREATE TABLE parts ( part_no TEXT PRIMARY KEY, -- '30642' name_de TEXT, name_en TEXT, name_fr TEXT, is_malformed INTEGER DEFAULT 0 -- the 6-digit '351159' ); CREATE TABLE occurrences ( -- part listed in a page's table id INTEGER PRIMARY KEY, part_no TEXT REFERENCES parts(part_no), pdf_page INTEGER REFERENCES pages(pdf_page), row_index INTEGER, footnote TEXT, -- '', '*', '**', '***' UNIQUE(part_no, pdf_page, row_index) ); CREATE TABLE labels ( -- callout found in the diagram raster id INTEGER PRIMARY KEY, pdf_page INTEGER REFERENCES pages(pdf_page), part_no TEXT REFERENCES parts(part_no), instance INTEGER, -- 0,1,2... for repeated callouts on one page lx0 INTEGER, ly0 INTEGER, lx1 INTEGER, ly1 INTEGER, -- label bbox, native px has_asterisk INTEGER, -- callout was drawn as '*30638' is_boxed INTEGER, -- callout enclosed in a rectangle -> assembly ocr_text TEXT, -- raw, before normalization ocr_conf REAL, match_kind TEXT -- 'exact' | 'fuzzy1' ); CREATE TABLE components ( -- resolved physical component label_id INTEGER PRIMARY KEY REFERENCES labels(id), cx0 INTEGER, cy0 INTEGER, cx1 INTEGER, cy1 INTEGER, -- may be NULL anchor_x INTEGER, anchor_y INTEGER, -- leader-line endpoint method TEXT, -- 'leader'|'anchor_radius'|'nearest'|'assembly'|'override' confidence REAL, note TEXT ); CREATE INDEX ix_occ_part ON occurrences(part_no); CREATE INDEX ix_lab_part ON labels(part_no); CREATE INDEX ix_lab_page ON labels(pdf_page); ``` `data/overrides.json`: ```json { "34:74506:1": { "box": [1001, 1448, 1106, 1540], "note": "was capturing the housing" } } ``` `data/build_report.json` โ€” per page: OCR token count, accepted/rejected labels, table parts with no label, labels with no table row, per-method counts. Written on every build; this is the project's own quality dashboard. --- ## 12. Proposed project folder structure ``` ImageOCR/ โ”œโ”€โ”€ 1.pdf โ”œโ”€โ”€ PSD.md <- this document โ”œโ”€โ”€ README.md โ”œโ”€โ”€ requirements.txt โ”œโ”€โ”€ app.py <- Streamlit entry point โ”œโ”€โ”€ spareparts/ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”œโ”€โ”€ config.py <- paths, thresholds, page ranges (one place) โ”‚ โ”œโ”€โ”€ pdf_loader.py <- page classification, diagram extraction โ”‚ โ”œโ”€โ”€ table_parser.py <- parts tables -> parts / occurrences โ”‚ โ”œโ”€โ”€ ocr.py <- RapidOCR wrapper + cache โ”‚ โ”œโ”€โ”€ matcher.py <- token normalization + snap to part list โ”‚ โ”œโ”€โ”€ localizer.py <- binarize, CC, leader-line trace, tiers A-D โ”‚ โ”œโ”€โ”€ index_db.py <- schema, writes, migrations โ”‚ โ”œโ”€โ”€ lookup.py <- runtime queries โ”‚ โ”œโ”€โ”€ overlay.py <- OpenCV drawing of boxes + captions โ”‚ โ””โ”€โ”€ build_index.py <- CLI: python -m spareparts.build_index โ”œโ”€โ”€ tools/ โ”‚ โ””โ”€โ”€ review.py <- build-time QA: render every box, write overrides โ”œโ”€โ”€ data/ <- generated, git-ignored except overrides.json โ”‚ โ”œโ”€โ”€ catalog.db โ”‚ โ”œโ”€โ”€ pages/p26.png ... p55.png โ”‚ โ”œโ”€โ”€ ocr_cache/p26.json ... p55.json โ”‚ โ”œโ”€โ”€ build_report.json โ”‚ โ””โ”€โ”€ overrides.json <- committed; it is hand-authored input โ””โ”€โ”€ tests/ โ”œโ”€โ”€ test_table_parser.py โ”œโ”€โ”€ test_matcher.py โ”œโ”€โ”€ test_localizer.py โ”œโ”€โ”€ test_index_integrity.py โ””โ”€โ”€ fixtures/ โ”œโ”€โ”€ page26_expected.json โ”œโ”€โ”€ page34_expected.json โ””โ”€โ”€ gold_boxes.json <- manual ground truth for evaluation ``` Flat, one package, two entry points. No `src/` indirection, no plugin system, no service boundaries. --- ## 13. Dependencies `requirements.txt`: ``` pymupdf>=1.24 # PDF parsing, table extraction, image extraction rapidocr-onnxruntime>=1.3 # OCR (PP-OCRv4 via ONNX Runtime, CPU, no system deps) opencv-python-headless>=4.9 numpy>=1.26 pillow>=10 streamlit>=1.30 ``` Dev only: `pytest`. Notes: * **Python 3.11** (the interpreter present in this workspace; 3.11.9 verified). * `opencv-python-headless` rather than `opencv-python`: no GUI windows are ever opened, and headless avoids Qt/GTK baggage. * `scipy` is **not** required โ€” `cv2.connectedComponentsWithStats` covers it. * No `pandas`: the data volumes are trivial and SQLite is the store. * Everything runs **offline on CPU**. No network calls at build or run time. Total install footprint is a few hundred MB, dominated by the ONNX models and OpenCV. * `pymupdf` version note: `find_tables()` prints a suggestion to install `pymupdf-layout`. It is not needed here โ€” the plain extractor produced correct 4-column output on all 30 pages. --- ## 14. UI design Single-page Streamlit app, no navigation, no login. ``` +----------------------------------------------------------------------+ | BQ188 Spare-Parts Locator | | | | Part number [ 30642 v ] [ Find ] | | (autocomplete over 705 known numbers) | +----------------------------------------------------------------------+ | 30642 ยท Schnecke,kpl. | | EN worm,cpl. FR vis sans fin,cpl | | Section 07 โ€” Motor-Antrieb / Motor-drive PDF page 34 | | 1 instance on this page * located | +----------------------------------------------------------------------+ | [ page 34 ] <- tabs if part is on many | | | | +---------------------------------------------+ | | | | | | | +---------+ | | | | | #######<--- red box on the component | | | | +---------+ | | | | 30642 <-- caption drawn on the image | | | | +------+ | | | | |30642 |<-- blue box on the callout | | | | +------+ | | | +---------------------------------------------+ | | | | [ Download annotated PNG ] [ ] show label boxes [ ] show all | +----------------------------------------------------------------------+ ``` Details: * **Red rectangle**, 4โ€“6 px at native resolution, on the component. **Blue thin rectangle** on the printed callout label (toggleable, on by default) so the user can see *why* the tool chose that component. These two colours are never swapped. * The part number is drawn **on the image** next to the red box, on a white plate for legibility against line art. * **Confidence badge** next to the header, driven by `components.method`: `located` (leader) / `approximate` (anchor_radius, nearest) / `assembly (group)` / `label only` (no component box). * **Multi-page parts** (61 of them) โ†’ one Streamlit tab per page. * **Multi-instance parts** โ†’ every instance boxed and numbered `#1`, `#2`, โ€ฆ in the caption; a count is shown in the header. * **"Show all parts on this page"** checkbox renders every label on the page at once โ€” genuinely useful for browsing, and it doubles as the QA view. * Unknown input โ†’ "No part `xxxxx` in this catalogue" plus the nearest 5 numbers by edit distance. * Images are served from the PNG cache; drawing is a few OpenCV calls, so response is effectively instant. --- ## 15. Error and edge cases | # | Case | Evidence | Handling | |---|---|---|---| | E1 | Part appears on several pages | 61 parts; `73290` on 9 pages | Return all; one UI tab per page | | E2 | Part appears several times on one page | `74506` ร—4 on p34 | Index every instance; box all | | E3 | Part in the table but not drawn | p39: 7 of 47 | Show the page, box nothing, say "listed in table; not drawn on this diagram" | | E4 | Callout drawn but not in that page's table | p40 `73182`, `73636`, `74167` | Accept via global part list; flag in build report | | E5 | Callout prefixed `*` | `*30638`, `*74012` | Strip in normalization; store `has_asterisk`; show the footnote text | | E6 | Boxed assembly callout | `31269`, `33333`, `34983` | Tier C; badge "assembly (group)" | | E7 | Footnote rows in the table | p42, p51, p52 (`*`, `**`, `***`, blank) | Filtered out of `parts`; text kept for E5 display | | E8 | Real parts listed *after* the footnote rows | p52 `30343`โ€“`30346`, p51 `73013` | Filter per row, never "stop at first footnote" | | E9 | Malformed part number in source | p47 table `351159` vs diagram `35159` | Stored with `is_malformed`; excluded from OCR snapping | | E10 | Off-page / decoy images | p26 second image at y 802โ€“1576; p29, p54 negative y | Select by max intersection with `page.rect` | | E11 | Same image xref on two pages | xref 152 on p36 and p37 | Cache per page, never per xref | | E12 | OCR noise: `Seite 18`, `061101`, `D / PU-S-12 / โ€ฆ`, `07` | Every page | Reject by header/footer strip + 5-digit rule + part-list snap | | E13 | OCR char errors: `*7.4012`, `-74506`, `4639`, `7497` | p34, p27, p48 | `re.sub(r'\D','')` then exact/fuzzy-1 snap | | E14 | Leader line merges component with the frame | p34 border component = 64 % of image | Frame components (>25 % area) excluded; Tier B size cap | | E15 | Leader line crosses a big neighbour | p34 `74506`/`74903`/`79105` โ†’ housing | Line-likeness check; >8 % area triggers Tier B | | E16 | Wrong nearest component wins | p34 `74025` captured `76352`'s bracket | Line-likeness check; then override | | E17 | Callout with no leader line | p40 | Tier B "nearest" | | E18 | Dashed leader line | p40 `73158`, `71058` | Gap-tolerant walk in Tier A | | E19 | User searches a number in the manual pages (1โ€“24) | โ€” | Those pages are not indexed; report "not found" | | E20 | User enters non-numeric / wrong length | โ€” | Validate `^\d{5}$`; suggest nearest known numbers | | E21 | `catalog.db` missing or stale | โ€” | App checks for the DB on start and prints the exact build command | --- ## 16. Testing strategy `pytest`, run against the real `1.pdf` โ€” it is fixed and committed, so it *is* the fixture. **Unit** * `test_table_parser.py` โ€” pages 26, 34 and 52 parsed against committed golden JSON: exact row counts, exact part numbers, correct footnote filtering (52), correct handling of real rows after footnotes (52). Asserts the global totals: 30 pages, 861 rows, 705 distinct numbers. * `test_matcher.py` โ€” `*7.4012 โ†’ 74012`, `-74506 โ†’ 74506`, `Seite 18 โ†’ rejected`, `061101 โ†’ rejected`, `07 โ†’ rejected`; fuzzy-1 accepts a single candidate and rejects ambiguous ties. * `test_localizer.py` โ€” on synthetic images (a label, a leader line, a target square, a decoy square): the target is chosen, the decoy is not; the frame guard fires; the size cap triggers Tier B; a dashed line is still followed. * `test_pdf_loader.py` โ€” the diagram picked for pages 26, 29, 36, 37 and 54 is the on-page one, not a decoy or a shared xref (this pins E10 and E11). **Integration** * `test_index_integrity.py` โ€” over the built `catalog.db`: * every `labels.part_no` exists in `parts` * every `occurrences.part_no` exists in `parts` * every label's bbox lies inside its page's image bounds * every component box, when present, lies inside the image bounds and is smaller than the Tier-B cap * overall label recall stays โ‰ฅ 93 % and no page drops below 85 % โ€” a regression guard on OCR settings, anchored to the Phase-1 measurement of 94.9 % / worst 85 % **Regression** * `test_known_parts.py` โ€” a handful of hand-verified pins that must never break: `30642` โ†’ page 34, section 07, `Schnecke,kpl.` / `worm,cpl.`, component box overlapping the worm; `31269` โ†’ page 26, boxed assembly; `73290` โ†’ 9 pages. **Manual** * `tools/review.py` renders every page with all boxes drawn into `data/review/`, which is how localization is actually judged (section 17) and how `overrides.json` gets written. --- ## 17. Accuracy and evaluation strategy Three metrics, each measured against something real. **M1 โ€” Label detection recall (automatic, no manual work).** For each page: distinct part numbers OCR'd from the diagram รท part numbers in that page's table. **Already measured in Phase 1 over all 30 pages: 817 / 861 = 94.9 %, 9 pages at 100 %, worst page 85 % (page 39).** It over-counts misses, because some listed parts are genuinely not drawn, so it is a **lower bound**. Target: **โ‰ฅ 95 % overall, โ‰ฅ 85 % on every page** โ€” i.e. do not regress from the Phase-1 baseline. **M2 โ€” Label precision (automatic).** Fraction of accepted labels that snap exactly to a known part number, and the count of rejected tokens per page (Phase-1 baseline: 69 rejects over 30 pages โ‰ˆ 2.3/page, nearly all of them known page furniture). Target: **โ‰ฅ 98 % exact matches; < 3 rejected tokens/page.** **M3 โ€” Component localization accuracy (manual gold set).** Build a ground-truth set of **80 (page, part, instance) triples** across 8 pages chosen for varied difficulty โ€” 26 (dense hardware), 34 (clean), 39 and 40 (very dense), 45, 49, 51 (accessories), 55 (nearly empty) โ€” by hand-drawing the correct component box in `tests/fixtures/gold_boxes.json` using `tools/review.py`. Scored two ways: * **Strict:** IoU(predicted, gold) โ‰ฅ 0.5. * **Useful:** the predicted box's centre lies inside the gold box *and* the predicted area is โ‰ค 3ร— the gold area. This is the metric that matters โ€” a slightly loose box that unambiguously indicates the right part still answers the user's question. Phase-1 prototype baseline on page 34, judged by eye: **~80 % correct**, with the four failure modes catalogued in 4.9. Targets after Tier A/B are properly implemented: | Metric | Target | |---|---| | M3 useful, Tier A+B automatic | โ‰ฅ 85 % | | M3 strict (IoU โ‰ฅ 0.5), automatic | โ‰ฅ 70 % | | After `overrides.json` review | โ‰ฅ 97 % useful | **Honesty requirement:** the build report records the per-method distribution (`leader` / `anchor_radius` / `nearest` / `assembly` / `override` / `none`), and the UI shows the method for the box the user is looking at. The tool never presents a Tier-B guess as if it were a Tier-A result. --- ## 18. Implementation phases | Phase | Deliverable | Definition of done | |---|---|---| | **1 (done)** | **This PSD** | PDF inspected, structure documented, OCR and localization prototyped and measured | | **2** | Index skeleton: `config`, `pdf_loader`, `table_parser`, `index_db`, `build_index` | `catalog.db` holds 30 pages, 705 parts, 861 occurrences; 30 PNGs cached; `test_table_parser` and `test_pdf_loader` pass | | **3** | `ocr.py` + `matcher.py` + OCR cache | `labels` populated for all 30 pages; M1 โ‰ฅ 95 %; build report written | | **4** | `localizer.py` Tiers Aโ€“C | `components` populated; per-method counts in the report; no box exceeds the size cap | | **5** | `tools/review.py`, gold set, `overrides.json` | 80-box gold set exists; M3 measured; overrides written for the residual failures | | **6** | `app.py` + `overlay.py` + `lookup.py` | `30642` โ†’ page 34 with a red box on the worm; multi-page and multi-instance paths work | | **7** | Polish: README, autocomplete, download, "show all", error states | Fresh clone โ†’ `pip install -r requirements.txt` โ†’ `python -m spareparts.build_index` โ†’ `streamlit run app.py` works end to end | Phases 2โ€“4 are the substance; 5 is what turns "mostly works" into "trustworthy"; 6โ€“7 are a day's work each. --- ## 19. Explicitly excluded technologies and features Excluded by requirement, and none of them would help here โ€” the catalogue is a fixed, fully enumerable 705-row lookup table. * **RAG in any form** โ€” no retrieval-augmented generation. * **LangChain / LlamaIndex** or any agent framework. * **Embeddings, vector databases, semantic or fuzzy "meaning" search** (FAISS, Chroma, Pinecone, pgvector, โ€ฆ). Lookup is an exact 5-digit key. * **LLMs anywhere in the pipeline**, including for OCR correction or description cleanup. * **Cloud services** โ€” no OCR APIs, no storage buckets, no hosted inference. The tool runs offline. * **Docker / Kubernetes / CI infrastructure.** Single developer, single machine, one `pip install`. There is no deployment target that would justify it. * **Microservices, REST APIs, message queues, background workers.** The build is a script; the app is a Streamlit process. * **Training or fine-tuning any model.** No annotated data exists, and a fixed 30-page document does not warrant creating it. * **A general-purpose PDF catalogue engine.** This is scoped to `1.pdf` and its exact layout. Generalization is explicitly a non-goal; the code should read as document-specific, with the document's quirks named in `config.py`. * **User accounts, multi-tenancy, telemetry, persistence of user state.** * **PostgreSQL / MongoDB.** SQLite holds ~1600 rows. * **Indexing pages 1โ€“24** (title, operation manual, needle table). They contain no part numbers. They may optionally be *viewable*, never searchable. --- ## 20. Known uncertainties and risks | # | Risk | Severity | Evidence / mitigation | |---|---|---|---| | **R1** | **Leader-line tracing is less reliable than the page-34 sample suggests.** Denser pages (39, 40, 51 โ€” up to 10.6 % ink, 61 parts) have crossing lines and touching parts; the "line touches only its target" property may break down. | **High โ€” this is the project's central risk** | Measured on one page only (~80 %). Mitigation: Tier B size cap keeps failures *conservative* rather than wrong-and-confident; Tier D overrides; the UI always shows the label box so the user can self-correct. **Validate on pages 39/40/51 first in Phase 4, before investing in Tier A refinements.** | | R2 | Component boxes are systematically too large where the leader line crosses a big outline | Medium | Observed on p34 (`74506`, `74903`, `79105`). Line-likeness check in Tier A; 8 % area cap โ†’ Tier B | | R3 | Assembly (`kpl.`) callouts have no single correct box | Medium | ~9 seen on p26/p34/p40. Tier C draws a group box and the UI says "assembly"; accepted as inherently approximate | | R4 | The ~15 % residual needs manual review; a fresh reviewer may find it tedious | Medium | 861 occurrences total; the review tool renders a page at a time. Budget ~3โ€“4 hours in Phase 5 | | R5 | OCR misses on the densest pages | Low | Measured 94.9 % overall, worst page 39 at 40/47, and several of those 7 are genuinely undrawn parts | | R6 | RapidOCR model download on first run | Low | Models ship with the wheel; verified โ€” the Phase-1 run needed no network. Pin the version in `requirements.txt` | | R7 | Binarization threshold 150 is tuned on one page | Low | JPEG artifacts vary slightly. Verified ink fraction 2.2โ€“10.6 % across all 30 pages, all bilevel line art. Make the threshold a `config.py` constant and sanity-check the resulting component count per page in the build report | | R8 | Part numbers appearing on up to 9 pages could confuse a user | Low | UI shows one tab per page with the section title, so the user picks by assembly | | R9 | Source-data defects (`351159`, `lsolierplatte`, `Kreuzsohnur`) | Low | Displayed as-is; flagged, never silently "corrected". Descriptions are never used for matching | | R10 | `pymupdf.find_tables()` behaviour could change across versions | Low | Pin `pymupdf`; `test_table_parser` asserts exact golden output on 3 pages and the global 861/705 totals, so a regression fails loudly | | R11 | Build time (~10 min) is annoying during development | Low | OCR results cached to `data/ocr_cache/`; localization can be re-run alone. Stages are independently invocable | ### Open questions to settle during implementation 1. **Which Tier-A line-following implementation wins** โ€” pixel walk vs. probabilistic Hough restricted to a window around the label. Decide empirically on pages 39/40/51 in Phase 4; do not commit to either now. 2. **The right Tier-B fallback box size.** "Median component bbox on the page" is a guess; tune it against the gold set. 3. **Whether boxed-assembly callouts should draw a union box or simply highlight the whole diagram region** with an explanatory caption. Decide after seeing Tier C on real pages. 4. **Whether to expose the operation manual (pages 2โ€“23) as browsable images** in the UI. Nice-to-have, zero indexing cost, and out of scope until Phase 7.
# Project Specification Document (PSD)
**Project:** Spare-Parts Diagram Locator for the BQ188 Warp Tying Machine catalogue
**Source document:** `1.pdf` (project root)
**Status:** Phase 1 โ€” specification complete, implementation not started
**Last updated:** 2026-08-30
> This document is the permanent source of truth for the project. Every statement in
> section 4 ("PDF / document structure discovered") was verified programmatically
> against `1.pdf`. Nothing in that section is assumed.
---
## 1. Project overview
A small, entirely local desktop tool. The user types a machine spare-part number
(e.g. `30642`). The tool finds the catalogue page that contains that part, renders
the exploded-view diagram from that page, and draws a **red rectangle around the
physical component** the number refers to โ€” not merely around the printed number โ€”
with the part number and name shown next to it.
The catalogue is a fixed, known, 55-page PDF. The document never changes at runtime.
Therefore all expensive work (text extraction, OCR, geometry) happens **once, offline,
in a build step**. The interactive application only performs a dictionary lookup and
draws an overlay. Lookup is deterministic and instant.
This is a **document-processing / computer-vision** project, not a retrieval or
question-answering project. See section 19 for what is explicitly out of scope.
---
## 2. Exact problem being solved
Given a 5-digit part number `P`:
| Step | Requirement |
|---|---|
| R1 | Determine every catalogue page on which `P` appears. |
| R2 | Retrieve the part's name (German / English / French) and the section it belongs to. |
| R3 | Render that page's exploded-view diagram as an image. |
| R4 | Find every callout label reading `P` inside that diagram. |
| R5 | For each label, resolve the **component** the label points to, and produce a bounding box around that component's drawn geometry. |
| R6 | Draw a red rectangle on the component, and print `P` (plus name) next to it. |
| R7 | Report page number, section number, section title, and part name. |
**The hard requirement is R5.** R4 (finding the printed text) is easy and already
proven. R5 requires following the drawing's leader line from the label to the part.
### Why this is not a text-search problem
The number `30642` appears twice on page 34: once as a row in the machine-readable
parts table on the left half of the page, and once as a raster callout inside the
diagram on the right half. Boxing either of those occurrences boxes *text*. The
component itself โ€” a worm gear (`Schnecke`) โ€” carries no text at all. It is
identified only by a thin line drawn from the callout to the part outline.
---
## 3. User workflow
```
1. User launches the app โ†’ streamlit run app.py
2. User types a part number โ†’ "30642"
3. App validates / autocompletes โ†’ suggests from 705 known numbers
4. App shows result header โ†’ 30642 ยท Schnecke,kpl. / worm,cpl. / vis sans fin,cpl
Section 07 "Motor-Antrieb / Motor-drive" ยท PDF page 34
5. App shows the diagram โ†’ full-page diagram image
ยท RED rectangle on the physical component
ยท part number drawn next to the rectangle
ยท thin blue rectangle on the printed callout label (so the user can verify)
ยท confidence badge: "located" / "approximate" / "label only"
6. If the part appears on several pages, a page selector appears (61 parts do).
7. If the part has several instances on one page, all instances are boxed.
8. User can zoom / download the annotated PNG.
```
Failure is always visible, never silent: if the component cannot be resolved, the
app still shows the page and boxes the label, and says so.
---
## 4. PDF / document structure discovered
All facts below were extracted from `1.pdf` with PyMuPDF 1.28.2.
### 4.1 File-level
| Property | Value |
|---|---|
| Pages | 55 |
| Page size | 1190 ร— 842 pt (A4 landscape), rotation 0, uniform on every page |
| PDF version | 1.7 |
| Producer / Creator | Microsoftยฎ Excelยฎ 2019 |
| Created | 2022-08-21 |
| Encrypted | No |
| Bookmarks / outline | None (0 entries) |
| File size | ~39.8 MB |
| Machine | BQ188 Warp Tying Machine โ€” Jingong Industrial Co., Ltd |
The PDF was exported from an Excel workbook. This matters: the parts tables are