emirkisa commited on
Commit
9cd110f
·
verified ·
1 Parent(s): 33c4a54

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +134 -315
app.py CHANGED
@@ -100,9 +100,7 @@ DAVIS_PALETTE = np.array([
100
  DEFAULT_FPS = 24
101
  DEFAULT_ALPHA = 0.55
102
  DEFAULT_CRF = 18
103
- MAX_COMPARE = 6 # slots in the Compare tab
104
- PAGE_SIZE = 9 # videos per page in Multi-Video tab
105
- THUMB_W, THUMB_H = 320, 200 # thumbnail dimensions for Gallery
106
 
107
  # ── Dataset download ───────────────────────────────────────────────────────────
108
 
@@ -514,8 +512,6 @@ def build_ui():
514
  n_2017 = int(DF["in_2017"].sum())
515
  _first = ALL_SEQUENCES[0]
516
  _first_n = len(_get_frame_paths(_first))
517
- total_pages = (len(ALL_SEQUENCES) + PAGE_SIZE - 1) // PAGE_SIZE
518
-
519
  with gr.Blocks(title="DAVIS Dataset Explorer") as demo:
520
 
521
  gr.Markdown(
@@ -648,14 +644,36 @@ def build_ui():
648
  btn_play.click(get_video, [seq_dd, v_ov, v_a, v_fps], [video_out, v_status])
649
 
650
  # ──────────────────────────────────────────────────────────────
651
- # Tab 3 · Gallery (thumbnail grid of all sequences)
652
  # ──────────────────────────────────────────────────────────────
653
  with gr.TabItem("🖼 Gallery"):
654
- gr.Markdown(
655
- "Thumbnails of all sequences (first frame). "
656
- "Use the filters to narrow down, then **click any thumbnail** "
657
- "to instantly play that sequence below."
 
658
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  with gr.Row():
660
  g_year = gr.Dropdown(["All years","2016 only","2017 only"],
661
  value="All years", label="Year", scale=1)
@@ -666,345 +684,146 @@ def build_ui():
666
  g_srch = gr.Textbox(placeholder="Search…", label="Search", scale=2)
667
  with gr.Row():
668
  g_fmin = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
669
- int(DF["frames"].min()), step=1, label="Min frames", scale=3)
 
670
  g_fmax = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
671
- int(DF["frames"].max()), step=1, label="Max frames", scale=3)
 
672
  with gr.Row():
673
- g_ov = gr.Checkbox(value=False, label="Show mask overlay on thumbnails")
674
-
675
- g_count_md = gr.Markdown(f"**{len(ALL_SEQUENCES)} sequences**")
 
 
 
 
 
 
 
676
 
677
- # Gallery component
678
  gallery = gr.Gallery(
679
  value=_ALL_THUMBS,
680
  label="Sequences",
681
- columns=5,
682
  rows=None,
683
  height="auto",
684
  allow_preview=False,
685
  show_label=False,
 
686
  )
687
 
688
- # State holding the sequence names matching current filter (in gallery order)
689
- g_seq_state = gr.State(ALL_SEQUENCES.copy())
 
690
 
691
- gr.Markdown("---")
692
- with gr.Row():
693
- g_info_md = gr.Markdown("*Click a thumbnail to play.*")
694
- with gr.Row():
695
- g_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1,
696
- label="FPS", scale=2)
697
- g_vid_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
698
- g_vid_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
699
- label="Opacity", scale=2)
700
- g_btn_play = gr.Button("▶ Play selected", variant="primary", scale=1)
701
-
702
- with gr.Column(scale=5):
703
- g_vid_status = gr.Markdown("")
704
- g_video = gr.Video(label="Playback", height=400, autoplay=True)
705
- g_selected = gr.State("")
706
-
707
- # Filter → rebuild gallery
708
  g_f_inputs = [g_year, g_split, g_obj, g_fmin, g_fmax, g_srch]
709
 
710
  def _on_g_filter(*args):
711
- ov = args[-1] # last arg is the overlay checkbox
712
- fargs = args[:-1] # filter args
713
  fdf = filter_df(*fargs)
714
  seqs = fdf["sequence"].tolist()
715
  items = build_gallery_items(seqs, overlay=ov)
716
- return items, seqs, f"**{len(seqs)} sequences**"
 
717
 
718
- for inp in g_f_inputs + [g_ov]:
719
- inp.change(_on_g_filter, g_f_inputs + [g_ov],
720
  [gallery, g_seq_state, g_count_md])
721
 
722
- # Click thumbnail load info + auto-generate video
723
- def _on_gallery_click(evt: gr.SelectData, seqs, ov, a, fps):
724
- if evt is None or not seqs:
725
- return "", gr.update(), None, ""
726
- seq = seqs[evt.index]
727
- info = _seq_info(seq)
728
- path, status = get_video(seq, ov, a, fps)
729
- return info, seq, path, status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
 
731
  gallery.select(
732
- _on_gallery_click,
733
- inputs=[g_seq_state, g_vid_ov, g_vid_a, g_fps],
734
- outputs=[g_info_md, g_selected, g_video, g_vid_status],
735
- )
736
- g_btn_play.click(
737
- lambda seq, ov, a, fps: get_video(seq, ov, a, fps),
738
- inputs=[g_selected, g_vid_ov, g_vid_a, g_fps],
739
- outputs=[g_video, g_vid_status],
740
  )
741
 
742
- # ──────────────────────────────────────────────────────────────
743
- # Tab 4 · Multi-Video (paged 3×3 grid, all as MP4)
744
- # ──────────────────────────────────────────────────────────────
745
- with gr.TabItem("📺 Multi-Video"):
746
- gr.Markdown(
747
- f"Watch **{PAGE_SIZE} sequences at once** in a 3×3 grid. "
748
- f"Page through all {len(ALL_SEQUENCES)} sequences. "
749
- "Videos are served from the permanent MP4 cache."
750
- )
751
 
752
- # ── Caching-in-progress overlay (hidden once done) ────────
753
- _initially_done = _is_cache_complete()
754
- with gr.Column(visible=not _initially_done) as mv_wait_col:
755
- gr.Markdown("### Building MP4 cache — please wait…")
756
- mv_wait_status = gr.Markdown(cache_status_md())
757
- gr.Markdown(
758
- "All 90 sequences are being encoded as MP4s in the background "
759
- "(raw + overlay variant each). The grid will unlock automatically "
760
- "when encoding finishes. You can watch other tabs in the meantime."
761
  )
762
- mv_refresh_btn = gr.Button("↻ Refresh status", size="sm")
763
-
764
- # ── Main grid (hidden until cache ready) ──────────────────
765
- with gr.Column(visible=_initially_done) as mv_grid_col:
766
- with gr.Row():
767
- mv_year = gr.Dropdown(["All years","2016 only","2017 only"],
768
- value="All years", label="Year", scale=1)
769
- mv_split = gr.Dropdown(["All splits","Train only","Val only"],
770
- value="All splits", label="Split", scale=1)
771
- mv_obj = gr.Dropdown(["Any # objects","1 object","2 objects","3+ objects"],
772
- value="Any # objects", label="Objects", scale=1)
773
- mv_srch = gr.Textbox(placeholder="Search…", label="Search", scale=2)
774
- with gr.Row():
775
- mv_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1, label="FPS", scale=2)
776
- mv_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
777
- mv_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
778
- label="Opacity", scale=2)
779
- mv_load = gr.Button("▶ Load Page", variant="primary", scale=1)
780
-
781
- with gr.Row():
782
- mv_prev = gr.Button("◀ Prev", scale=1)
783
- with gr.Column(scale=3):
784
- mv_page_lbl = gr.Markdown(f"**Page 1 / {total_pages}**")
785
- mv_next = gr.Button("Next ▶", scale=1)
786
-
787
- mv_status = gr.Markdown("")
788
-
789
- # 9 fixed video slots, 3 rows × 3 cols
790
- mv_vids = []
791
- mv_lbls = []
792
- for row_i in range(3):
793
- with gr.Row():
794
- for col_i in range(3):
795
- with gr.Column():
796
- lbl = gr.Markdown("—")
797
- vid = gr.Video(height=260, autoplay=True, label="")
798
- mv_lbls.append(lbl)
799
- mv_vids.append(vid)
800
-
801
- # State: list of sequences currently matching filter, page index
802
- mv_seq_state = gr.State(ALL_SEQUENCES.copy())
803
- mv_page_state = gr.State(0)
804
-
805
- def _mv_filter(*args):
806
- fdf = filter_df(*args)
807
- seqs = fdf["sequence"].tolist()
808
- tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
809
- return seqs, 0, f"**Page 1 / {tp}**"
810
-
811
- mv_f_inputs = [mv_year, mv_split, mv_obj,
812
- gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
813
- int(DF["frames"].min()), step=1, label=""),
814
- gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
815
- int(DF["frames"].max()), step=1, label=""),
816
- mv_srch]
817
-
818
- # Simpler: just use the three dropdowns + search for multi-video filter
819
- def _mv_filter_simple(yr, sp, ob, sr):
820
- fdf = filter_df(yr, sp, ob,
821
- int(DF["frames"].min()), int(DF["frames"].max()), sr)
822
- seqs = fdf["sequence"].tolist()
823
- tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
824
- return seqs, 0, f"**Page 1 / {tp}**"
825
-
826
- mv_simple_f = [mv_year, mv_split, mv_obj, mv_srch]
827
- for inp in mv_simple_f:
828
- inp.change(_mv_filter_simple, mv_simple_f,
829
- [mv_seq_state, mv_page_state, mv_page_lbl])
830
-
831
- def _load_page(seqs, page, ov, a, fps):
832
- # Block if pre-cache not yet finished
833
- if not _is_cache_complete():
834
- with _cache_lock:
835
- done = sum(1 for v in _cache_progress.values() if v == "done")
836
- total = len(ALL_SEQUENCES)
837
- pct = int(done / total * 100) if total else 0
838
- bar = "█" * (pct // 5) + "░" * (20 - pct // 5)
839
- status = (f"⏳ `[{bar}]` {done}/{total} sequences cached ({pct}%) — "
840
- "please wait for caching to finish then click Load Page again.")
841
- out = []
842
- for _ in range(PAGE_SIZE):
843
- out.append("—")
844
- out.append(None)
845
- out.append(status)
846
- out.append(f"**Page {page + 1} / ?**")
847
- return out
848
-
849
- start = page * PAGE_SIZE
850
- chunk = seqs[start: start + PAGE_SIZE]
851
- tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
852
- pg_lbl = f"**Page {page + 1} / {tp}**"
853
-
854
- # Sequential — encode_sequence() returns in <1 ms when already cached
855
- res, lbs = [], []
856
- for s in chunk:
857
- try:
858
- p, _ = get_video(s, ov, a, fps)
859
- res.append(str(p) if p else None)
860
- except Exception:
861
- res.append(None)
862
- lbs.append(s)
863
-
864
- while len(res) < PAGE_SIZE: # pad
865
- res.append(None); lbs.append("—")
866
-
867
- n_loaded = sum(1 for r in res if r)
868
- status = f"✅ {n_loaded}/{len(chunk)} videos loaded (page {page+1}/{tp})"
869
-
870
- out = []
871
- for lb, r in zip(lbs, res):
872
- out.append(f"**{lb}**" if lb and lb != "—" else "—")
873
- out.append(r)
874
- out.append(status)
875
- out.append(pg_lbl)
876
- return out
877
-
878
- mv_page_outputs = []
879
- for l, v in zip(mv_lbls, mv_vids):
880
- mv_page_outputs.append(l)
881
- mv_page_outputs.append(v)
882
- mv_page_outputs.append(mv_status)
883
- mv_page_outputs.append(mv_page_lbl)
884
-
885
- mv_load.click(
886
- _load_page,
887
- inputs=[mv_seq_state, mv_page_state, mv_ov, mv_a, mv_fps],
888
- outputs=mv_page_outputs,
889
- )
890
 
891
- def _prev(seqs, page):
892
- new_p = max(0, page - 1)
893
- tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
894
- return new_p, f"**Page {new_p+1} / {tp}**"
895
-
896
- def _next(seqs, page):
897
- tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
898
- new_p = min(tp - 1, page + 1)
899
- return new_p, f"**Page {new_p+1} / {tp}**"
900
-
901
- mv_prev.click(_prev, [mv_seq_state, mv_page_state],
902
- [mv_page_state, mv_page_lbl])
903
- mv_next.click(_next, [mv_seq_state, mv_page_state],
904
- [mv_page_state, mv_page_lbl])
905
-
906
- # ── Cache progress wiring (timer + manual refresh) ────────
907
- def _mv_cache_tick():
908
- """Auto-refresh: hides the wait panel and shows the grid when done."""
909
- done_now = _is_cache_complete()
910
- status = cache_status_md()
911
  return (
912
- status, # mv_wait_status
913
- gr.update(visible=not done_now), # mv_wait_col
914
- gr.update(visible=done_now), # mv_grid_col
915
- gr.update(active=not done_now), # timer — stop when done
 
 
 
 
916
  )
917
 
918
- mv_refresh_btn.click(_mv_cache_tick,
919
- outputs=[mv_wait_status, mv_wait_col,
920
- mv_grid_col, gr.State()])
921
-
922
- mv_timer = gr.Timer(value=4, active=not _initially_done)
923
- mv_timer.tick(_mv_cache_tick,
924
- outputs=[mv_wait_status, mv_wait_col,
925
- mv_grid_col, mv_timer])
926
-
927
- # ──────────────────────────────────────────────────────────────
928
- # Tab 5 · Compare (up to 6 side-by-side)
929
- # ──────────────────────────────────────────────────────────────
930
- with gr.TabItem("⚖️ Compare"):
931
- gr.Markdown(
932
- "Pick up to **6 sequences**, set FPS/overlay, "
933
- "then **Load All** — encoded in parallel and cached."
934
  )
935
- with gr.Row():
936
- cmp_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1, label="FPS", scale=2)
937
- cmp_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
938
- cmp_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
939
- label="Opacity", scale=2)
940
- cmp_btn = gr.Button("▶ Load All", variant="primary", scale=1)
941
-
942
- cmp_dds = []
943
- cmp_vids = []
944
- cmp_lbls = []
945
- default_seqs = (ALL_SEQUENCES + [None] * MAX_COMPARE)[:MAX_COMPARE]
946
-
947
- for row_i in range(2):
948
- with gr.Row():
949
- for col_i in range(3):
950
- si = row_i * 3 + col_i
951
- with gr.Column():
952
- dd = gr.Dropdown([""] + ALL_SEQUENCES,
953
- value=default_seqs[si] or "",
954
- label=f"Slot {si+1}")
955
- vid = gr.Video(height=270, autoplay=True, label="")
956
- lbl = gr.Markdown(
957
- f"*{default_seqs[si]}*" if default_seqs[si] else "*empty*")
958
- cmp_dds.append(dd)
959
- cmp_vids.append(vid)
960
- cmp_lbls.append(lbl)
961
-
962
- cmp_status = gr.Markdown("")
963
-
964
- cmp_outputs = []
965
- for v, l in zip(cmp_vids, cmp_lbls):
966
- cmp_outputs.append(v)
967
- cmp_outputs.append(l)
968
- cmp_outputs.append(cmp_status)
969
-
970
- def _load_all(*args):
971
- ov, a, fps = args[0], args[1], args[2]
972
- slots = list(args[3:])
973
- res = [None] * MAX_COMPARE
974
- lbs = [""] * MAX_COMPARE
975
-
976
- # Sequential — fast when pre-cached, safe in all environments
977
- for i, seq in enumerate(slots):
978
- if seq:
979
- try:
980
- p, _ = get_video(seq, ov, a, fps)
981
- res[i] = str(p) if p else None
982
- except Exception:
983
- res[i] = None
984
- lbs[i] = seq
985
-
986
- n_ok = sum(1 for r in res if r)
987
- out = []
988
- for r, l in zip(res, lbs):
989
- out.append(r)
990
- out.append(f"**{l}**" if l else "*empty*")
991
- out.append(f"✅ {n_ok}/{len([s for s in slots if s])} slots loaded")
992
- return out
993
-
994
- cmp_btn.click(_load_all,
995
- inputs=[cmp_ov, cmp_a, cmp_fps] + cmp_dds,
996
- outputs=cmp_outputs)
997
-
998
- for i, (dd, vid, lbl) in enumerate(zip(cmp_dds, cmp_vids, cmp_lbls)):
999
- def _mk(idx):
1000
- def _single(seq, ov, a, fps):
1001
- p, _ = get_video(seq, ov, a, fps)
1002
- return p, f"**{seq}**" if seq else "*empty*"
1003
- return _single
1004
- dd.change(_mk(i), [dd, cmp_ov, cmp_a, cmp_fps], [vid, lbl])
1005
 
1006
  # ──────────────────────────────────────────────────────────────
1007
- # Tab 6 · Statistics
1008
  # ──────────────────────────────────────────────────────────────
1009
  with gr.TabItem("📊 Statistics"):
1010
  gr.Markdown("### Dataset Overview")
@@ -1025,7 +844,7 @@ def build_ui():
1025
  """)
1026
 
1027
  # ──────────────────────────────────────────────────────────────
1028
- # Tab 7 · About
1029
  # ──────────────────────────────────────────────────────────────
1030
  with gr.TabItem("ℹ️ About"):
1031
  gr.Markdown(f"""
 
100
  DEFAULT_FPS = 24
101
  DEFAULT_ALPHA = 0.55
102
  DEFAULT_CRF = 18
103
+ THUMB_W, THUMB_H = 427, 240 # 16:9 thumbnails (half of 854×480 DAVIS frames)
 
 
104
 
105
  # ── Dataset download ───────────────────────────────────────────────────────────
106
 
 
512
  n_2017 = int(DF["in_2017"].sum())
513
  _first = ALL_SEQUENCES[0]
514
  _first_n = len(_get_frame_paths(_first))
 
 
515
  with gr.Blocks(title="DAVIS Dataset Explorer") as demo:
516
 
517
  gr.Markdown(
 
644
  btn_play.click(get_video, [seq_dd, v_ov, v_a, v_fps], [video_out, v_status])
645
 
646
  # ──────────────────────────────────────────────────────────────
647
+ # Tab 3 · Gallery (toggle up to 4 sequences — videos at top)
648
  # ──────────────────────────────────────────────────────────────
649
  with gr.TabItem("🖼 Gallery"):
650
+
651
+ # ── Video playback area (top) ──────────────────────────────
652
+ g_placeholder = gr.Markdown(
653
+ "### 🎬 Choose up to 4 sequences from the gallery below",
654
+ visible=True,
655
  )
656
+ g_sel_info = gr.Markdown("", visible=False)
657
+
658
+ with gr.Row():
659
+ g_vid_0 = gr.Video(visible=False, autoplay=True,
660
+ height=320, label="")
661
+ g_vid_1 = gr.Video(visible=False, autoplay=True,
662
+ height=320, label="")
663
+ with gr.Row():
664
+ g_vid_2 = gr.Video(visible=False, autoplay=True,
665
+ height=320, label="")
666
+ g_vid_3 = gr.Video(visible=False, autoplay=True,
667
+ height=320, label="")
668
+
669
+ with gr.Row():
670
+ g_clr_btn = gr.Button("✕ Clear selection", size="sm",
671
+ visible=False, scale=1)
672
+ gr.Markdown("", scale=4)
673
+
674
+ gr.Markdown("---")
675
+
676
+ # ── Filter + video options ─────────────────────────────────
677
  with gr.Row():
678
  g_year = gr.Dropdown(["All years","2016 only","2017 only"],
679
  value="All years", label="Year", scale=1)
 
684
  g_srch = gr.Textbox(placeholder="Search…", label="Search", scale=2)
685
  with gr.Row():
686
  g_fmin = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
687
+ int(DF["frames"].min()), step=1,
688
+ label="Min frames", scale=3)
689
  g_fmax = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
690
+ int(DF["frames"].max()), step=1,
691
+ label="Max frames", scale=3)
692
  with gr.Row():
693
+ g_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1, label="FPS", scale=2)
694
+ g_vid_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
695
+ g_vid_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
696
+ label="Opacity", scale=2)
697
+ g_ov_th = gr.Checkbox(value=False, label="Overlay on thumbnails",
698
+ scale=1)
699
+
700
+ g_count_md = gr.Markdown(
701
+ f"**{len(ALL_SEQUENCES)} sequences** — click thumbnails to toggle (max 4)"
702
+ )
703
 
704
+ # ── Gallery thumbnails (bottom) ────────────────────────────
705
  gallery = gr.Gallery(
706
  value=_ALL_THUMBS,
707
  label="Sequences",
708
+ columns=4,
709
  rows=None,
710
  height="auto",
711
  allow_preview=False,
712
  show_label=False,
713
+ object_fit="contain",
714
  )
715
 
716
+ # States
717
+ g_seq_state = gr.State(ALL_SEQUENCES.copy())
718
+ g_selected_state = gr.State([]) # list[str], max 4
719
 
720
+ # ── Filter → rebuild gallery ───────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  g_f_inputs = [g_year, g_split, g_obj, g_fmin, g_fmax, g_srch]
722
 
723
  def _on_g_filter(*args):
724
+ ov = args[-1]
725
+ fargs = args[:-1]
726
  fdf = filter_df(*fargs)
727
  seqs = fdf["sequence"].tolist()
728
  items = build_gallery_items(seqs, overlay=ov)
729
+ return (items, seqs,
730
+ f"**{len(seqs)} sequences** — click thumbnails to toggle (max 4)")
731
 
732
+ for inp in g_f_inputs + [g_ov_th]:
733
+ inp.change(_on_g_filter, g_f_inputs + [g_ov_th],
734
  [gallery, g_seq_state, g_count_md])
735
 
736
+ # ── Toggle helpers ─────────────────────────────────────────
737
+ def _build_video_updates(sel_seqs, ov, a, fps):
738
+ """Return 4 gr.update() objects for video slots 0-3."""
739
+ updates = []
740
+ for i in range(4):
741
+ if i < len(sel_seqs):
742
+ try:
743
+ p, _ = get_video(sel_seqs[i], ov, a, fps)
744
+ path = str(p) if p else None
745
+ except Exception:
746
+ path = None
747
+ updates.append(gr.update(
748
+ visible=True, value=path, label=sel_seqs[i]))
749
+ else:
750
+ updates.append(gr.update(visible=False, value=None))
751
+ return updates
752
+
753
+ # ── Gallery click → toggle selection ──────────────────────
754
+ def _on_gallery_toggle(evt: gr.SelectData,
755
+ sel_seqs, g_seqs, ov, a, fps):
756
+ if evt is None or not g_seqs:
757
+ return (sel_seqs,
758
+ gr.update(visible=True), gr.update(visible=False),
759
+ gr.update(visible=False, value=None),
760
+ gr.update(visible=False, value=None),
761
+ gr.update(visible=False, value=None),
762
+ gr.update(visible=False, value=None),
763
+ gr.update(visible=False))
764
+
765
+ seq = g_seqs[evt.index]
766
+ if seq in sel_seqs:
767
+ sel_seqs = [s for s in sel_seqs if s != seq]
768
+ elif len(sel_seqs) < 4:
769
+ sel_seqs = sel_seqs + [seq]
770
+ # else: already 4 selected — silently ignore
771
+
772
+ n = len(sel_seqs)
773
+ vid_updates = _build_video_updates(sel_seqs, ov, a, fps)
774
+ info_txt = ("▶ " +
775
+ " · ".join(f"**{s}**" for s in sel_seqs) +
776
+ " *(click a thumbnail to deselect)*") if n > 0 else ""
777
+ return (
778
+ sel_seqs,
779
+ gr.update(visible=(n == 0)), # placeholder
780
+ gr.update(visible=(n > 0), value=info_txt), # sel_info
781
+ vid_updates[0],
782
+ vid_updates[1],
783
+ vid_updates[2],
784
+ vid_updates[3],
785
+ gr.update(visible=(n > 0)), # clear button
786
+ )
787
 
788
  gallery.select(
789
+ _on_gallery_toggle,
790
+ inputs=[g_selected_state, g_seq_state, g_vid_ov, g_vid_a, g_fps],
791
+ outputs=[g_selected_state, g_placeholder, g_sel_info,
792
+ g_vid_0, g_vid_1, g_vid_2, g_vid_3, g_clr_btn],
 
 
 
 
793
  )
794
 
795
+ # Re-encode when overlay / FPS settings change
796
+ def _reload_settings(sel_seqs, ov, a, fps):
797
+ return _build_video_updates(sel_seqs, ov, a, fps)
 
 
 
 
 
 
798
 
799
+ for _inp in [g_vid_ov, g_vid_a, g_fps]:
800
+ _inp.change(
801
+ _reload_settings,
802
+ inputs=[g_selected_state, g_vid_ov, g_vid_a, g_fps],
803
+ outputs=[g_vid_0, g_vid_1, g_vid_2, g_vid_3],
 
 
 
 
804
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
806
+ # Clear selection button
807
+ def _clear_selection():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
  return (
809
+ [],
810
+ gr.update(visible=True),
811
+ gr.update(visible=False, value=""),
812
+ gr.update(visible=False, value=None),
813
+ gr.update(visible=False, value=None),
814
+ gr.update(visible=False, value=None),
815
+ gr.update(visible=False, value=None),
816
+ gr.update(visible=False),
817
  )
818
 
819
+ g_clr_btn.click(
820
+ _clear_selection,
821
+ outputs=[g_selected_state, g_placeholder, g_sel_info,
822
+ g_vid_0, g_vid_1, g_vid_2, g_vid_3, g_clr_btn],
 
 
 
 
 
 
 
 
 
 
 
 
823
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
 
825
  # ──────────────────────────────────────────────────────────────
826
+ # Tab 4 · Statistics
827
  # ──────────────────────────────────────────────────────────────
828
  with gr.TabItem("📊 Statistics"):
829
  gr.Markdown("### Dataset Overview")
 
844
  """)
845
 
846
  # ──────────────────────────────────────────────────────────────
847
+ # Tab 5 · About
848
  # ──────────────────────────────────────────────────────────────
849
  with gr.TabItem("ℹ️ About"):
850
  gr.Markdown(f"""