Spaces:
Sleeping
Sleeping
| # GoodFind β OCR (+ optional ML) with graceful fallback | |
| import streamlit as st, base64, shutil | |
| # Optional ML (if you added valuation.py earlier) | |
| try: | |
| from valuation import DealValuator, deal_score | |
| HAS_ML = True | |
| except Exception: | |
| HAS_ML = False | |
| from ocr_utils import ocr_image, guess_price, guess_title, annotate_price_box | |
| st.set_page_config(page_title="GoodFind (OCR Demo)", page_icon="π") | |
| st.title("π GoodFind β OCR Demo") | |
| # Check if the tesseract binary exists (fallback-safe) | |
| HAS_TESSERACT = shutil.which("tesseract") is not None | |
| status = [] | |
| status.append("OCR engine: β available" if HAS_TESSERACT else "OCR engine: β missing (fallback to manual)") | |
| status.append("ML estimator: β " if HAS_ML else "ML estimator: β (using keyword fallback)") | |
| st.caption(" Β· ".join(status)) | |
| # If ML is present, initialize once | |
| if HAS_ML and 'valuator' not in st.session_state: | |
| st.session_state['valuator'] = DealValuator() | |
| valuator = st.session_state.get('valuator') | |
| # Simple fallback estimator (if no ML) | |
| PRICE_TABLE = {"wii":85,"playstation":120,"ps2":75,"ps3":110,"ps4":160,"ps5":350,"iphone":240,"macbook":450,"aeron":500, | |
| "pyrex":120,"le creuset":240,"kitchenaid":220,"bose":140,"walkman":95,"marantz":400,"yeti":18,"dansko":40, | |
| "coach":65,"levi":28,"seiko":95,"all-clad":55} | |
| DEFAULT_RESALE = 45.0 | |
| def quick_estimate(title: str) -> float: | |
| t = (title or "").lower() | |
| best = None | |
| for k, v in PRICE_TABLE.items(): | |
| if k in t: | |
| best = max(best or 0, v) | |
| return float(best if best else DEFAULT_RESALE) | |
| def deal_score_simple(predicted_resale: float, ask: float, fee_rate: float=0.13, ship: float=12.0): | |
| fees = predicted_resale * fee_rate | |
| net = predicted_resale - fees - ship | |
| profit = net - ask | |
| margin = (profit / ask) if ask > 0 else 0.0 | |
| if profit >= 50 and margin >= 0.8: label = "Home Run" | |
| elif profit >= 25 and margin >= 0.5: label = "Great" | |
| elif profit >= 10 and margin >= 0.3: label = "Good" | |
| elif profit >= 5: label = "Meh" | |
| else: label = "Pass" | |
| return round(fees,2), round(net,2), round(profit,2), round(margin,2), label | |
| # UI controls | |
| fees_rate = st.slider("Fees rate", 0.05, 0.20, 0.13, 0.01) | |
| ship = st.slider("Shipping estimate ($)", 0.0, 30.0, 12.0, 1.0) | |
| autoscan = st.checkbox("Auto-scan photos with OCR (read price tags + suggest names)", value=True) | |
| photos = st.file_uploader("Upload photos (JPG/PNG/WebP)", type=["jpg","jpeg","png","webp"], accept_multiple_files=True) | |
| items = [] | |
| if photos: | |
| for i, img in enumerate(photos): | |
| raw = img.read() | |
| suggested_title, suggested_price, annotated = "", "", None | |
| if autoscan and HAS_TESSERACT: | |
| text, tokens, _ = ocr_image(raw) | |
| suggested_title = guess_title(text) or "" | |
| gp = guess_price(tokens) | |
| if gp: | |
| val, box = gp | |
| suggested_price = f"{val:.2f}" | |
| try: | |
| annotated = annotate_price_box(raw, box, label=f"${val:.2f}") | |
| except Exception: | |
| annotated = None | |
| elif autoscan and not HAS_TESSERACT: | |
| st.info("OCR isnβt available in this runtime; you can still type the name and price manually.") | |
| with st.expander(f"Photo {i+1}", expanded=True): | |
| st.image(annotated or raw, use_container_width=True) | |
| c1, c2 = st.columns([2,1]) | |
| with c1: | |
| name = st.text_input(f"Item name #{i+1}", value=suggested_title, placeholder="e.g., Bose QC35 headphones", key=f"name_{i}") | |
| with c2: | |
| price_txt = st.text_input(f"Asking price #{i+1} ($)", value=suggested_price, placeholder="e.g., 25.00", key=f"price_{i}") | |
| items.append((name, price_txt, raw, img.type or "image/jpeg")) | |
| if st.button("Evaluate group", type="primary"): | |
| rows, gallery_blocks = [], [] | |
| for idx, (name, price_txt, raw, mime) in enumerate(items): | |
| name = (name or "").strip() or f"Item {idx+1}" | |
| try: | |
| ask = float(price_txt) if price_txt and price_txt.strip() else 0.0 | |
| except: | |
| ask = 0.0 | |
| if HAS_ML and valuator: | |
| resale = valuator.predict_resale(name) | |
| fees_v, net, profit, margin, label = deal_score(resale, ask, fees_rate, ship) | |
| else: | |
| resale = quick_estimate(name) | |
| fees_v, net, profit, margin, label = deal_score_simple(resale, ask, fees_rate, ship) | |
| rows.append({"title": name, "ask": ask, "resale": resale, "profit": profit, "margin": margin, "label": label}) | |
| b64 = base64.b64encode(raw).decode("utf-8") | |
| gallery_blocks.append(f""" | |
| <div class="img-wrap" title="{name}"> | |
| <img src="data:{mime};base64,{b64}" alt="{name}"/> | |
| <span class="hover-label">{name}</span> | |
| </div> | |
| """) | |
| if rows: | |
| rows.sort(key=lambda r: (r["profit"], r["resale"]), reverse=True) | |
| st.markdown("### Ranked results") | |
| st.table([ | |
| {"Title": r["title"], "Ask ($)": f"{r['ask']:.2f}", | |
| "Est. resale ($)": f"{r['resale']:.2f}", "Profit ($)": f"{r['profit']:.2f}", | |
| "Margin": f"{r['margin']:.2f}", "Verdict": r["label"]} | |
| for r in rows | |
| ]) | |
| top = rows[0] | |
| st.success(f"Top pick: **{top['title']}** β resale ${top['resale']:.0f}, profit ${top['profit']:.0f} ({top['label']})") | |
| st.markdown("### Hover over each image to see the item name") | |
| st.markdown("".join(gallery_blocks), unsafe_allow_html=True) | |