Spaces:
Configuration error
Configuration error
Download app.py from ZainabEman/OCR-Based-Spare-Part-Localization: direct link, hf CLI and curl.
- Browser
- Download file 7.85 kB
-
https://huggingface.co/spaces/ZainabEman/OCR-Based-Spare-Part-Localization/resolve/main/app.py
- Command line
-
hf download hf://spaces/ZainabEman/OCR-Based-Spare-Part-Localization/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ZainabEman/OCR-Based-Spare-Part-Localization/resolve/main/app.py
7.85 kB
| """Machine Part Finder -- Streamlit front end for the BQ188 spare-parts catalogue. | |
| streamlit run app.py | |
| Every answer comes from `data/catalog.db` and the cached diagrams that the | |
| offline build produced. Nothing here runs OCR, computer vision or PDF parsing: | |
| a search is a SQLite read plus a few rectangles drawn on a copy of a PNG. | |
| """ | |
| from __future__ import annotations | |
| import streamlit as st | |
| from spareparts import config, index_db, lookup, overlay | |
| st.set_page_config(page_title="Machine Part Finder", page_icon=":mag:", layout="wide") | |
| def _connection(): | |
| # Streamlit reruns the script on a different thread each time, so the | |
| # cached connection must not be pinned to the thread that opened it. | |
| return index_db.connect(config.DB_PATH, same_thread=False) | |
| def _annotated_png(png_path: str, boxes: tuple, part_no: str, show_callout: bool) -> bytes: | |
| """Render and cache one annotated diagram. | |
| Keyed on plain values so Streamlit can cache it; the boxes come straight | |
| from the database. | |
| """ | |
| from spareparts.lookup import Highlight | |
| highlights = [ | |
| Highlight( | |
| instance=i, | |
| label_box=tuple(label), | |
| component_box=tuple(component) if component else None, | |
| method="", confidence=0.0, note="", | |
| ) | |
| for i, (label, component) in enumerate(boxes) | |
| ] | |
| image = overlay.load_diagram(png_path) | |
| return overlay.encode_png(overlay.annotate(image, highlights, part_no, show_callout)) | |
| def _render_occurrence(part, occurrence, show_callout: bool) -> None: | |
| """Show one catalogue page: its metadata, then the annotated diagram.""" | |
| left, middle, right = st.columns([2, 2, 3]) | |
| left.metric("PDF page", occurrence.pdf_page) | |
| middle.metric("Catalogue section", occurrence.section_code or "-") | |
| right.markdown( | |
| f"**{occurrence.section_en or '-'}** \n" | |
| f"<span style='color:#666'>{occurrence.section_de or ''}</span>", | |
| unsafe_allow_html=True, | |
| ) | |
| if not occurrence.listed_in_table: | |
| st.info( | |
| f"Part {part.part_no} is drawn on this diagram but is not listed in this " | |
| "page's own parts table. The callout was matched against the full " | |
| "catalogue, so it is genuine." | |
| ) | |
| if not occurrence.is_drawn: | |
| st.warning( | |
| f"Part {part.part_no} is listed in this page's parts table but no callout " | |
| "for it was found on the diagram. The catalogue does not draw every " | |
| "listed part. The page is shown below without a highlight." | |
| ) | |
| st.image( | |
| overlay.to_rgb(overlay.load_diagram(occurrence.png_path)), | |
| width="stretch", | |
| caption=f"PDF page {occurrence.pdf_page} - no highlight available", | |
| ) | |
| return | |
| # Localization quality, stated plainly rather than implied. | |
| for position, highlight in enumerate(occurrence.highlights, start=1): | |
| prefix = f"#{position} " if len(occurrence.highlights) > 1 else "" | |
| if highlight.component_box is None: | |
| st.warning( | |
| f"{prefix}Callout found, but no component could be resolved. " | |
| f"{highlight.explanation}" | |
| ) | |
| elif highlight.method in ("leader", "override"): | |
| st.caption( | |
| f"{prefix}**{highlight.status}** - {highlight.explanation} " | |
| f"Confidence {highlight.confidence:.2f}." | |
| ) | |
| else: | |
| note = f" ({highlight.note})" if highlight.note else "" | |
| st.caption( | |
| f":orange[{prefix}**{highlight.status}**] - {highlight.explanation}" | |
| f"{note} Confidence {highlight.confidence:.2f}." | |
| ) | |
| if len(occurrence.highlights) > 1: | |
| st.caption( | |
| f"This part is drawn {len(occurrence.highlights)} times on this page; " | |
| "each instance is boxed and numbered." | |
| ) | |
| boxes = tuple( | |
| (h.label_box, h.component_box) for h in occurrence.highlights | |
| ) | |
| png = _annotated_png(str(occurrence.png_path), boxes, part.part_no, show_callout) | |
| st.image(png, width="stretch") | |
| st.download_button( | |
| "Download annotated diagram", | |
| data=png, | |
| file_name=f"{part.part_no}_page{occurrence.pdf_page}.png", | |
| mime="image/png", | |
| key=f"dl_{part.part_no}_{occurrence.pdf_page}", | |
| ) | |
| def main() -> None: | |
| st.title("Machine Part Finder") | |
| st.write( | |
| "Search the **BQ188 warp tying machine** spare-parts catalogue by part " | |
| "number. The matching exploded-view diagram is shown with the actual " | |
| "component outlined in red." | |
| ) | |
| ready, problem = lookup.index_is_ready(config.DB_PATH) | |
| if not ready: | |
| st.error(problem) | |
| st.code("python -m spareparts.build_index", language="bash") | |
| st.caption("Run the offline build once, then reload this page.") | |
| return | |
| conn = _connection() | |
| stats = lookup.catalogue_stats(conn) | |
| with st.form("search"): | |
| columns = st.columns([4, 1]) | |
| query = columns[0].text_input( | |
| "Part number", | |
| placeholder="e.g. 30642", | |
| help=f"{stats['parts']} part numbers across {stats['pages']} catalogue pages.", | |
| ) | |
| columns[1].markdown("<div style='height:1.85rem'></div>", unsafe_allow_html=True) | |
| submitted = columns[1].form_submit_button("Search", width="stretch") | |
| show_callout = st.sidebar.checkbox( | |
| "Show the printed callout too", | |
| value=True, | |
| help="Outlines the part number printed on the diagram in blue, so you can " | |
| "see which callout the red box was derived from.", | |
| ) | |
| st.sidebar.caption( | |
| f"Catalogue: {stats['parts']} parts, {stats['pages']} diagram pages, " | |
| f"{stats['callouts']} located callouts." | |
| ) | |
| if not (submitted or query): | |
| st.caption("Enter a part number above and press Enter or click Search.") | |
| return | |
| part_no = lookup.normalize_query(query) | |
| if not part_no: | |
| st.warning("Please enter a part number, for example 30642.") | |
| return | |
| part = lookup.find_part(conn, part_no) | |
| if part is None: | |
| st.error(f"Part number {part_no} not found in the catalogue.") | |
| near = lookup.suggest(conn, part_no) | |
| if near: | |
| st.caption("Did you mean: " + ", ".join(f"`{n}`" for n in near) + "?") | |
| return | |
| if part_no != (query or "").strip(): | |
| st.caption(f"Searched for `{part_no}`.") | |
| st.subheader(f"{part.part_no} - {part.display_name}") | |
| st.write( | |
| f"**German** {part.name_de or '-'} | **French** {part.name_fr or '-'}" | |
| ) | |
| if part.is_malformed: | |
| st.warning( | |
| "This part number is printed with an unusual number of digits in the " | |
| "source catalogue and may be a typo in the original document." | |
| ) | |
| occurrences = part.occurrences | |
| if not occurrences: | |
| st.error("This part is in the catalogue index but no page references it.") | |
| return | |
| if len(occurrences) == 1: | |
| _render_occurrence(part, occurrences[0], show_callout) | |
| return | |
| st.info( | |
| f"This part appears on {len(occurrences)} catalogue pages. " | |
| "Choose which one to view." | |
| ) | |
| options = list(range(len(occurrences))) | |
| chosen = st.radio( | |
| "Occurrence", | |
| options, | |
| format_func=lambda i: occurrences[i].describe(i + 1), | |
| horizontal=False, | |
| ) | |
| st.divider() | |
| _render_occurrence(part, occurrences[chosen], show_callout) | |
| if __name__ == "__main__": | |
| main() | |