Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -715,7 +715,90 @@ def _render_header_html(px: int) -> str:
|
|
| 715 |
|
| 716 |
|
| 717 |
# ------------------------------
|
| 718 |
-
# 11)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 937 |
-
step_next
|
| 938 |
step_finish = gr.Button("Finish")
|
| 939 |
|
| 940 |
# ---- Exports
|
| 941 |
with gr.Row():
|
| 942 |
with gr.Column():
|
| 943 |
-
export_csv_btn
|
| 944 |
-
csv_file
|
| 945 |
with gr.Column():
|
| 946 |
export_xlsx_btn = gr.Button("Export Excel (.xlsx) with thumbnails")
|
| 947 |
-
xlsx_file
|
| 948 |
with gr.Column():
|
| 949 |
-
export_txt_btn
|
| 950 |
-
txt_zip
|
| 951 |
|
|
|
|
| 952 |
gr.HTML("""
|
| 953 |
<script>
|
| 954 |
(() => {
|
| 955 |
-
|
| 956 |
-
const
|
| 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 |
-
|
| 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 |
-
# ----
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 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 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 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 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 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 |
|