JS6969 commited on
Commit
f8d0d78
·
verified ·
1 Parent(s): f066549

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -189
app.py CHANGED
@@ -715,7 +715,90 @@ def _render_header_html(px: int) -> str:
715
 
716
 
717
  # ------------------------------
718
- # 11) UI (Blocks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  # ------------------------------
720
  BASE_CSS = """
721
  :root{--galleryW:50%;--tableW:50%;}
@@ -782,11 +865,15 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
782
  settings.get("caption_length", "long"),
783
  ),
784
  )
785
- dataset_name = gr.Textbox(label="Dataset name (export title prefix)",
786
- value=settings.get("dataset_name", "forgecaptions"))
787
- max_side = gr.Slider(256, 1024, settings.get("max_side", 896), step=32, label="Max side (resize)")
788
- excel_thumb_px = gr.Slider(64, 256, value=settings.get("excel_thumb_px", 128),
789
- step=8, label="Excel thumbnail size (px)")
 
 
 
 
790
  # Chunking
791
  chunk_mode = gr.Radio(
792
  choices=["Auto", "Manual (step)"],
@@ -824,6 +911,7 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
824
  cfg["dataset_name"] = sanitize_basename(name)
825
  save_settings(cfg)
826
  return gr.update()
 
827
  dataset_name.change(_save_dataset_name, inputs=[dataset_name], outputs=[])
828
 
829
  # ---- Shape Aliases (with plural matching + persist)
@@ -847,7 +935,7 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
847
  headers=["shape (token or synonyms)", "name to insert"],
848
  value=init_rows,
849
  row_count=(max(1, len(init_rows)), "dynamic"),
850
- datatype=["str","str"],
851
  type="array",
852
  interactive=True
853
  )
@@ -874,6 +962,7 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
874
  cfg["shape_aliases_persist"] = bool(v)
875
  save_settings(cfg)
876
  return gr.update()
 
877
  persist_aliases.change(_save_alias_persist_flag, inputs=[persist_aliases], outputs=[])
878
 
879
  save_btn.click(
@@ -905,7 +994,7 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
905
  run_button = gr.Button("Caption batch", variant="primary")
906
 
907
  # ---- Results area (gallery left / table right)
908
- rows_state = gr.State(load_session())
909
  autosave_md = gr.Markdown("Ready.")
910
  progress_md = gr.Markdown("")
911
  remaining_state = gr.State([])
@@ -933,43 +1022,35 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
933
  # ---- Step panel
934
  step_panel = gr.Group(visible=False)
935
  with step_panel:
936
- step_msg = gr.Markdown("")
937
- step_next = gr.Button("Process next chunk")
938
  step_finish = gr.Button("Finish")
939
 
940
  # ---- Exports
941
  with gr.Row():
942
  with gr.Column():
943
- export_csv_btn = gr.Button("Export CSV")
944
- csv_file = gr.File(label="CSV file", visible=False)
945
  with gr.Column():
946
  export_xlsx_btn = gr.Button("Export Excel (.xlsx) with thumbnails")
947
- xlsx_file = gr.File(label="Excel file", visible=False)
948
  with gr.Column():
949
- export_txt_btn = gr.Button("Export captions as .txt (zip)")
950
- txt_zip = gr.File(label="TXT zip", visible=False)
951
 
 
952
  gr.HTML("""
953
  <script>
954
  (() => {
955
- // ---- Config: your wrapper IDs
956
- const GAL_WRAP_SEL = "#cfGal"; // wrapper around the gallery
957
- const TABLE_WRAP_SEL = "#cfTableWrap"; // wrapper around your table
958
-
959
- // ---- Helpers
960
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
961
-
962
  function findGalleryHost() {
963
  const wrap = document.querySelector(GAL_WRAP_SEL);
964
  if (!wrap) return null;
965
- // Prefer explicit gallery node if present; otherwise use wrapper
966
  return wrap.querySelector('[data-testid="gallery"], [data-testid="image-gallery"]') || wrap;
967
  }
968
-
969
- function findTableHost() {
970
- return document.querySelector(TABLE_WRAP_SEL);
971
- }
972
-
973
  function setMaxHeights(gal, tab) {
974
  const targetH = clamp(tab.clientHeight || 520, 360, 520);
975
  gal.style.maxHeight = targetH + "px";
@@ -977,202 +1058,74 @@ with gr.Blocks(css=BASE_CSS, title="ForgeCaptions") as demo:
977
  tab.style.maxHeight = targetH + "px";
978
  tab.style.overflowY = "auto";
979
  }
980
-
981
  function attachScrollSync(a, b) {
982
  if (!a || !b) return () => {};
983
  let lock = false;
984
-
985
- // Use scroll ratio so different scrollHeights stay aligned
986
  const sync = (src, dst) => {
987
  const maxSrc = Math.max(1, src.scrollHeight - src.clientHeight);
988
  const r = src.scrollTop / maxSrc;
989
  const maxDst = Math.max(1, dst.scrollHeight - dst.clientHeight);
990
  const next = r * maxDst;
991
- if (Math.abs(dst.scrollTop - next) > 1) {
992
- dst.scrollTop = next;
993
- }
994
  };
995
-
996
  const onA = () => { if (lock) return; lock = true; requestAnimationFrame(() => { sync(a, b); lock = false; }); };
997
  const onB = () => { if (lock) return; lock = true; requestAnimationFrame(() => { sync(b, a); lock = false; }); };
998
-
999
  a.addEventListener("scroll", onA, { passive: true });
1000
  b.addEventListener("scroll", onB, { passive: true });
1001
-
1002
- // Return cleanup
1003
- return () => {
1004
- a.removeEventListener("scroll", onA);
1005
- b.removeEventListener("scroll", onB);
1006
- };
1007
  }
1008
-
1009
- let cleanupScroll = null;
1010
- let resizeObs = null;
1011
-
1012
  function wireUp() {
1013
- const gal = findGalleryHost();
1014
- const tab = findTableHost();
1015
  if (!gal || !tab) return false;
1016
-
1017
- // (Re)apply heights & listeners
1018
  setMaxHeights(gal, tab);
1019
  if (cleanupScroll) cleanupScroll();
1020
  cleanupScroll = attachScrollSync(gal, tab);
1021
-
1022
- // Keep heights in sync on resize/content changes
1023
  if (resizeObs) resizeObs.disconnect();
1024
  resizeObs = new ResizeObserver(() => setMaxHeights(gal, tab));
1025
- resizeObs.observe(tab);
1026
- resizeObs.observe(gal);
1027
-
1028
  return true;
1029
  }
1030
-
1031
- // First attempt immediately (fast path)
1032
  if (wireUp()) return;
1033
-
1034
- // Observe DOM for late mounts / Gradio re-renders
1035
- const mo = new MutationObserver(() => {
1036
- if (wireUp()) {
1037
- // Once wired, continue watching for *re-renders* to re-wire
1038
- // If you prefer, remove this `disconnect()` to keep watching forever
1039
- // but then you should also detect detach and clean up.
1040
- // Here we keep observing to handle re-renders:
1041
- }
1042
- });
1043
-
1044
- mo.observe(document.documentElement || document.body, {
1045
- childList: true,
1046
- subtree: true,
1047
- });
1048
-
1049
- // Optional: cleanup on page unload
1050
  window.addEventListener("beforeunload", () => {
1051
- mo.disconnect();
1052
- if (resizeObs) resizeObs.disconnect();
1053
- if (cleanupScroll) cleanupScroll();
1054
  });
1055
  })();
1056
- </script> """)
1057
-
1058
-
1059
- # ---- Chunking logic
1060
- def _split_chunks(files, csize: int):
1061
- files = files or []
1062
- c = max(1, int(csize))
1063
- return [files[i:i + c] for i in range(0, len(files), c)]
1064
-
1065
-
1066
- def _tpms():
1067
- s = load_settings()
1068
- return s.get("temperature", 0.6), s.get("top_p", 0.9), s.get("max_tokens", 256)
1069
-
1070
-
1071
- def _run_click(files, rows, instr, ms, mode, csize, budget_s, no_limit):
1072
- t, p, m = _tpms()
1073
- files = files or []
1074
- budget = None if no_limit else float(budget_s)
1075
-
1076
- if mode == "Manual (step)" and files:
1077
- chunks = _split_chunks(files, int(csize))
1078
- batch = chunks[0]
1079
- remaining = sum(chunks[1:], [])
1080
- new_rows, gal, tbl, stamp, leftover_from_batch, done, total = run_batch(
1081
- batch, rows or [], instr, t, p, m, int(ms), budget
1082
- )
1083
- remaining = (leftover_from_batch or []) + remaining
1084
- panel_vis = gr.update(visible=bool(remaining))
1085
- msg = f"{len(remaining)} files remain. Process next chunk?"
1086
- prog = f"Batch progress: {done}/{total} processed in this step • Remaining overall: {len(remaining)}"
1087
- return new_rows, gal, tbl, stamp, remaining, panel_vis, gr.update(value=msg), gr.update(value=prog)
1088
-
1089
- # Auto
1090
- new_rows, gal, tbl, stamp, leftover, done, total = run_batch(
1091
- files, rows or [], instr, t, p, m, int(ms), budget
1092
  )
1093
- panel_vis = gr.update(visible=bool(leftover))
1094
- msg = f"{len(leftover)} files remain. Process next chunk?" if leftover else ""
1095
- prog = f"Batch progress: {done}/{total} processed in this call • Remaining: {len(leftover)}"
1096
- return new_rows, gal, tbl, stamp, leftover, panel_vis, gr.update(value=msg), gr.update(value=prog)
1097
-
1098
-
1099
- run_button.click(
1100
- _run_click,
1101
- inputs=[input_files, rows_state, instruction_preview, max_side, chunk_mode, chunk_size, gpu_budget, no_time_limit],
1102
- outputs=[rows_state, gallery, table, autosave_md, remaining_state, step_panel, step_msg, progress_md]
1103
- )
1104
-
1105
-
1106
- def _step_next(remain, rows, instr, ms, csize, budget_s, no_limit):
1107
- t, p, m = _tpms()
1108
- remain = remain or []
1109
- budget = None if no_limit else float(budget_s)
1110
 
1111
- if not remain:
1112
- return (
1113
- rows,
1114
- gr.update(value="No files remaining."),
1115
- gr.update(visible=False),
1116
- [],
1117
- [],
1118
- [],
1119
- "Saved.",
1120
- gr.update(value="")
1121
- )
1122
-
1123
- batch = remain[:int(csize)]
1124
- leftover = remain[int(csize):]
1125
- new_rows, gal, tbl, stamp, leftover_from_batch, done, total = run_batch(
1126
- batch, rows or [], instr, t, p, m, int(ms), budget
1127
  )
1128
- leftover = (leftover_from_batch or []) + leftover
1129
- panel_vis = gr.update(visible=bool(leftover))
1130
- msg = f"{len(leftover)} files remain. Process next chunk?" if leftover else "All done."
1131
- prog = f"Batch progress: {done}/{total} processed in this step • Remaining overall: {len(leftover)}"
1132
- return new_rows, msg, panel_vis, leftover, gal, tbl, stamp, gr.update(value=prog)
1133
-
1134
-
1135
- step_next.click(
1136
- _step_next,
1137
- inputs=[remaining_state, rows_state, instruction_preview, max_side, chunk_size, gpu_budget, no_time_limit],
1138
- outputs=[rows_state, step_msg, step_panel, remaining_state, gallery, table, autosave_md, progress_md]
1139
- )
1140
-
1141
-
1142
- def _step_finish():
1143
- return gr.update(visible=False), gr.update(value=""), []
1144
-
1145
 
1146
- step_finish.click(_step_finish, inputs=None, outputs=[step_panel, step_msg, remaining_state])
1147
 
 
1148
 
1149
- # ---- Table edits → persist + refresh gallery
1150
- def sync_table_to_session(table_value: Any, session_rows: List[dict]) -> Tuple[List[dict], list, str]:
1151
- session_rows = _table_to_rows(table_value, session_rows or [])
1152
- save_session(session_rows)
1153
- gallery_pairs = [((r.get("thumb_path") or r.get("path")), r.get("caption", ""))
1154
- for r in session_rows if (r.get("thumb_path") or r.get("path"))]
1155
- return session_rows, gallery_pairs, f"Saved {time.strftime('%H:%M:%S')}"
1156
-
1157
-
1158
- table.change(sync_table_to_session, inputs=[table, rows_state], outputs=[rows_state, gallery, autosave_md])
1159
-
1160
-
1161
- # ---- Exports
1162
- export_csv_btn.click(
1163
- lambda tbl, ds: (export_csv_from_table(tbl, ds), gr.update(visible=True)),
1164
- inputs=[table, dataset_name], outputs=[csv_file, csv_file]
1165
- )
1166
-
1167
- export_xlsx_btn.click(
1168
- lambda tbl, rows, px, ds: (export_excel_with_thumbs(tbl, rows or [], int(px), ds), gr.update(visible=True)),
1169
- inputs=[table, rows_state, excel_thumb_px, dataset_name], outputs=[xlsx_file, xlsx_file]
1170
- )
1171
 
1172
- export_txt_btn.click(
1173
- lambda tbl, ds: (export_txt_zip(tbl, ds), gr.update(visible=True)),
1174
- inputs=[table, dataset_name], outputs=[txt_zip, txt_zip]
1175
- )
1176
 
1177
 
1178
 
 
715
 
716
 
717
  # ------------------------------
718
+ # 11) Handlers (defined before UI)
719
+ # ------------------------------
720
+ def _split_chunks(files, csize: int):
721
+ files = files or []
722
+ c = max(1, int(csize))
723
+ return [files[i:i + c] for i in range(0, len(files), c)]
724
+
725
+
726
+ def _tpms():
727
+ s = load_settings()
728
+ return s.get("temperature", 0.6), s.get("top_p", 0.9), s.get("max_tokens", 256)
729
+
730
+
731
+ def _run_click(files, rows, instr, ms, mode, csize, budget_s, no_limit):
732
+ t, p, m = _tpms()
733
+ files = files or []
734
+ budget = None if no_limit else float(budget_s)
735
+
736
+ if mode == "Manual (step)" and files:
737
+ chunks = _split_chunks(files, int(csize))
738
+ batch = chunks[0]
739
+ remaining = sum(chunks[1:], [])
740
+ new_rows, gal, tbl, stamp, leftover_from_batch, done, total = run_batch(
741
+ batch, rows or [], instr, t, p, m, int(ms), budget
742
+ )
743
+ remaining = (leftover_from_batch or []) + remaining
744
+ panel_vis = gr.update(visible=bool(remaining))
745
+ msg = f"{len(remaining)} files remain. Process next chunk?"
746
+ prog = f"Batch progress: {done}/{total} processed in this step • Remaining overall: {len(remaining)}"
747
+ return new_rows, gal, tbl, stamp, remaining, panel_vis, gr.update(value=msg), gr.update(value=prog)
748
+
749
+ # Auto
750
+ new_rows, gal, tbl, stamp, leftover, done, total = run_batch(
751
+ files, rows or [], instr, t, p, m, int(ms), budget
752
+ )
753
+ panel_vis = gr.update(visible=bool(leftover))
754
+ msg = f"{len(leftover)} files remain. Process next chunk?" if leftover else ""
755
+ prog = f"Batch progress: {done}/{total} processed in this call • Remaining: {len(leftover)}"
756
+ return new_rows, gal, tbl, stamp, leftover, panel_vis, gr.update(value=msg), gr.update(value=prog)
757
+
758
+
759
+ def _step_next(remain, rows, instr, ms, csize, budget_s, no_limit):
760
+ t, p, m = _tpms()
761
+ remain = remain or []
762
+ budget = None if no_limit else float(budget_s)
763
+
764
+ if not remain:
765
+ return (
766
+ rows,
767
+ gr.update(value="No files remaining."),
768
+ gr.update(visible=False),
769
+ [],
770
+ [],
771
+ [],
772
+ "Saved.",
773
+ gr.update(value="")
774
+ )
775
+
776
+ batch = remain[:int(csize)]
777
+ leftover = remain[int(csize):]
778
+ new_rows, gal, tbl, stamp, leftover_from_batch, done, total = run_batch(
779
+ batch, rows or [], instr, t, p, m, int(ms), budget
780
+ )
781
+ leftover = (leftover_from_batch or []) + leftover
782
+ panel_vis = gr.update(visible=bool(leftover))
783
+ msg = f"{len(leftover)} files remain. Process next chunk?" if leftover else "All done."
784
+ prog = f"Batch progress: {done}/{total} processed in this step • Remaining overall: {len(leftover)}"
785
+ return new_rows, msg, panel_vis, leftover, gal, tbl, stamp, gr.update(value=prog)
786
+
787
+
788
+ def _step_finish():
789
+ return gr.update(visible=False), gr.update(value=""), []
790
+
791
+
792
+ def sync_table_to_session(table_value: Any, session_rows: List[dict]) -> Tuple[List[dict], list, str]:
793
+ session_rows = _table_to_rows(table_value, session_rows or [])
794
+ save_session(session_rows)
795
+ gallery_pairs = [((r.get("thumb_path") or r.get("path")), r.get("caption", ""))
796
+ for r in session_rows if (r.get("thumb_path") or r.get("path"))]
797
+ return session_rows, gallery_pairs, f"Saved • {time.strftime('%H:%M:%S')}"
798
+
799
+
800
+ # ------------------------------
801
+ # 12) UI (Blocks)
802
  # ------------------------------
803
  BASE_CSS = """
804
  :root{--galleryW:50%;--tableW:50%;}
 
865
  settings.get("caption_length", "long"),
866
  ),
867
  )
868
+ dataset_name = gr.Textbox(
869
+ label="Dataset name (export title prefix)",
870
+ value=settings.get("dataset_name", "forgecaptions")
871
+ )
872
+ max_side = gr.Slider(256, 1024, settings.get("max_side", 896), step=32, label="Max side (resize)")
873
+ excel_thumb_px = gr.Slider(
874
+ 64, 256, value=settings.get("excel_thumb_px", 128),
875
+ step=8, label="Excel thumbnail size (px)"
876
+ )
877
  # Chunking
878
  chunk_mode = gr.Radio(
879
  choices=["Auto", "Manual (step)"],
 
911
  cfg["dataset_name"] = sanitize_basename(name)
912
  save_settings(cfg)
913
  return gr.update()
914
+
915
  dataset_name.change(_save_dataset_name, inputs=[dataset_name], outputs=[])
916
 
917
  # ---- Shape Aliases (with plural matching + persist)
 
935
  headers=["shape (token or synonyms)", "name to insert"],
936
  value=init_rows,
937
  row_count=(max(1, len(init_rows)), "dynamic"),
938
+ datatype=["str", "str"],
939
  type="array",
940
  interactive=True
941
  )
 
962
  cfg["shape_aliases_persist"] = bool(v)
963
  save_settings(cfg)
964
  return gr.update()
965
+
966
  persist_aliases.change(_save_alias_persist_flag, inputs=[persist_aliases], outputs=[])
967
 
968
  save_btn.click(
 
994
  run_button = gr.Button("Caption batch", variant="primary")
995
 
996
  # ---- Results area (gallery left / table right)
997
+ rows_state = gr.State(load_session())
998
  autosave_md = gr.Markdown("Ready.")
999
  progress_md = gr.Markdown("")
1000
  remaining_state = gr.State([])
 
1022
  # ---- Step panel
1023
  step_panel = gr.Group(visible=False)
1024
  with step_panel:
1025
+ step_msg = gr.Markdown("")
1026
+ step_next = gr.Button("Process next chunk")
1027
  step_finish = gr.Button("Finish")
1028
 
1029
  # ---- Exports
1030
  with gr.Row():
1031
  with gr.Column():
1032
+ export_csv_btn = gr.Button("Export CSV")
1033
+ csv_file = gr.File(label="CSV file", visible=False)
1034
  with gr.Column():
1035
  export_xlsx_btn = gr.Button("Export Excel (.xlsx) with thumbnails")
1036
+ xlsx_file = gr.File(label="Excel file", visible=False)
1037
  with gr.Column():
1038
+ export_txt_btn = gr.Button("Export captions as .txt (zip)")
1039
+ txt_zip = gr.File(label="TXT zip", visible=False)
1040
 
1041
+ # ---- Scroll-sync JS injection (inside Blocks)
1042
  gr.HTML("""
1043
  <script>
1044
  (() => {
1045
+ const GAL_WRAP_SEL = "#cfGal";
1046
+ const TABLE_WRAP_SEL = "#cfTableWrap";
 
 
 
1047
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
 
1048
  function findGalleryHost() {
1049
  const wrap = document.querySelector(GAL_WRAP_SEL);
1050
  if (!wrap) return null;
 
1051
  return wrap.querySelector('[data-testid="gallery"], [data-testid="image-gallery"]') || wrap;
1052
  }
1053
+ function findTableHost() { return document.querySelector(TABLE_WRAP_SEL); }
 
 
 
 
1054
  function setMaxHeights(gal, tab) {
1055
  const targetH = clamp(tab.clientHeight || 520, 360, 520);
1056
  gal.style.maxHeight = targetH + "px";
 
1058
  tab.style.maxHeight = targetH + "px";
1059
  tab.style.overflowY = "auto";
1060
  }
 
1061
  function attachScrollSync(a, b) {
1062
  if (!a || !b) return () => {};
1063
  let lock = false;
 
 
1064
  const sync = (src, dst) => {
1065
  const maxSrc = Math.max(1, src.scrollHeight - src.clientHeight);
1066
  const r = src.scrollTop / maxSrc;
1067
  const maxDst = Math.max(1, dst.scrollHeight - dst.clientHeight);
1068
  const next = r * maxDst;
1069
+ if (Math.abs(dst.scrollTop - next) > 1) dst.scrollTop = next;
 
 
1070
  };
 
1071
  const onA = () => { if (lock) return; lock = true; requestAnimationFrame(() => { sync(a, b); lock = false; }); };
1072
  const onB = () => { if (lock) return; lock = true; requestAnimationFrame(() => { sync(b, a); lock = false; }); };
 
1073
  a.addEventListener("scroll", onA, { passive: true });
1074
  b.addEventListener("scroll", onB, { passive: true });
1075
+ return () => { a.removeEventListener("scroll", onA); b.removeEventListener("scroll", onB); };
 
 
 
 
 
1076
  }
1077
+ let cleanupScroll = null, resizeObs = null;
 
 
 
1078
  function wireUp() {
1079
+ const gal = findGalleryHost(), tab = findTableHost();
 
1080
  if (!gal || !tab) return false;
 
 
1081
  setMaxHeights(gal, tab);
1082
  if (cleanupScroll) cleanupScroll();
1083
  cleanupScroll = attachScrollSync(gal, tab);
 
 
1084
  if (resizeObs) resizeObs.disconnect();
1085
  resizeObs = new ResizeObserver(() => setMaxHeights(gal, tab));
1086
+ resizeObs.observe(tab); resizeObs.observe(gal);
 
 
1087
  return true;
1088
  }
 
 
1089
  if (wireUp()) return;
1090
+ const mo = new MutationObserver(() => { wireUp(); });
1091
+ mo.observe(document.documentElement || document.body, { childList: true, subtree: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1092
  window.addEventListener("beforeunload", () => {
1093
+ mo.disconnect(); if (resizeObs) resizeObs.disconnect(); if (cleanupScroll) cleanupScroll();
 
 
1094
  });
1095
  })();
1096
+ </script>
1097
+ """)
1098
+
1099
+ # ---- Event bindings (MUST be inside Blocks in Gradio v5)
1100
+ run_button.click(
1101
+ _run_click,
1102
+ inputs=[input_files, rows_state, instruction_preview, max_side, chunk_mode, chunk_size, gpu_budget, no_time_limit],
1103
+ outputs=[rows_state, gallery, table, autosave_md, remaining_state, step_panel, step_msg, progress_md]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
 
1106
+ step_next.click(
1107
+ _step_next,
1108
+ inputs=[remaining_state, rows_state, instruction_preview, max_side, chunk_size, gpu_budget, no_time_limit],
1109
+ outputs=[rows_state, step_msg, step_panel, remaining_state, gallery, table, autosave_md, progress_md]
 
 
 
 
 
 
 
 
 
 
 
 
1110
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1111
 
1112
+ step_finish.click(_step_finish, inputs=None, outputs=[step_panel, step_msg, remaining_state])
1113
 
1114
+ table.change(sync_table_to_session, inputs=[table, rows_state], outputs=[rows_state, gallery, autosave_md])
1115
 
1116
+ export_csv_btn.click(
1117
+ lambda tbl, ds: (export_csv_from_table(tbl, ds), gr.update(visible=True)),
1118
+ inputs=[table, dataset_name], outputs=[csv_file, csv_file]
1119
+ )
1120
+ export_xlsx_btn.click(
1121
+ lambda tbl, rows, px, ds: (export_excel_with_thumbs(tbl, rows or [], int(px), ds), gr.update(visible=True)),
1122
+ inputs=[table, rows_state, excel_thumb_px, dataset_name], outputs=[xlsx_file, xlsx_file]
1123
+ )
1124
+ export_txt_btn.click(
1125
+ lambda tbl, ds: (export_txt_zip(tbl, ds), gr.update(visible=True)),
1126
+ inputs=[table, dataset_name], outputs=[txt_zip, txt_zip]
1127
+ )
 
 
 
 
 
 
 
 
 
 
1128
 
 
 
 
 
1129
 
1130
 
1131