scottymcgee commited on
Commit
9f09c1f
·
verified ·
1 Parent(s): 3890900

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -118
app.py CHANGED
@@ -1,12 +1,11 @@
1
  # app.py
2
  import gradio as gr
3
- from PIL import Image
4
  import numpy as np
5
  import pandas as pd
6
  import io
7
  import os
8
  import cv2
9
- import glob
10
  from pathlib import Path
11
  from typing import List, Dict, Any, Tuple
12
 
@@ -35,6 +34,21 @@ def _to_np(img: Image.Image) -> np.ndarray:
35
  return np.array(_ensure_rgb(img))
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def _hsv_hist_features(img: Image.Image) -> np.ndarray:
39
  """Return simple features for matching and scoring.
40
  - Hue histogram (18 bins)
@@ -69,7 +83,9 @@ def _complementary_hue_score(q_hist: np.ndarray, w_hist: np.ndarray) -> float:
69
  q_h = q_hist[:hb]
70
  w_h = w_hist[:hb]
71
  q_shift = np.roll(q_h, hb // 2)
72
- hue_sim = float(np.dot(q_shift, w_h) / (np.linalg.norm(q_shift) * np.linalg.norm(w_h) + 1e-8))
 
 
73
 
74
  # Encourage pairing items with different edge density (texture contrast)
75
  q_ed = q_hist[-1]
@@ -254,7 +270,9 @@ def _get_embedder() -> _Embedder:
254
  # ----------------------------
255
  # State schema:
256
  # {
257
- # "wardrobe": [ {"id": int, "name": str, "image": PIL.Image, "features": np.ndarray, "embedding": np.ndarray, "rating": int|None} ],
 
 
258
  # "selected_idx": int|None
259
  # }
260
 
@@ -262,7 +280,8 @@ def _blank_state() -> Dict[str, Any]:
262
  return {"wardrobe": [], "selected_idx": None}
263
 
264
 
265
-
 
266
  # ----------------------------
267
 
268
  def add_wardrobe(files: List[Any], state: Dict[str, Any]):
@@ -293,6 +312,7 @@ def add_wardrobe(files: List[Any], state: Dict[str, Any]):
293
  "id": next_id,
294
  "name": name,
295
  "image": img,
 
296
  "features": feats,
297
  "embedding": emb,
298
  "category": category,
@@ -307,9 +327,7 @@ def add_wardrobe(files: List[Any], state: Dict[str, Any]):
307
 
308
 
309
  def add_wardrobe_from_dir(example_dir: str, state: Dict[str, Any]):
310
- """Load all images in a folder into the wardrobe and auto-rate/classify them.
311
- Used by gr.Examples. Accepts relative paths in the Space repo.
312
- """
313
  if not example_dir:
314
  return state, _render_gallery(state), _ratings_df(state)
315
  p = Path(example_dir)
@@ -325,34 +343,13 @@ def clear_wardrobe(state: Dict[str, Any]):
325
  return state, [], _ratings_df(state)
326
 
327
 
328
- def _render_gallery(state: Dict[str, Any]) -> List[Image.Image]:
329
- return [w["image"] for w in state.get("wardrobe", [])]
330
-
331
-
332
- def on_select_item(item_label: str, state: Dict[str, Any]):
333
- if not item_label:
334
- return state, None, gr.update(value=50)
335
- # label format: "#<id> · <name>"
336
- try:
337
- item_id = int(item_label.split(" ")[0][1:])
338
- except Exception:
339
- return state, None, gr.update(value=50)
340
-
341
- idx = next((i for i, w in enumerate(state["wardrobe"]) if w["id"] == item_id), None)
342
- state["selected_idx"] = idx
343
- if idx is None:
344
- return state, None, gr.update(value=50)
345
- w = state["wardrobe"][idx]
346
- current_rating = w["rating"] if w["rating"] is not None else 50
347
- return state, w["image"], gr.update(value=int(current_rating))
348
-
349
-
350
- def save_rating(rating: int, state: Dict[str, Any]):
351
- idx = state.get("selected_idx", None)
352
- if idx is None:
353
- return state, _ratings_df(state)
354
- state["wardrobe"][idx]["rating"] = int(rating)
355
- return state, _ratings_df(state)
356
 
357
 
358
  def _ratings_df(state: Dict[str, Any]) -> pd.DataFrame:
@@ -376,25 +373,6 @@ def export_ratings(state: Dict[str, Any]):
376
  df.to_csv(buf, index=False)
377
  buf.seek(0)
378
  return buf
379
- buf = io.BytesIO()
380
- df.to_csv(buf, index=False)
381
- buf.seek(0)
382
- return buf
383
-
384
-
385
- def import_ratings(file_obj, state: Dict[str, Any]):
386
- # Deprecated in auto-rating flow; keep no-op for compatibility
387
- return state, _ratings_df(state)
388
- try:
389
- df = pd.read_csv(file_obj.name if hasattr(file_obj, "name") else file_obj)
390
- names_to_rating = {str(row["name"]): int(row["rating"]) if not pd.isna(row["rating"]) else None
391
- for _, row in df.iterrows()}
392
- for w in state.get("wardrobe", []):
393
- if w["name"] in names_to_rating:
394
- w["rating"] = names_to_rating[w["name"]]
395
- except Exception:
396
- pass
397
- return state, _ratings_df(state)
398
 
399
 
400
  # ----------------------------
@@ -403,7 +381,9 @@ def import_ratings(file_obj, state: Dict[str, Any]):
403
 
404
  def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, state: Dict[str, Any]):
405
  if query_img is None:
406
- return 0, "No image provided.", []
 
 
407
 
408
  query_img = _ensure_rgb(query_img)
409
 
@@ -452,7 +432,7 @@ def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, s
452
  else: # Complementary color + style
453
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
454
 
455
- # Quality prior
456
  if w.get("rating") is not None:
457
  qual = 0.5 + 0.5 * (w["rating"] / 100.0)
458
  final *= qual
@@ -470,73 +450,112 @@ def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, s
470
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
471
  candidates.append((final, w))
472
 
 
473
  candidates.sort(key=lambda x: x[0], reverse=True)
474
- recs = [w["image"] for _, w in candidates[: max(0, top_k)]]
475
-
476
- if len(recs) == 0:
477
- txt = f"Predicted rating: {pred}/100. No matches found in your wardrobe."
478
- return pred, txt, []
 
 
 
 
479
 
480
- top_names = ", ".join([f"{w['name']} ({w.get('category')})" for _, w in candidates[: max(0, top_k)]])
 
481
  txt = (
482
- f"Predicted rating: {pred}/100. Query category: {qcat}. Suggested pairings: {top_names}."
483
- f"Logic: {matching_mode.lower()} with category filtering and quality prior."
 
 
 
484
  )
485
- return pred, txt, recs
486
 
487
- # Model rating
488
- try:
489
- scorer = _get_scorer()
490
- pred = scorer.predict_1to100(query_img)
491
- except Exception:
492
- # Fallback if model unavailable
493
- pred = 50
494
-
495
- # Compute compatibility with wardrobe
496
- qfeat = _hsv_hist_features(query_img)
497
- candidates = []
498
- for w in state.get("wardrobe", []):
499
- comp = _complementary_hue_score(qfeat, w["features"])
500
- # Weight by user rating if available
501
- user_w = 1.0
502
- if w["rating"] is not None:
503
- user_w = 0.5 + 0.5 * (w["rating"] / 100.0) # 0.5..1.0
504
- final = comp * user_w
505
- candidates.append((final, w))
506
 
507
- candidates.sort(key=lambda x: x[0], reverse=True)
508
- recs = [w["image"] for _, w in candidates[: max(0, top_k)]]
 
 
 
 
 
509
 
510
- if len(recs) == 0:
511
- txt = f"Predicted rating: {pred}/100. No matches found in your wardrobe."
512
- return pred, txt, []
513
-
514
- top_names = ", ".join([w["name"] for _, w in candidates[: max(0, top_k)]])
515
- txt = (
516
- f"Predicted rating: {pred}/100. Suggested pairings from your wardrobe: {top_names}."
517
- f"Logic: complementary hues + texture contrast + your cached ratings."
518
- )
519
- return pred, txt, recs
520
 
521
 
522
  # ----------------------------
523
  # Gradio UI
524
  # ----------------------------
525
- with gr.Blocks(title="Wardrobe Rater + Recommender", css=".gradio-container {max-width: 1200px}") as demo:
526
- gr.Markdown("# Wardrobe Rater + Recommender\nUpload, auto-rate with your HF model, and get AI pair suggestions. Category-aware matching avoids recommending the same type.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
 
528
  app_state = gr.State(_blank_state())
529
 
530
  with gr.Tab("1) Wardrobe Manager"):
 
531
  with gr.Row():
532
- wardrobe_uploader = gr.File(label="Upload wardrobe images", file_types=["image"], file_count="multiple")
 
 
 
 
 
533
  with gr.Row():
534
- add_btn = gr.Button("Add to wardrobe (auto-rate + auto-category)")
535
- clear_btn = gr.Button("Clear wardrobe")
536
- gallery = gr.Gallery(label="Current wardrobe", columns=6, height=250)
537
- ratings_table = gr.Dataframe(headers=["id", "name", "category", "model_rating"], interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
538
 
539
- # Add Gradio Examples for example wardrobe
540
  gr.Markdown("### Or load an example wardrobe")
541
  example_dir = gr.Textbox(label="Example folder path", value="examples/wardrobe_basic", visible=False)
542
  gr.Examples(
@@ -548,23 +567,61 @@ with gr.Blocks(title="Wardrobe Rater + Recommender", css=".gradio-container {max
548
  run_on_click=True,
549
  )
550
 
551
-
552
  with gr.Tab("2) Rate + Recommend New Item"):
 
553
  with gr.Row():
554
- query_img = gr.Image(label="Upload or take photo", sources=["upload", "webcam","clipboard"], type="pil")
 
 
 
 
 
 
555
  topk = gr.Slider(1, 6, value=3, step=1, label="# of matches to return")
556
- matching_mode = gr.Radio(["Complementary color+style", "Similar style"], value="Complementary color+style", label="Matching mode")
557
- go_btn = gr.Button("Rate + Recommend")
 
 
 
 
 
 
558
  with gr.Row():
559
- pred_score = gr.Number(label="Predicted rating (1-100)")
560
- rec_text = gr.Markdown()
561
- rec_gallery = gr.Gallery(label="Matches in your wardrobe", columns=6, height=250)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
562
 
563
  # --- Wiring ---
564
- add_btn.click(add_wardrobe, inputs=[wardrobe_uploader, app_state], outputs=[app_state, gallery, ratings_table])
565
- clear_btn.click(clear_wardrobe, inputs=[app_state], outputs=[app_state, gallery, ratings_table])
 
 
 
 
 
 
 
 
566
 
567
- go_btn.click(rate_and_recommend, inputs=[query_img, topk, matching_mode, app_state], outputs=[pred_score, rec_text, rec_gallery])
 
 
 
 
568
 
569
  # Lightweight tests. Run only when RUN_TESTS=1
570
  if __name__ == "__main__":
@@ -586,9 +643,14 @@ if __name__ == "__main__":
586
  # Test recommend path with small wardrobe
587
  st = _blank_state()
588
  img_b = solid(32)
589
- st["wardrobe"].append({"id":0, "name":"test.png", "image":img_b, "features":_hsv_hist_features(img_b), "embedding":_get_embedder().embed(img_b), "rating":50})
590
- pred, txt, recs = rate_and_recommend(solid(200), 1, "Similar style", st)
 
 
 
 
591
  assert isinstance(pred, int) and isinstance(txt, str) and isinstance(recs, list)
 
592
  print("Tests passed.")
593
  else:
594
- demo.launch()
 
1
  # app.py
2
  import gradio as gr
3
+ from PIL import Image, ImageOps
4
  import numpy as np
5
  import pandas as pd
6
  import io
7
  import os
8
  import cv2
 
9
  from pathlib import Path
10
  from typing import List, Dict, Any, Tuple
11
 
 
34
  return np.array(_ensure_rgb(img))
35
 
36
 
37
+ def _thumbnail(img: Image.Image, max_side: int = 320) -> Image.Image:
38
+ """
39
+ Create a *small* thumbnail for gallery display so you don't need to scroll.
40
+ Keeps aspect ratio; pads to square so grids look neat.
41
+ """
42
+ img = _ensure_rgb(img.copy())
43
+ img.thumbnail((max_side, max_side))
44
+ # Pad to square with white (looks nicer for product photos)
45
+ w, h = img.size
46
+ side = max(w, h)
47
+ bg = Image.new("RGB", (side, side), (255, 255, 255))
48
+ bg.paste(img, ((side - w) // 2, (side - h) // 2))
49
+ return bg
50
+
51
+
52
  def _hsv_hist_features(img: Image.Image) -> np.ndarray:
53
  """Return simple features for matching and scoring.
54
  - Hue histogram (18 bins)
 
83
  q_h = q_hist[:hb]
84
  w_h = w_hist[:hb]
85
  q_shift = np.roll(q_h, hb // 2)
86
+ # cosine similarity on shifted hues
87
+ denom = (np.linalg.norm(q_shift) * np.linalg.norm(w_h) + 1e-8)
88
+ hue_sim = float(np.dot(q_shift, w_h) / denom)
89
 
90
  # Encourage pairing items with different edge density (texture contrast)
91
  q_ed = q_hist[-1]
 
270
  # ----------------------------
271
  # State schema:
272
  # {
273
+ # "wardrobe": [ {"id": int, "name": str, "image": PIL.Image, "thumb": PIL.Image,
274
+ # "features": np.ndarray, "embedding": np.ndarray, "category": str,
275
+ # "rating": int|None} ],
276
  # "selected_idx": int|None
277
  # }
278
 
 
280
  return {"wardrobe": [], "selected_idx": None}
281
 
282
 
283
+ # ----------------------------
284
+ # Wardrobe management
285
  # ----------------------------
286
 
287
  def add_wardrobe(files: List[Any], state: Dict[str, Any]):
 
312
  "id": next_id,
313
  "name": name,
314
  "image": img,
315
+ "thumb": _thumbnail(img),
316
  "features": feats,
317
  "embedding": emb,
318
  "category": category,
 
327
 
328
 
329
  def add_wardrobe_from_dir(example_dir: str, state: Dict[str, Any]):
330
+ """Load all images in a folder into the wardrobe and auto-rate/classify them."""
 
 
331
  if not example_dir:
332
  return state, _render_gallery(state), _ratings_df(state)
333
  p = Path(example_dir)
 
343
  return state, [], _ratings_df(state)
344
 
345
 
346
+ def _render_gallery(state: Dict[str, Any]) -> List[Tuple[Image.Image, str]]:
347
+ """Return list of (thumbnail, caption)."""
348
+ out = []
349
+ for w in state.get("wardrobe", []):
350
+ caption = f"#{w['id']} · {w['name']} · {w.get('category','?')} · {w.get('rating','-')}/100"
351
+ out.append((w["thumb"], caption))
352
+ return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
 
355
  def _ratings_df(state: Dict[str, Any]) -> pd.DataFrame:
 
373
  df.to_csv(buf, index=False)
374
  buf.seek(0)
375
  return buf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
 
378
  # ----------------------------
 
381
 
382
  def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, state: Dict[str, Any]):
383
  if query_img is None:
384
+ return 0, "No image provided.", [], pd.DataFrame(columns=[
385
+ "rank", "name", "category", "model_rating", "match_score"
386
+ ])
387
 
388
  query_img = _ensure_rgb(query_img)
389
 
 
432
  else: # Complementary color + style
433
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
434
 
435
+ # Quality prior (prefer items you generally like)
436
  if w.get("rating") is not None:
437
  qual = 0.5 + 0.5 * (w["rating"] / 100.0)
438
  final *= qual
 
450
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
451
  candidates.append((final, w))
452
 
453
+ # Rank and prepare outputs
454
  candidates.sort(key=lambda x: x[0], reverse=True)
455
+ chosen = candidates[: max(0, top_k)]
456
+
457
+ if not chosen:
458
+ txt = f"**Predicted wear score:** {pred}/100 \n" \
459
+ f"**Detected category:** {qcat} \n" \
460
+ f"**Results:** No compatible matches found in your wardrobe."
461
+ return pred, txt, [], pd.DataFrame(columns=[
462
+ "rank", "name", "category", "model_rating", "match_score"
463
+ ])
464
 
465
+ # Human-friendly summary
466
+ top_names = ", ".join([f"{w['name']} ({w.get('category')})" for _, w in chosen])
467
  txt = (
468
+ f"**Predicted wear score:** {pred}/100 \n"
469
+ f"**Detected category:** `{qcat}` \n"
470
+ f"**Matching mode:** `{matching_mode}` \n"
471
+ f"**Suggested pairings:** {top_names} \n"
472
+ f"_Scoring blends style similarity (CLIP) with color/texture complement and your model ratings._"
473
  )
 
474
 
475
+ # Gallery (thumbnails) + table
476
+ rec_gallery = [(w["thumb"], f"{w['name']} · {w.get('category')} · {w.get('rating','-')}/100")
477
+ for _, w in chosen]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ rec_table = pd.DataFrame([{
480
+ "rank": i + 1,
481
+ "name": w["name"],
482
+ "category": w.get("category"),
483
+ "model_rating": w.get("rating"),
484
+ "match_score": round(float(s), 3)
485
+ } for i, (s, w) in enumerate(chosen)])
486
 
487
+ return pred, txt, rec_gallery, rec_table
 
 
 
 
 
 
 
 
 
488
 
489
 
490
  # ----------------------------
491
  # Gradio UI
492
  # ----------------------------
493
+
494
+ APP_DESCRIPTION = """
495
+ # Wardrobe Rater + Recommender
496
+
497
+ **What this app does (1 minute):**
498
+ - **Upload** photos of items in your wardrobe (tops, pants, jackets, shoes).
499
+ - The app **auto-rates** each item with your model and tags its category.
500
+ - When you upload a **new item** (e.g., a shopping photo), it:
501
+ 1) Predicts how likely **you** are to wear it (1–100),
502
+ 2) Suggests the **best matches** from your wardrobe using style similarity and color/texture complement.
503
+
504
+ **How to use it:**
505
+ 1. Go to **“1) Wardrobe Manager”** and upload several wardrobe images (front-on, good lighting).
506
+ 2. Then open **“2) Rate + Recommend New Item”**, upload your candidate item, pick the number of matches, and click **Rate + Recommend**.
507
+ 3. Review the **score**, the **explanation**, the **top matches** (thumbnails), and the **table**.
508
+ """
509
+
510
+ WARDROBE_TIPS = """
511
+ **What to upload:** clear product-style photos (JPG/PNG).
512
+ Avoid group photos or cluttered backgrounds when possible.
513
+ You can click thumbnails to preview full size.
514
+ """
515
+
516
+ QUERY_TIPS = """
517
+ **Upload a single item** you’re considering (e.g., a screenshot or photo from a listing).
518
+ Good lighting and centered framing helps the detector and embeddings.
519
+ """
520
+
521
+ with gr.Blocks(title="Wardrobe Rater + Recommender",
522
+ css="""
523
+ .gradio-container {max-width: 1100px}
524
+ .small-note {font-size: 0.9em; color: #4b5563}
525
+ """) as demo:
526
+ gr.Markdown(APP_DESCRIPTION)
527
 
528
  app_state = gr.State(_blank_state())
529
 
530
  with gr.Tab("1) Wardrobe Manager"):
531
+ gr.Markdown("### Upload your wardrobe")
532
  with gr.Row():
533
+ wardrobe_uploader = gr.File(
534
+ label="Upload wardrobe images",
535
+ file_types=["image"],
536
+ file_count="multiple"
537
+ )
538
+ gr.Markdown(WARDROBE_TIPS, elem_classes=["small-note"])
539
  with gr.Row():
540
+ add_btn = gr.Button("Add to wardrobe (auto-rate + auto-category)", variant="primary")
541
+ clear_btn = gr.Button("🗑️ Clear wardrobe", variant="secondary")
542
+
543
+ gallery = gr.Gallery(
544
+ label="Current wardrobe (click to preview)",
545
+ columns=6,
546
+ height=220,
547
+ allow_preview=True,
548
+ show_label=True
549
+ )
550
+
551
+ gr.Markdown("### Item summary")
552
+ ratings_table = gr.Dataframe(
553
+ headers=["id", "name", "category", "model_rating"],
554
+ interactive=False,
555
+ wrap=True
556
+ )
557
 
558
+ # Optional Example loader (kept hidden input textbox)
559
  gr.Markdown("### Or load an example wardrobe")
560
  example_dir = gr.Textbox(label="Example folder path", value="examples/wardrobe_basic", visible=False)
561
  gr.Examples(
 
567
  run_on_click=True,
568
  )
569
 
 
570
  with gr.Tab("2) Rate + Recommend New Item"):
571
+ gr.Markdown("### Upload a candidate item")
572
  with gr.Row():
573
+ query_img = gr.Image(
574
+ label="Upload or paste image (single item)",
575
+ sources=["upload", "webcam", "clipboard"],
576
+ type="pil"
577
+ )
578
+ controls_col = gr.Column()
579
+ with controls_col:
580
  topk = gr.Slider(1, 6, value=3, step=1, label="# of matches to return")
581
+ matching_mode = gr.Radio(
582
+ ["Complementary color+style", "Similar style"],
583
+ value="Complementary color+style",
584
+ label="Matching mode"
585
+ )
586
+ go_btn = gr.Button("⭐ Rate + Recommend", variant="primary")
587
+ gr.Markdown(QUERY_TIPS, elem_classes=["small-note"])
588
+
589
  with gr.Row():
590
+ pred_score = gr.Number(label="Predicted wear score (1100)")
591
+ rec_text = gr.Markdown() # human-readable summary
592
+
593
+ gr.Markdown("### Top matches (click to preview)")
594
+ rec_gallery = gr.Gallery(
595
+ columns=6,
596
+ height=220,
597
+ allow_preview=True,
598
+ show_label=False
599
+ )
600
+
601
+ gr.Markdown("### Details")
602
+ rec_table = gr.Dataframe(
603
+ headers=["rank", "name", "category", "model_rating", "match_score"],
604
+ interactive=False,
605
+ wrap=True
606
+ )
607
 
608
  # --- Wiring ---
609
+ add_btn.click(
610
+ add_wardrobe,
611
+ inputs=[wardrobe_uploader, app_state],
612
+ outputs=[app_state, gallery, ratings_table]
613
+ )
614
+ clear_btn.click(
615
+ clear_wardrobe,
616
+ inputs=[app_state],
617
+ outputs=[app_state, gallery, ratings_table]
618
+ )
619
 
620
+ go_btn.click(
621
+ rate_and_recommend,
622
+ inputs=[query_img, topk, matching_mode, app_state],
623
+ outputs=[pred_score, rec_text, rec_gallery, rec_table]
624
+ )
625
 
626
  # Lightweight tests. Run only when RUN_TESTS=1
627
  if __name__ == "__main__":
 
643
  # Test recommend path with small wardrobe
644
  st = _blank_state()
645
  img_b = solid(32)
646
+ st["wardrobe"].append({
647
+ "id":0, "name":"test.png", "image":img_b, "thumb":_thumbnail(img_b),
648
+ "features":_hsv_hist_features(img_b), "embedding":_get_embedder().embed(img_b),
649
+ "rating":50, "category":"pants"
650
+ })
651
+ pred, txt, recs, tbl = rate_and_recommend(solid(200), 1, "Similar style", st)
652
  assert isinstance(pred, int) and isinstance(txt, str) and isinstance(recs, list)
653
+ assert isinstance(tbl, pd.DataFrame)
654
  print("Tests passed.")
655
  else:
656
+ demo.launch()