snipebargain / src /streamlit_app.py
theDavidGuy's picture
Update src/streamlit_app.py
6aa907d verified
# 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)