Spaces:
Sleeping
Sleeping
Claude commited on
Commit ·
b1608ed
1
Parent(s): ec14162
feat: single-button pipeline — Register + OCR + Review in one click
Browse files- smoke_signal_tab.py +153 -47
smoke_signal_tab.py
CHANGED
|
@@ -1867,6 +1867,106 @@ def _run_tesseract_batch(
|
|
| 1867 |
return outputs
|
| 1868 |
|
| 1869 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1870 |
def run_ocr(page_selection: str = "", page_exclusion: str = "", replace_book_queue: bool = True) -> tuple:
|
| 1871 |
"""Run Surya OCR on all profiled PDFs."""
|
| 1872 |
df = load_manifest_df()
|
|
@@ -2714,14 +2814,7 @@ def smoke_signal_tab():
|
|
| 2714 |
gr.HTML("""<div class="ss-panel-header" style="padding:20px 0 0 0">
|
| 2715 |
<div class="ss-panel-icon">📥</div>
|
| 2716 |
<div><p class="ss-panel-title">Source Registry</p>
|
| 2717 |
-
<p class="ss-panel-sub">
|
| 2718 |
-
</div>
|
| 2719 |
-
<div style="background:#1a2535;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin:12px 0;font-size:13px;color:#8b949e;line-height:2.0">
|
| 2720 |
-
<b style="color:#e6edf3">▶ How to run OCR — follow this order:</b><br>
|
| 2721 |
-
<span style="color:#3fb950">1.</span> Drop your PDF · set <b style="color:#e6edf3">Rights Class</b> · enter <b style="color:#e6edf3">Book Code</b> + <b style="color:#e6edf3">Year</b> · click <b style="color:#f0883e">Register + Auto-Profile</b><br>
|
| 2722 |
-
<span style="color:#3fb950">2.</span> Enter <b style="color:#e6edf3">Story Pages Include</b> (e.g. 7-27) and <b style="color:#e6edf3">Story Pages Exclude</b> (e.g. 1,2,3) · click <b style="color:#f0883e">Save Book Pages</b><br>
|
| 2723 |
-
<span style="color:#3fb950">3.</span> Go to tab <b style="color:#e6edf3">③ OCR</b> · click <b style="color:#f0883e">Run OCR</b> · wait for the log to complete (may take 1-2 min)<br>
|
| 2724 |
-
<span style="color:#3fb950">4.</span> Go to tab <b style="color:#e6edf3">④ Review</b> · click <b style="color:#f0883e">↻ Load Review Queue</b> · accept / edit / reject pages
|
| 2725 |
</div>""")
|
| 2726 |
|
| 2727 |
ingest_status = gr.HTML(_ingest_status_html("idle"))
|
|
@@ -2729,14 +2822,14 @@ def smoke_signal_tab():
|
|
| 2729 |
with gr.Row():
|
| 2730 |
with gr.Column(scale=2):
|
| 2731 |
pdf_upload = gr.File(
|
| 2732 |
-
label="Drop
|
| 2733 |
file_types=[".pdf"],
|
| 2734 |
file_count="multiple",
|
| 2735 |
type="filepath",
|
| 2736 |
)
|
| 2737 |
with gr.Column(scale=1):
|
| 2738 |
rights_dd = gr.Dropdown(
|
| 2739 |
-
label="Rights Class",
|
| 2740 |
choices=["public-domain","licensed-owned","controlled-internal","unknown"],
|
| 2741 |
value="unknown",
|
| 2742 |
)
|
|
@@ -2751,47 +2844,45 @@ def smoke_signal_tab():
|
|
| 2751 |
lines=1,
|
| 2752 |
)
|
| 2753 |
ingest_notes = gr.Textbox(label="Notes", placeholder="Source, edition, etc.", lines=2)
|
| 2754 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2755 |
|
| 2756 |
manifest_table = gr.DataFrame(
|
| 2757 |
label="Source Manifest",
|
| 2758 |
interactive=False,
|
| 2759 |
wrap=True,
|
| 2760 |
)
|
| 2761 |
-
ingest_log = gr.Textbox(label="Log", lines=6, interactive=False, elem_classes=["ss-log"])
|
| 2762 |
-
ingest_copy_log_btn = gr.Button("Copy Log", size="sm")
|
| 2763 |
refresh_btn = gr.Button("↻ Refresh Manifest", size="sm")
|
| 2764 |
-
refresh_btn.click(lambda: load_manifest_df(), outputs=[manifest_table])
|
| 2765 |
-
ingest_copy_log_btn.click(
|
| 2766 |
-
fn=None,
|
| 2767 |
-
inputs=[ingest_log],
|
| 2768 |
-
outputs=[],
|
| 2769 |
-
js="(logTxt) => { if (navigator && navigator.clipboard) { navigator.clipboard.writeText(logTxt || ''); } return []; }",
|
| 2770 |
-
)
|
| 2771 |
|
| 2772 |
-
|
| 2773 |
-
|
| 2774 |
-
|
| 2775 |
-
|
| 2776 |
-
|
| 2777 |
-
|
| 2778 |
-
value="licensed-owned",
|
| 2779 |
-
scale=1,
|
| 2780 |
-
)
|
| 2781 |
-
update_rights_btn = gr.Button("Update Rights", size="sm", scale=1)
|
| 2782 |
-
update_rights_btn.click(
|
| 2783 |
-
update_rights,
|
| 2784 |
-
inputs=[update_book_id, update_rights_dd],
|
| 2785 |
-
outputs=[ingest_status, manifest_table, ingest_log],
|
| 2786 |
-
)
|
| 2787 |
-
|
| 2788 |
-
gr.Markdown("**Save Book Pages + Safe Title (persisted):**")
|
| 2789 |
-
with gr.Row():
|
| 2790 |
-
scope_book_id = gr.Textbox(label="Book ID (optional)", placeholder="grf1999 or leave blank to use latest", scale=1)
|
| 2791 |
-
scope_include = gr.Textbox(label="Story Pages Include", placeholder="all or 7-27 or 7,8,9,11-27", scale=1)
|
| 2792 |
-
scope_exclude = gr.Textbox(label="Story Pages Exclude", placeholder="e.g. 1,2,3,17,25", scale=1)
|
| 2793 |
-
scope_title = gr.Textbox(label="Safe Book Title", placeholder="e.g. the-gruffalo", scale=1)
|
| 2794 |
-
scope_save_btn = gr.Button("Save Book Pages →", size="sm")
|
| 2795 |
|
| 2796 |
# ── Auto-populate: book code + year → both book ID fields ──────
|
| 2797 |
def _make_book_id(code, year):
|
|
@@ -2829,10 +2920,24 @@ def smoke_signal_tab():
|
|
| 2829 |
outputs=[scope_title],
|
| 2830 |
)
|
| 2831 |
|
| 2832 |
-
|
| 2833 |
-
|
| 2834 |
-
|
| 2835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2836 |
)
|
| 2837 |
gr.HTML('<div style="height:16px"></div>')
|
| 2838 |
# Auto-load removed — use Refresh button instead to avoid SSR hang
|
|
@@ -3037,6 +3142,7 @@ def smoke_signal_tab():
|
|
| 3037 |
)
|
| 3038 |
review_tab.select(load_review, outputs=review_load_outputs)
|
| 3039 |
ocr_done.change(load_review, outputs=review_load_outputs)
|
|
|
|
| 3040 |
|
| 3041 |
accept_btn.click(do_accept, inputs=[current_idx, final_text_box, reviewer_name, reason_code], outputs=action_outputs)
|
| 3042 |
edit_btn.click(do_accept, inputs=[current_idx, final_text_box, reviewer_name, reason_code], outputs=action_outputs)
|
|
|
|
| 1867 |
return outputs
|
| 1868 |
|
| 1869 |
|
| 1870 |
+
def run_full_pipeline(
|
| 1871 |
+
files,
|
| 1872 |
+
rights_class: str,
|
| 1873 |
+
notes: str,
|
| 1874 |
+
book_code_hint: str,
|
| 1875 |
+
publication_year: str,
|
| 1876 |
+
scope_include: str,
|
| 1877 |
+
scope_exclude: str,
|
| 1878 |
+
safe_title: str,
|
| 1879 |
+
):
|
| 1880 |
+
"""
|
| 1881 |
+
Single-button full pipeline:
|
| 1882 |
+
1. Ingest + profile PDF
|
| 1883 |
+
2. Update rights + save page scope
|
| 1884 |
+
3. Run OCR
|
| 1885 |
+
Yields log updates throughout so UI stays live.
|
| 1886 |
+
"""
|
| 1887 |
+
log = []
|
| 1888 |
+
|
| 1889 |
+
def _line(msg):
|
| 1890 |
+
from datetime import datetime
|
| 1891 |
+
return f"[{datetime.now().strftime('%H:%M:%S')}] {msg}"
|
| 1892 |
+
|
| 1893 |
+
# Step 1: Ingest + profile
|
| 1894 |
+
log.append(_line("① Registering PDF..."))
|
| 1895 |
+
yield _ingest_status_html("idle"), "\n".join(log), 0
|
| 1896 |
+
|
| 1897 |
+
try:
|
| 1898 |
+
status_html, manifest_df, ingest_log = ingest_pdfs(
|
| 1899 |
+
files, rights_class, notes, book_code_hint, publication_year
|
| 1900 |
+
)
|
| 1901 |
+
for line in str(ingest_log).splitlines():
|
| 1902 |
+
if line.strip():
|
| 1903 |
+
log.append(line)
|
| 1904 |
+
except Exception as e:
|
| 1905 |
+
log.append(_line(f"✗ Ingest failed: {e}"))
|
| 1906 |
+
yield _ingest_status_html("idle"), "\n".join(log), 0
|
| 1907 |
+
return
|
| 1908 |
+
|
| 1909 |
+
yield status_html, "\n".join(log), 0
|
| 1910 |
+
|
| 1911 |
+
# Step 2: Save page scope + rights (resolve book_id from manifest)
|
| 1912 |
+
df = load_manifest_df()
|
| 1913 |
+
if df.empty:
|
| 1914 |
+
log.append(_line("✗ No books in manifest after ingest"))
|
| 1915 |
+
yield _ingest_status_html("idle"), "\n".join(log), 0
|
| 1916 |
+
return
|
| 1917 |
+
|
| 1918 |
+
# Get the book we just registered/updated
|
| 1919 |
+
latest_book_id = str(df.iloc[-1]["book_id"])
|
| 1920 |
+
|
| 1921 |
+
# Update rights if not unknown
|
| 1922 |
+
if rights_class and rights_class != "unknown":
|
| 1923 |
+
try:
|
| 1924 |
+
_, _, rights_log = update_rights(latest_book_id, rights_class)
|
| 1925 |
+
for line in str(rights_log).splitlines():
|
| 1926 |
+
if line.strip() and "not found" not in line.lower():
|
| 1927 |
+
log.append(line)
|
| 1928 |
+
except Exception as e:
|
| 1929 |
+
log.append(_line(f"⚠ Rights update: {e}"))
|
| 1930 |
+
|
| 1931 |
+
# Save page scope
|
| 1932 |
+
if scope_include or scope_exclude or safe_title:
|
| 1933 |
+
log.append(_line(f"② Saving page scope for {latest_book_id}..."))
|
| 1934 |
+
yield _ingest_status_html("done"), "\n".join(log), 0
|
| 1935 |
+
try:
|
| 1936 |
+
_, _, scope_log = save_book_scope(
|
| 1937 |
+
latest_book_id, scope_include, scope_exclude, safe_title
|
| 1938 |
+
)
|
| 1939 |
+
for line in str(scope_log).splitlines():
|
| 1940 |
+
if line.strip():
|
| 1941 |
+
log.append(line)
|
| 1942 |
+
except Exception as e:
|
| 1943 |
+
log.append(_line(f"⚠ Scope save: {e}"))
|
| 1944 |
+
|
| 1945 |
+
yield _ingest_status_html("done"), "\n".join(log), 0
|
| 1946 |
+
|
| 1947 |
+
# Step 3: Run OCR
|
| 1948 |
+
log.append(_line("③ Starting OCR..."))
|
| 1949 |
+
yield _ingest_status_html("done"), "\n".join(log), 0
|
| 1950 |
+
|
| 1951 |
+
try:
|
| 1952 |
+
ocr_gen = run_ocr(scope_include, scope_exclude, replace_book_queue=True)
|
| 1953 |
+
for ocr_status_html, ocr_log in ocr_gen:
|
| 1954 |
+
for line in str(ocr_log).splitlines():
|
| 1955 |
+
if line.strip() and line not in log:
|
| 1956 |
+
log.append(line)
|
| 1957 |
+
yield _ingest_status_html("done"), "\n".join(log), 0
|
| 1958 |
+
except Exception as e:
|
| 1959 |
+
import traceback
|
| 1960 |
+
log.append(_line(f"✗ OCR error: {e}"))
|
| 1961 |
+
log.append(traceback.format_exc())
|
| 1962 |
+
yield _ingest_status_html("done"), "\n".join(log), 0
|
| 1963 |
+
return
|
| 1964 |
+
|
| 1965 |
+
log.append(_line("✓ Pipeline complete — go to Review tab"))
|
| 1966 |
+
yield _ingest_status_html("done"), "\n".join(log), 1 # 1 = triggers review load
|
| 1967 |
+
|
| 1968 |
+
|
| 1969 |
+
|
| 1970 |
def run_ocr(page_selection: str = "", page_exclusion: str = "", replace_book_queue: bool = True) -> tuple:
|
| 1971 |
"""Run Surya OCR on all profiled PDFs."""
|
| 1972 |
df = load_manifest_df()
|
|
|
|
| 2814 |
gr.HTML("""<div class="ss-panel-header" style="padding:20px 0 0 0">
|
| 2815 |
<div class="ss-panel-icon">📥</div>
|
| 2816 |
<div><p class="ss-panel-title">Source Registry</p>
|
| 2817 |
+
<p class="ss-panel-sub">Drop PDF · Set pages · Click Run — everything else is automatic</p></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2818 |
</div>""")
|
| 2819 |
|
| 2820 |
ingest_status = gr.HTML(_ingest_status_html("idle"))
|
|
|
|
| 2822 |
with gr.Row():
|
| 2823 |
with gr.Column(scale=2):
|
| 2824 |
pdf_upload = gr.File(
|
| 2825 |
+
label="① Drop PDF here",
|
| 2826 |
file_types=[".pdf"],
|
| 2827 |
file_count="multiple",
|
| 2828 |
type="filepath",
|
| 2829 |
)
|
| 2830 |
with gr.Column(scale=1):
|
| 2831 |
rights_dd = gr.Dropdown(
|
| 2832 |
+
label="② Rights Class",
|
| 2833 |
choices=["public-domain","licensed-owned","controlled-internal","unknown"],
|
| 2834 |
value="unknown",
|
| 2835 |
)
|
|
|
|
| 2844 |
lines=1,
|
| 2845 |
)
|
| 2846 |
ingest_notes = gr.Textbox(label="Notes", placeholder="Source, edition, etc.", lines=2)
|
| 2847 |
+
|
| 2848 |
+
with gr.Row():
|
| 2849 |
+
scope_include = gr.Textbox(
|
| 2850 |
+
label="③ Story Pages Include",
|
| 2851 |
+
placeholder="e.g. 7-27 or 7,8,9,11-27 or all",
|
| 2852 |
+
scale=2,
|
| 2853 |
+
)
|
| 2854 |
+
scope_exclude = gr.Textbox(
|
| 2855 |
+
label="Story Pages Exclude",
|
| 2856 |
+
placeholder="e.g. 1,2,3,17,25",
|
| 2857 |
+
scale=2,
|
| 2858 |
+
)
|
| 2859 |
+
scope_title = gr.Textbox(
|
| 2860 |
+
label="Safe Book Title",
|
| 2861 |
+
placeholder="auto-filled from filename",
|
| 2862 |
+
scale=2,
|
| 2863 |
+
)
|
| 2864 |
+
|
| 2865 |
+
run_pipeline_btn = gr.Button(
|
| 2866 |
+
"④ Register + OCR + Send to Review →",
|
| 2867 |
+
elem_classes=["ss-btn-run"],
|
| 2868 |
+
)
|
| 2869 |
+
pipeline_done = gr.State(0)
|
| 2870 |
+
ingest_log = gr.Textbox(label="Log", lines=10, interactive=False, elem_classes=["ss-log"])
|
| 2871 |
+
ingest_copy_log_btn = gr.Button("Copy Log", size="sm")
|
| 2872 |
|
| 2873 |
manifest_table = gr.DataFrame(
|
| 2874 |
label="Source Manifest",
|
| 2875 |
interactive=False,
|
| 2876 |
wrap=True,
|
| 2877 |
)
|
|
|
|
|
|
|
| 2878 |
refresh_btn = gr.Button("↻ Refresh Manifest", size="sm")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2879 |
|
| 2880 |
+
# Keep hidden compat fields for wiring that references them later
|
| 2881 |
+
update_book_id = gr.Textbox(visible=False, value="")
|
| 2882 |
+
scope_book_id = gr.Textbox(visible=False, value="")
|
| 2883 |
+
update_rights_dd = gr.Dropdown(choices=["public-domain","licensed-owned","controlled-internal","unknown"], value="licensed-owned", visible=False)
|
| 2884 |
+
scope_save_btn = gr.Button(visible=False)
|
| 2885 |
+
ingest_btn = gr.Button(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2886 |
|
| 2887 |
# ── Auto-populate: book code + year → both book ID fields ──────
|
| 2888 |
def _make_book_id(code, year):
|
|
|
|
| 2920 |
outputs=[scope_title],
|
| 2921 |
)
|
| 2922 |
|
| 2923 |
+
# Main pipeline button — does everything
|
| 2924 |
+
run_pipeline_btn.click(
|
| 2925 |
+
run_full_pipeline,
|
| 2926 |
+
inputs=[pdf_upload, rights_dd, ingest_notes, book_code_input, pub_year_input,
|
| 2927 |
+
scope_include, scope_exclude, scope_title],
|
| 2928 |
+
outputs=[ingest_status, ingest_log, pipeline_done],
|
| 2929 |
+
show_progress="minimal",
|
| 2930 |
+
trigger_mode="once",
|
| 2931 |
+
)
|
| 2932 |
+
# Refresh manifest after pipeline completes
|
| 2933 |
+
pipeline_done.change(lambda: load_manifest_df(), outputs=[manifest_table])
|
| 2934 |
+
|
| 2935 |
+
refresh_btn.click(lambda: load_manifest_df(), outputs=[manifest_table])
|
| 2936 |
+
ingest_copy_log_btn.click(
|
| 2937 |
+
fn=None,
|
| 2938 |
+
inputs=[ingest_log],
|
| 2939 |
+
outputs=[],
|
| 2940 |
+
js="(logTxt) => { if (navigator && navigator.clipboard) { navigator.clipboard.writeText(logTxt || ''); } return []; }",
|
| 2941 |
)
|
| 2942 |
gr.HTML('<div style="height:16px"></div>')
|
| 2943 |
# Auto-load removed — use Refresh button instead to avoid SSR hang
|
|
|
|
| 3142 |
)
|
| 3143 |
review_tab.select(load_review, outputs=review_load_outputs)
|
| 3144 |
ocr_done.change(load_review, outputs=review_load_outputs)
|
| 3145 |
+
pipeline_done.change(load_review, outputs=review_load_outputs)
|
| 3146 |
|
| 3147 |
accept_btn.click(do_accept, inputs=[current_idx, final_text_box, reviewer_name, reason_code], outputs=action_outputs)
|
| 3148 |
edit_btn.click(do_accept, inputs=[current_idx, final_text_box, reviewer_name, reason_code], outputs=action_outputs)
|