Spaces:
Sleeping
Sleeping
try fixes again
Browse files
app.py
CHANGED
|
@@ -19,7 +19,6 @@ with open("items.json", "r", encoding="utf-8") as f:
|
|
| 19 |
items_data = json.load(f)["items"]
|
| 20 |
|
| 21 |
ITEM_TO_TYPE = {v["name"]: v["type"].lower() for v in items_data.values() if "name" in v and "type" in v}
|
| 22 |
-
ALL_ITEMS = list(ITEM_TO_TYPE.keys())
|
| 23 |
ALL_CATEGORIES = sorted(set(ITEM_TO_TYPE.values()))
|
| 24 |
ITEM_FILE_MTIME = os.path.getmtime("items.json")
|
| 25 |
|
|
@@ -67,7 +66,6 @@ def save_cached_embeddings(aliases, embeds):
|
|
| 67 |
print(f"β οΈ Cache save failed: {e}")
|
| 68 |
|
| 69 |
def auto_alias_categories(embedder, all_categories, all_item_names, top_k=6, threshold=0.38):
|
| 70 |
-
"""Builds semantic aliases for each category by finding similar item names."""
|
| 71 |
print("π€ Building category aliases dynamically...")
|
| 72 |
cat_embs = {c: embedder.encode(c, convert_to_tensor=True) for c in all_categories}
|
| 73 |
item_embs = {i: embedder.encode(i, convert_to_tensor=True) for i in all_item_names}
|
|
@@ -120,12 +118,13 @@ def normalize_country_query(q: str) -> str | None:
|
|
| 120 |
return q.upper()
|
| 121 |
return None
|
| 122 |
|
|
|
|
| 123 |
def parse_freeform_query(text: str):
|
| 124 |
"""Supports 'plushies in uk' and 'uk plushies'."""
|
| 125 |
if not text:
|
| 126 |
return "", ""
|
| 127 |
text = text.strip().lower()
|
| 128 |
-
m = re.match(r"(.+?)
|
| 129 |
if m:
|
| 130 |
return m.group(1).strip(), m.group(2).strip()
|
| 131 |
parts = text.split()
|
|
@@ -137,23 +136,29 @@ def parse_freeform_query(text: str):
|
|
| 137 |
return first, second
|
| 138 |
return text, ""
|
| 139 |
|
|
|
|
| 140 |
def semantic_match(query, top_k=15):
|
| 141 |
if not query:
|
| 142 |
return {"category": None, "items": []}
|
|
|
|
| 143 |
query = query.strip().lower()
|
| 144 |
q_emb = embedder.encode(query, convert_to_tensor=True)
|
| 145 |
sims_items = {n: float(util.cos_sim(q_emb, emb)) for n, emb in ITEM_EMBEDS.items()}
|
| 146 |
ranked_items = sorted(sims_items.items(), key=lambda x: x[1], reverse=True)
|
| 147 |
item_hits = [n for n, score in ranked_items[:top_k] if score > 0.35]
|
|
|
|
| 148 |
sims_cats = {c: float(util.cos_sim(q_emb, emb)) for c, emb in CATEGORY_EMBEDS.items()}
|
| 149 |
ranked_cats = sorted(sims_cats.items(), key=lambda x: x[1], reverse=True)
|
| 150 |
top_cat, cat_score = (ranked_cats[0] if ranked_cats else (None, 0.0))
|
|
|
|
| 151 |
related_items = []
|
| 152 |
if top_cat and cat_score > 0.35:
|
| 153 |
related_items = [n for n, t in ITEM_TO_TYPE.items() if t == top_cat]
|
|
|
|
| 154 |
combined = list(set(item_hits + related_items))
|
| 155 |
return {"category": top_cat if related_items else None, "items": combined}
|
| 156 |
|
|
|
|
| 157 |
def fetch_yata(force_refresh=False):
|
| 158 |
if not force_refresh and _cache["data"] and (time.time() - _cache["timestamp"] < 300):
|
| 159 |
return _cache["data"], _cache["last_update"]
|
|
@@ -172,19 +177,14 @@ def fetch_yata(force_refresh=False):
|
|
| 172 |
print(f"β Fetch error: {e}")
|
| 173 |
return {"stocks": {}}, "Fetch failed"
|
| 174 |
|
| 175 |
-
#
|
| 176 |
-
_user_capacity = 10
|
| 177 |
-
|
| 178 |
def query_inventory(query_text="", category="", country_name="", capacity=10, refresh=False):
|
| 179 |
-
|
| 180 |
-
if capacity != _user_capacity:
|
| 181 |
-
print(f"π Updating stored capacity: {_user_capacity} β {capacity}")
|
| 182 |
-
_user_capacity = capacity
|
| 183 |
-
|
| 184 |
data, last_update = fetch_yata(force_refresh=refresh)
|
| 185 |
stocks = data.get("stocks", {})
|
| 186 |
rows = []
|
| 187 |
|
|
|
|
| 188 |
parsed_item, parsed_country = parse_freeform_query(query_text)
|
| 189 |
if not country_name and parsed_country:
|
| 190 |
country_name = parsed_country
|
|
@@ -192,38 +192,43 @@ def query_inventory(query_text="", category="", country_name="", capacity=10, re
|
|
| 192 |
semantic_result = semantic_match(item_term) if item_term else {"category": None, "items": []}
|
| 193 |
semantic_items = semantic_result["items"]
|
| 194 |
semantic_category = semantic_result["category"]
|
| 195 |
-
user_code = normalize_country_query(country_name)
|
| 196 |
|
| 197 |
-
|
|
|
|
| 198 |
|
| 199 |
for code_raw, cdata in stocks.items():
|
| 200 |
code = code_raw.upper()
|
| 201 |
cname = COUNTRY_NAMES.get(code, code)
|
|
|
|
| 202 |
if country_name:
|
| 203 |
if user_code:
|
| 204 |
if code != user_code:
|
| 205 |
continue
|
| 206 |
elif country_name.lower() not in cname.lower():
|
| 207 |
continue
|
|
|
|
| 208 |
update_ts = cdata.get("update")
|
| 209 |
update_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_ts)) if update_ts else "Unknown"
|
| 210 |
-
live_lookup = {i["name"]: i for i in cdata.get("stocks", [])}
|
| 211 |
-
candidate_names = sorted(COUNTRY_CATALOG.get(code, set()))
|
| 212 |
|
| 213 |
-
|
|
|
|
|
|
|
| 214 |
itype = ITEM_TO_TYPE.get(iname, "").lower()
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
| 220 |
item_term.lower() in iname.lower()
|
| 221 |
or iname in semantic_items
|
| 222 |
or (semantic_category and itype == semantic_category.lower())
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
| 227 |
if item_ok:
|
| 228 |
rows.append({
|
| 229 |
"Country": cname,
|
|
@@ -231,7 +236,8 @@ def query_inventory(query_text="", category="", country_name="", capacity=10, re
|
|
| 231 |
"Category": itype.title() if itype else "",
|
| 232 |
"Quantity": qty,
|
| 233 |
"Cost": cost,
|
| 234 |
-
|
|
|
|
| 235 |
"Updated": update_str,
|
| 236 |
})
|
| 237 |
|
|
@@ -242,20 +248,37 @@ def query_inventory(query_text="", category="", country_name="", capacity=10, re
|
|
| 242 |
for col in ["Quantity", "Cost", "Max Capacity Cost"]:
|
| 243 |
if col in df.columns:
|
| 244 |
df[col] = df[col].apply(lambda x: f"{x:,.0f}" if isinstance(x, (int, float)) and x != "" else x)
|
| 245 |
-
return df, f"Last update: {last_update}
|
| 246 |
|
|
|
|
| 247 |
def run_query(query_text, category, country, capacity, refresh):
|
| 248 |
return query_inventory(query_text, category, country, capacity, refresh)
|
| 249 |
|
|
|
|
| 250 |
with gr.Blocks(title="π§³ Torn Inventory Viewer") as iface:
|
| 251 |
gr.Markdown("## π§³ Torn Inventory Viewer")
|
| 252 |
-
gr.Markdown("_Search Torn YATA travel stocks with semantic
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
country_box = gr.Textbox(label="Country")
|
| 257 |
-
capacity_slider = gr.Number(label="Travel Capacity", value=_user_capacity, minimum=5, maximum=88, precision=0)
|
| 258 |
-
refresh_check = gr.Checkbox(label="Force refresh", value=False)
|
| 259 |
|
| 260 |
run_btn = gr.Button("π Search / Refresh")
|
| 261 |
result_df = gr.Dataframe(label="Results")
|
|
@@ -265,6 +288,7 @@ with gr.Blocks(title="π§³ Torn Inventory Viewer") as iface:
|
|
| 265 |
inputs=[query_box, category_drop, country_box, capacity_slider, refresh_check],
|
| 266 |
outputs=[result_df, meta_box])
|
| 267 |
|
|
|
|
| 268 |
gr.HTML("""
|
| 269 |
<script>
|
| 270 |
(function () {
|
|
@@ -272,24 +296,42 @@ with gr.Blocks(title="π§³ Torn Inventory Viewer") as iface:
|
|
| 272 |
const app = document.querySelector('gradio-app');
|
| 273 |
return app && app.shadowRoot ? app.shadowRoot : document;
|
| 274 |
}
|
| 275 |
-
function
|
|
|
|
|
|
|
| 276 |
const saved = localStorage.getItem('travel_capacity');
|
| 277 |
-
const
|
| 278 |
-
const
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
}
|
|
|
|
| 290 |
window.addEventListener('DOMContentLoaded', () => {
|
| 291 |
-
setTimeout(
|
| 292 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
})();
|
| 294 |
</script>
|
| 295 |
""")
|
|
@@ -299,4 +341,5 @@ try:
|
|
| 299 |
iface.launch()
|
| 300 |
except Exception as e:
|
| 301 |
import traceback
|
|
|
|
| 302 |
traceback.print_exc()
|
|
|
|
| 19 |
items_data = json.load(f)["items"]
|
| 20 |
|
| 21 |
ITEM_TO_TYPE = {v["name"]: v["type"].lower() for v in items_data.values() if "name" in v and "type" in v}
|
|
|
|
| 22 |
ALL_CATEGORIES = sorted(set(ITEM_TO_TYPE.values()))
|
| 23 |
ITEM_FILE_MTIME = os.path.getmtime("items.json")
|
| 24 |
|
|
|
|
| 66 |
print(f"β οΈ Cache save failed: {e}")
|
| 67 |
|
| 68 |
def auto_alias_categories(embedder, all_categories, all_item_names, top_k=6, threshold=0.38):
|
|
|
|
| 69 |
print("π€ Building category aliases dynamically...")
|
| 70 |
cat_embs = {c: embedder.encode(c, convert_to_tensor=True) for c in all_categories}
|
| 71 |
item_embs = {i: embedder.encode(i, convert_to_tensor=True) for i in all_item_names}
|
|
|
|
| 118 |
return q.upper()
|
| 119 |
return None
|
| 120 |
|
| 121 |
+
# ---------------- Helpers ----------------
|
| 122 |
def parse_freeform_query(text: str):
|
| 123 |
"""Supports 'plushies in uk' and 'uk plushies'."""
|
| 124 |
if not text:
|
| 125 |
return "", ""
|
| 126 |
text = text.strip().lower()
|
| 127 |
+
m = re.match(r"(.+?)\s+in\s+(.+)", text, flags=re.IGNORECASE)
|
| 128 |
if m:
|
| 129 |
return m.group(1).strip(), m.group(2).strip()
|
| 130 |
parts = text.split()
|
|
|
|
| 136 |
return first, second
|
| 137 |
return text, ""
|
| 138 |
|
| 139 |
+
# ---------------- Semantic Match ----------------
|
| 140 |
def semantic_match(query, top_k=15):
|
| 141 |
if not query:
|
| 142 |
return {"category": None, "items": []}
|
| 143 |
+
|
| 144 |
query = query.strip().lower()
|
| 145 |
q_emb = embedder.encode(query, convert_to_tensor=True)
|
| 146 |
sims_items = {n: float(util.cos_sim(q_emb, emb)) for n, emb in ITEM_EMBEDS.items()}
|
| 147 |
ranked_items = sorted(sims_items.items(), key=lambda x: x[1], reverse=True)
|
| 148 |
item_hits = [n for n, score in ranked_items[:top_k] if score > 0.35]
|
| 149 |
+
|
| 150 |
sims_cats = {c: float(util.cos_sim(q_emb, emb)) for c, emb in CATEGORY_EMBEDS.items()}
|
| 151 |
ranked_cats = sorted(sims_cats.items(), key=lambda x: x[1], reverse=True)
|
| 152 |
top_cat, cat_score = (ranked_cats[0] if ranked_cats else (None, 0.0))
|
| 153 |
+
|
| 154 |
related_items = []
|
| 155 |
if top_cat and cat_score > 0.35:
|
| 156 |
related_items = [n for n, t in ITEM_TO_TYPE.items() if t == top_cat]
|
| 157 |
+
|
| 158 |
combined = list(set(item_hits + related_items))
|
| 159 |
return {"category": top_cat if related_items else None, "items": combined}
|
| 160 |
|
| 161 |
+
# ---------------- Fetch YATA ----------------
|
| 162 |
def fetch_yata(force_refresh=False):
|
| 163 |
if not force_refresh and _cache["data"] and (time.time() - _cache["timestamp"] < 300):
|
| 164 |
return _cache["data"], _cache["last_update"]
|
|
|
|
| 177 |
print(f"β Fetch error: {e}")
|
| 178 |
return {"stocks": {}}, "Fetch failed"
|
| 179 |
|
| 180 |
+
# ---------------- Core logic ----------------
|
|
|
|
|
|
|
| 181 |
def query_inventory(query_text="", category="", country_name="", capacity=10, refresh=False):
|
| 182 |
+
print(f"π Query received: '{query_text}', category='{category}', country='{country_name}', cap={capacity}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
data, last_update = fetch_yata(force_refresh=refresh)
|
| 184 |
stocks = data.get("stocks", {})
|
| 185 |
rows = []
|
| 186 |
|
| 187 |
+
# Parse query
|
| 188 |
parsed_item, parsed_country = parse_freeform_query(query_text)
|
| 189 |
if not country_name and parsed_country:
|
| 190 |
country_name = parsed_country
|
|
|
|
| 192 |
semantic_result = semantic_match(item_term) if item_term else {"category": None, "items": []}
|
| 193 |
semantic_items = semantic_result["items"]
|
| 194 |
semantic_category = semantic_result["category"]
|
|
|
|
| 195 |
|
| 196 |
+
# Country gating (strict)
|
| 197 |
+
user_code = normalize_country_query(country_name)
|
| 198 |
|
| 199 |
for code_raw, cdata in stocks.items():
|
| 200 |
code = code_raw.upper()
|
| 201 |
cname = COUNTRY_NAMES.get(code, code)
|
| 202 |
+
|
| 203 |
if country_name:
|
| 204 |
if user_code:
|
| 205 |
if code != user_code:
|
| 206 |
continue
|
| 207 |
elif country_name.lower() not in cname.lower():
|
| 208 |
continue
|
| 209 |
+
|
| 210 |
update_ts = cdata.get("update")
|
| 211 |
update_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_ts)) if update_ts else "Unknown"
|
|
|
|
|
|
|
| 212 |
|
| 213 |
+
# ONLY iterate this country's actual items (no global catalog)
|
| 214 |
+
for item in cdata.get("stocks", []):
|
| 215 |
+
iname = item.get("name", "")
|
| 216 |
itype = ITEM_TO_TYPE.get(iname, "").lower()
|
| 217 |
+
qty = item.get("quantity", 0)
|
| 218 |
+
cost = item.get("cost", 0)
|
| 219 |
+
|
| 220 |
+
# Item filter
|
| 221 |
+
if item_term:
|
| 222 |
+
item_ok = (
|
| 223 |
item_term.lower() in iname.lower()
|
| 224 |
or iname in semantic_items
|
| 225 |
or (semantic_category and itype == semantic_category.lower())
|
| 226 |
+
)
|
| 227 |
+
elif category:
|
| 228 |
+
item_ok = category.lower() == itype
|
| 229 |
+
else:
|
| 230 |
+
item_ok = True
|
| 231 |
+
|
| 232 |
if item_ok:
|
| 233 |
rows.append({
|
| 234 |
"Country": cname,
|
|
|
|
| 236 |
"Category": itype.title() if itype else "",
|
| 237 |
"Quantity": qty,
|
| 238 |
"Cost": cost,
|
| 239 |
+
# IMPORTANT: use the function arg 'capacity' directly
|
| 240 |
+
"Max Capacity Cost": cost * capacity,
|
| 241 |
"Updated": update_str,
|
| 242 |
})
|
| 243 |
|
|
|
|
| 248 |
for col in ["Quantity", "Cost", "Max Capacity Cost"]:
|
| 249 |
if col in df.columns:
|
| 250 |
df[col] = df[col].apply(lambda x: f"{x:,.0f}" if isinstance(x, (int, float)) and x != "" else x)
|
| 251 |
+
return df, f"Last update: {last_update}"
|
| 252 |
|
| 253 |
+
# ---------------- Wrapper ----------------
|
| 254 |
def run_query(query_text, category, country, capacity, refresh):
|
| 255 |
return query_inventory(query_text, category, country, capacity, refresh)
|
| 256 |
|
| 257 |
+
# ---------------- Gradio UI ----------------
|
| 258 |
with gr.Blocks(title="π§³ Torn Inventory Viewer") as iface:
|
| 259 |
gr.Markdown("## π§³ Torn Inventory Viewer")
|
| 260 |
+
gr.Markdown("_Search Torn YATA travel stocks with semantic matching._")
|
| 261 |
+
|
| 262 |
+
query_box = gr.Textbox(label="Search (semantic, e.g. 'flowers in England')", elem_id="qbox")
|
| 263 |
+
category_drop = gr.Dropdown(label="Category (optional exact match)", choices=[""] + ALL_CATEGORIES, elem_id="catdrop")
|
| 264 |
+
country_box = gr.Textbox(label="Country (optional, e.g. UK, Cayman, Japan)", elem_id="countrybox")
|
| 265 |
+
capacity_slider = gr.Number(label="Travel Capacity", value=10, minimum=5, maximum=88, precision=0, elem_id="cap_num")
|
| 266 |
+
refresh_check = gr.Checkbox(label="Force refresh (ignore cache)", value=False)
|
| 267 |
+
|
| 268 |
+
# Hidden bridge for capacity β backend init
|
| 269 |
+
cap_bridge = gr.Textbox(visible=False, elem_id="cap_bridge")
|
| 270 |
+
|
| 271 |
+
def apply_bridge(cap_text):
|
| 272 |
+
try:
|
| 273 |
+
v = float(cap_text)
|
| 274 |
+
if 5 <= v <= 88:
|
| 275 |
+
return gr.Number.update(value=v)
|
| 276 |
+
except:
|
| 277 |
+
pass
|
| 278 |
+
return gr.Number.update() # no change
|
| 279 |
|
| 280 |
+
# When the bridge receives a value, update the slider (so backend sees it)
|
| 281 |
+
cap_bridge.change(apply_bridge, inputs=[cap_bridge], outputs=[capacity_slider])
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
run_btn = gr.Button("π Search / Refresh")
|
| 284 |
result_df = gr.Dataframe(label="Results")
|
|
|
|
| 288 |
inputs=[query_box, category_drop, country_box, capacity_slider, refresh_check],
|
| 289 |
outputs=[result_df, meta_box])
|
| 290 |
|
| 291 |
+
# --- Shadow-DOM aware JS: restore capacity & push into hidden bridge BEFORE first search ---
|
| 292 |
gr.HTML("""
|
| 293 |
<script>
|
| 294 |
(function () {
|
|
|
|
| 296 |
const app = document.querySelector('gradio-app');
|
| 297 |
return app && app.shadowRoot ? app.shadowRoot : document;
|
| 298 |
}
|
| 299 |
+
function el(sel) { return root().querySelector(sel); }
|
| 300 |
+
|
| 301 |
+
function restoreCapacity() {
|
| 302 |
const saved = localStorage.getItem('travel_capacity');
|
| 303 |
+
const capField = el('#cap_num input[type=number]');
|
| 304 |
+
const bridge = el('#cap_bridge textarea');
|
| 305 |
+
|
| 306 |
+
if (saved && capField) {
|
| 307 |
+
// Update visible field
|
| 308 |
+
capField.value = saved;
|
| 309 |
+
// Update frontend state
|
| 310 |
+
capField.dispatchEvent(new Event('input', { bubbles: true }));
|
| 311 |
+
// Push into hidden bridge -> triggers backend .change -> updates slider value server-side
|
| 312 |
+
if (bridge) {
|
| 313 |
+
bridge.value = saved;
|
| 314 |
+
bridge.dispatchEvent(new Event('input', { bubbles: true }));
|
| 315 |
+
bridge.dispatchEvent(new Event('change', { bubbles: true }));
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
// Save on changes
|
| 320 |
+
if (capField && !capField.dataset.synced) {
|
| 321 |
+
capField.dataset.synced = '1';
|
| 322 |
+
capField.addEventListener('input', () => {
|
| 323 |
+
localStorage.setItem('travel_capacity', capField.value);
|
| 324 |
+
});
|
| 325 |
+
}
|
| 326 |
}
|
| 327 |
+
|
| 328 |
window.addEventListener('DOMContentLoaded', () => {
|
| 329 |
+
setTimeout(restoreCapacity, 500);
|
| 330 |
});
|
| 331 |
+
|
| 332 |
+
// Keep trying on re-renders
|
| 333 |
+
const obs = new MutationObserver(() => setTimeout(restoreCapacity, 100));
|
| 334 |
+
obs.observe(document.documentElement, { childList: true, subtree: true });
|
| 335 |
})();
|
| 336 |
</script>
|
| 337 |
""")
|
|
|
|
| 341 |
iface.launch()
|
| 342 |
except Exception as e:
|
| 343 |
import traceback
|
| 344 |
+
print("β Application failed to start:")
|
| 345 |
traceback.print_exc()
|