ZainabEman's picture
Upload 152 files
546c207 verified
Raw History Blame Contribute Delete
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")
@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"<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()