Snipe / ps
theDavidGuy's picture
import streamlit as st from valuation import DealValuator, compute_deal_score import pandas as pd import os, base64 from datetime import datetime DATA_DIR = "data" FEEDBACK_CSV = os.path.join(DATA_DIR, "feedback.csv") st.set_page_config(page_title="GoodFind: Thrift Deal Finder", page_icon="πŸ›’") st.title("πŸ›’ GoodFind β€” Thrift & Yard Sale Deal Checker") with st.expander("How it works"): st.markdown(""" - A lightweight ML model predicts **resale value** from item title (plus optional brand/category/condition). - I estimate marketplace fees and shipping to compute **profit** and a **deal label**. - **Group scan** lets you review many items at once (with photos). If a price isn't visible, **enter it manually**. - **Auto-scan with OCR**: tries to read **price tags** and **names/brands** from your photo and pre-fill fields. - **Hover labels**: after evaluation, the item name appears on hover over the photo. - **πŸ‘/πŸ‘Ž feedback** trains the estimator over time (nudges predictions Β±10% for similar items). """) if 'valuator' not in st.session_state: os.makedirs(DATA_DIR, exist_ok=True) if not os.path.exists(FEEDBACK_CSV): pd.DataFrame(columns=["title","correct","ts"]).to_csv(FEEDBACK_CSV, index=False) st.session_state['valuator'] = DealValuator(os.path.join(DATA_DIR,'comps.csv'), feedback_csv=FEEDBACK_CSV) valuator = st.session_state['valuator'] # CSS for overlays st.markdown(''' <style> .img-wrap{position:relative; display:inline-block; margin:8px; max-width:100%;} .img-wrap img{display:block; max-width:100%; height:auto; border-radius:6px; border:1px solid rgba(0,0,0,0.1);} .hover-label{ position:absolute; left:8px; top:8px; padding:4px 8px; border-radius:4px; background:rgba(0,0,0,0.65); color:#fff; font-size:0.9rem; opacity:0; transition:opacity .15s; pointer-events:none; } .img-wrap:hover .hover-label{opacity:1;} </style> ''', unsafe_allow_html=True) tab1, tab2, tab3 = st.tabs(["Single check", "Batch (CSV)", "Group scan (photos + OCR)"]) # -------- Single -------- with tab1: st.subheader("Check a deal") title = st.text_input("Listing title", placeholder="e.g., Nintendo Wii bundle with games") col1, col2, col3 = st.columns(3) with col1: asking = st.number_input("Asking price ($)", min_value=0.0, step=1.0, value=10.0) with col2: condition = st.selectbox("Condition", ["Used-Good","Used-Fair","Used-Excellent","New-Open Box","New"]) with col3: fees_rate = st.slider("Fees rate", min_value=0.05, max_value=0.20, value=0.13, step=0.01) extra = st.expander("Optional details") with extra: category = st.text_input("Category (optional)", placeholder="Electronics") brand = st.text_input("Brand (optional)", placeholder="Nintendo") if st.button("Evaluate Deal", type="primary") and title.strip(): res = valuator.predict_resale(title=title, category=category or None, brand=brand or None, condition=condition) predicted_resale = res['predicted_resale'] score = compute_deal_score(predicted_resale, asking_price=asking, fees_rate=fees_rate) st.markdown(f"### Estimated resale: **${predicted_resale:.2f}**") st.markdown(f"**Deal verdict:** {score['label']}") st.progress(min(1.0, max(0.0, (score['margin']+1)/2))) st.markdown("**Breakdown**") st.write(pd.DataFrame([{ 'Category': res['category'] or 'β€”', 'Brand': res['brand'] or 'β€”', 'Condition': res['condition'], 'Fees ($)': score['fees'], 'Net after fees+ship ($)': score['net_after_fees'], 'Profit ($)': score['profit'], 'Margin on ask': score['margin'] }])) # -------- Batch CSV -------- with tab2: st.subheader("Batch evaluate via CSV") st.caption("Upload a CSV with columns: title, asking_price, [category], [brand], [condition].") fees_rate_batch = st.slider("Fees rate (apply to all)", min_value=0.05, max_value=0.20, value=0.13, step=0.01, key="fees_batch") file = st.file_uploader("CSV file", type=['csv'], key="csv_uploader") if file is not None: df = pd.read_csv(file) required = {'title','asking_price'} if not required.issubset(set(df.columns)): st.error("CSV must include at least 'title' and 'asking_price' columns.") else: vals = [] for _, row in df.iterrows(): r = valuator.predict_resale( title=row['title'], category=row.get('category', None), brand=row.get('brand', None), condition=row.get('condition', 'Used-Good') ) s = compute_deal_score(r['predicted_resale'], row['asking_price'], fees_rate=fees_rate_batch) vals.append({**row.to_dict(), **r, **s}) out = pd.DataFrame(vals).sort_values(by=['profit','predicted_resale'], ascending=[False, False]) st.dataframe(out, use_container_width=True) st.download_button("Download results CSV", data=out.to_csv(index=False), file_name="goodfind_results.csv", mime="text/csv") top = out.iloc[0] st.success(f"Top pick: **{top['title']}** β€” est. resale ${top['predicted_resale']:.0f}, profit ${top['profit']:.0f} (label: {top['label']})") # -------- Group scan with OCR -------- with tab3: st.subheader("Group scan: photos + OCR (auto price & name)") st.caption("Upload photos, let OCR prefill titles and prices (you can edit), then evaluate and rank.") fees_rate_group = st.slider("Fees rate (apply to all)", min_value=0.05, max_value=0.20, value=0.13, step=0.01, key="fees_group") from ocr_utils import ocr_image, guess_price, annotate_with_box from detect_utils import load_brands, guess_title_from_text brands_map = load_brands(os.path.join(DATA_DIR, "brands.json")) photos = st.file_uploader("Upload photos (multiple allowed)", type=['jpg','jpeg','png','webp'], accept_multiple_files=True, key="photo_uploader") photo_meta = [] # (title, price_text, cond, bytes, mime, suggested_title) autoscan = st.checkbox("Auto-scan with OCR (read tags & names)", value=True) if photos: for i, img in enumerate(photos): b = img.read() mime = "image/" + (img.type.split('/')[-1] if img.type else 'jpeg') suggested_title = "" suggested_price = "" annotated = None if autoscan: full, tokens, _ = ocr_image(b) suggested_title = guess_title_from_text(full, brands_map) if full else "" gp = guess_price(tokens) if gp: val, box = gp suggested_price = f"{val:.2f}" try: annotated = annotate_with_box(b, box, label=f"${val:.2f}") except Exception: annotated = None with st.expander(f"Photo {i+1}"): st.image(annotated or b, use_column_width=True) c1, c2, c3 = st.columns([2,1,1]) with c1: t = st.text_input(f"Item title #{i+1}", value=suggested_title, placeholder="e.g., Bose QC35 headphones", key=f"title_{i}") with c2: price = st.text_input(f"Asking price #{i+1} ($)", value=suggested_price, placeholder="e.g., 25 or 25.00", key=f"price_{i}") with c3: cond = st.selectbox(f"Condition #{i+1}", ["Used-Good","Used-Fair","Used-Excellent","New-Open Box","New"], key=f"cond_{i}") photo_meta.append((t, price, cond, b, mime, suggested_title)) if st.button("Evaluate group", type="primary"): rows = [] gallery_blocks = [] for idx, (t, price, cond, b, mime, suggested_title) in enumerate(photo_meta): if not str(t).strip(): t = suggested_title or f"Item {idx+1}" try: ask = float(price) if str(price).strip() else 0.0 except: ask = 0.0 r = valuator.predict_resale(title=t, condition=cond) s = compute_deal_score(r['predicted_resale'], ask, fees_rate=fees_rate_group) rows.append({'idx': idx, 'title': t, 'asking_price': ask, **r, **s}) b64 = base64.b64encode(b).decode('utf-8') html = f''' <div class="img-wrap" title="{t}"> <img src="data:{mime};base64,{b64}" alt="{t}"/> <span class="hover-label">{t}</span> </div> ''' gallery_blocks.append(html) if rows: out = pd.DataFrame(rows).sort_values(by=['profit','predicted_resale'], ascending=[False, False]).reset_index(drop=True) st.markdown("### Ranked results") st.dataframe(out[['title','asking_price','predicted_resale','profit','margin','label','category','brand','condition']], use_container_width=True) top = out.iloc[0] st.success(f"Top pick: **{top['title']}** β€” est. resale ${top['predicted_resale']:.0f}, profit ${top['profit']:.0f} (label: {top['label']})") st.markdown("### Hover over each image to see the identified name") st.markdown("".join(gallery_blocks), unsafe_allow_html=True) st.markdown("#### Feedback") st.caption("Mark whether the auto-identification/valuation felt right. This helps future scans.") for i, row in out.iterrows(): c1, c2, c3 = st.columns([6,1,1]) with c1: st.write(f"**{row['title']}** β€” est. ${row['predicted_resale']:.0f}, profit ${row['profit']:.0f}") with c2: if st.button("πŸ‘", key=f"fb_up_{i}"): _append_feedback(FEEDBACK_CSV, row['title'], True) st.toast(f"Thanks! Logged πŸ‘ for '{row['title']}'.") with c3: if st.button("πŸ‘Ž", key=f"fb_dn_{i}"): _append_feedback(FEEDBACK_CSV, row['title'], False) st.toast(f"Logged πŸ‘Ž for '{row['title']}'.") def _append_feedback(path: str, title: str, correct: bool): os.makedirs(os.path.dirname(path), exist_ok=True) new = pd.DataFrame([{'title': title, 'correct': 1 if correct else 0, 'ts': datetime.utcnow().isoformat()}]) if os.path.exists(path): new.to_csv(path, mode='a', header=False, index=False) else: new.to_csv(path, index=False)
1ba2be8 verified