"""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") @st.cache_resource 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) @st.cache_data(show_spinner=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"{occurrence.section_de or ''}", 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("
", 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()