Spaces:
Configuration error
Configuration error
File size: 7,849 Bytes
546c207 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | """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()
|