Claude commited on
Commit
b1608ed
·
1 Parent(s): ec14162

feat: single-button pipeline — Register + OCR + Review in one click

Browse files
Files changed (1) hide show
  1. 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">Upload PDFs · Register · Auto-profile · Rights class</p></div>
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">&#9654; How to run OCR — follow this order:</b><br>
2721
- <span style="color:#3fb950">1.</span> Drop your PDF &nbsp;·&nbsp; set <b style="color:#e6edf3">Rights Class</b> &nbsp;·&nbsp; enter <b style="color:#e6edf3">Book Code</b> + <b style="color:#e6edf3">Year</b> &nbsp;·&nbsp; 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) &nbsp;·&nbsp; 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> &nbsp;·&nbsp; click <b style="color:#f0883e">Run OCR</b> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; click <b style="color:#f0883e">↻ Load Review Queue</b> &nbsp;·&nbsp; 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 picture-book PDFs here",
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
- ingest_btn = gr.Button("Register + Auto-Profile →", elem_classes=["ss-btn-run"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- gr.Markdown("**Update rights class for existing book:**")
2773
- with gr.Row():
2774
- update_book_id = gr.Textbox(label="Book ID (optional)", placeholder="grf1999 or leave blank to use latest", scale=1)
2775
- update_rights_dd = gr.Dropdown(
2776
- label="New Rights Class",
2777
- choices=["public-domain","licensed-owned","controlled-internal","unknown"],
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
- ingest_btn.click(
2833
- ingest_pdfs,
2834
- inputs=[pdf_upload, rights_dd, ingest_notes, book_code_input, pub_year_input],
2835
- outputs=[ingest_status, manifest_table, ingest_log],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)