Pointf5ive commited on
Commit
600a5ed
Β·
1 Parent(s): 26d3c0e

feat: persist workbook home and add dashboard manuscript intake bridge

Browse files
Files changed (2) hide show
  1. .gitattributes +0 -3
  2. app.py +131 -9
.gitattributes CHANGED
@@ -37,6 +37,3 @@ smoke_signal/tessdata/gill_sans_infant.traineddata.otf filter=lfs diff=lfs merge
37
  *.traineddata filter=lfs diff=lfs merge=lfs -text
38
  *.otf filter=lfs diff=lfs merge=lfs -text
39
  assets/Brand/Side[[:space:]]Navigation[[:space:]]Bar.png filter=lfs diff=lfs merge=lfs -text
40
- assets/Brand/Ui[[:space:]]Totem.png filter=lfs diff=lfs merge=lfs -text
41
- assets/Brand/Hero.png filter=lfs diff=lfs merge=lfs -text
42
- assets/Brand/side_nav_logo.png filter=lfs diff=lfs merge=lfs -text
 
37
  *.traineddata filter=lfs diff=lfs merge=lfs -text
38
  *.otf filter=lfs diff=lfs merge=lfs -text
39
  assets/Brand/Side[[:space:]]Navigation[[:space:]]Bar.png filter=lfs diff=lfs merge=lfs -text
 
 
 
app.py CHANGED
@@ -4,6 +4,7 @@ import hashlib
4
  import html
5
  import json
6
  import re
 
7
  from html import escape
8
  from pathlib import Path
9
 
@@ -31,6 +32,8 @@ from smoke_signal_tab import smoke_signal_tab, SS_CSS
31
 
32
  ORIGINAL_WORKBOOK_PATH = "data/order69_macmillan_totem_rebuilt.xlsx"
33
  CODEX_CATALOGUE_PATH = Path("data/codex_catalogue.xlsx")
 
 
34
 
35
 
36
  def _patch_gradio_schema_bool_compat() -> None:
@@ -1794,6 +1797,32 @@ def _validate_workbook_path(path: Path) -> Path:
1794
  return path
1795
 
1796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1797
  def _score_summary(log_df: pd.DataFrame | None) -> str:
1798
  if log_df is None or log_df.empty:
1799
  return "No scored rows yet."
@@ -2685,13 +2714,14 @@ def load_workbook(uploaded_file=None, notice: str = ""):
2685
 
2686
 
2687
  def load_default():
2688
- return load_workbook(None, "Bundled workbook reloaded.")
2689
 
2690
 
2691
  def load_uploaded(uploaded_file):
2692
  if uploaded_file is None:
2693
  raise gr.Error("Choose an .xlsx or .xlsm workbook first.")
2694
- return load_workbook(uploaded_file, "Workbook uploaded and analysed.")
 
2695
 
2696
 
2697
  def load_local_path(path_text: str):
@@ -2702,27 +2732,76 @@ def load_local_path(path_text: str):
2702
 
2703
 
2704
  def run_analysis(active_path: str):
2705
- path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
2706
  log_df = score_log(path)
2707
  state = _build_dashboard_state_from_workbook(path, "complete", "TOTEM analysis refreshed.")
2708
  return render_dashboard(state), log_df, _score_summary(log_df)
2709
 
2710
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2711
  def recalc_log(log_df, active_path: str):
2712
- path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
2713
  recalculated = recalculate_log(log_df, path)
2714
  state = _build_dashboard_state_from_workbook(path, "complete", "Gates recalculated.")
2715
  return render_dashboard(state), recalculated, _score_summary(recalculated)
2716
 
2717
 
2718
  def export_log(log_df, active_path: str):
2719
- path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
2720
  return export_updated_workbook(log_df, path)
2721
 
2722
 
2723
  def single_score(active_path, sequence, stanza_id, draft_pass, clarity, rhythm, flow,
2724
  emotional_truth, visual_strength, commercial, notes):
2725
- path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
2726
  df = score_single_row(path, sequence, stanza_id, draft_pass, clarity, rhythm,
2727
  flow, emotional_truth, visual_strength, commercial, notes)
2728
  return df, _score_summary(df)
@@ -2734,7 +2813,7 @@ def initial_dashboard_html() -> str:
2734
  Falls back gracefully if workbook read fails.
2735
  """
2736
  state = get_initial_dashboard_state()
2737
- state["workbook_loaded"] = bool(DEFAULT_WORKBOOK.exists())
2738
  state["analysis_status"] = "ready" if state["workbook_loaded"] else "idle"
2739
  state["project_name"] = "Editorial Workspace"
2740
  state["totem_signal"] = compute_totem_signal(
@@ -2748,7 +2827,7 @@ def initial_dashboard_html() -> str:
2748
  # ── GRADIO INTERFACE ──────────────────────────────────────────────────────────
2749
 
2750
  with gr.Blocks(title="TOTEM Studio") as demo:
2751
- active_path = gr.State(str(DEFAULT_WORKBOOK))
2752
  log_state = gr.State(pd.DataFrame(columns=LOG_COLUMNS))
2753
 
2754
  with gr.Tabs():
@@ -2768,9 +2847,40 @@ with gr.Blocks(title="TOTEM Studio") as demo:
2768
  )
2769
  run_button = gr.Button("Run TOTEM Analysis", variant="secondary", scale=1)
2770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2771
  with gr.Row(elem_id="path-panel"):
2772
  with gr.Column(elem_classes=["wrap"]):
2773
- path_input = gr.Textbox(label="Local workbook path", value=ORIGINAL_WORKBOOK_PATH)
2774
  path_button = gr.Button("Load Local Path", variant="primary")
2775
 
2776
  with gr.Accordion("Private scoring controls", open=False, elem_id="score-panel"):
@@ -2926,6 +3036,18 @@ with gr.Blocks(title="TOTEM Studio") as demo:
2926
  outputs=[dashboard, log_state, score_status])
2927
  path_button.click(load_local_path, inputs=[path_input],
2928
  outputs=[active_path, dashboard, log_state, score_status])
 
 
 
 
 
 
 
 
 
 
 
 
2929
  recalc_button.click(recalc_log, inputs=[log_state, active_path],
2930
  outputs=[dashboard, log_state, score_status])
2931
  export_button.click(export_log, inputs=[log_state, active_path], outputs=[exported_file])
 
4
  import html
5
  import json
6
  import re
7
+ from shutil import copy2
8
  from html import escape
9
  from pathlib import Path
10
 
 
32
 
33
  ORIGINAL_WORKBOOK_PATH = "data/order69_macmillan_totem_rebuilt.xlsx"
34
  CODEX_CATALOGUE_PATH = Path("data/codex_catalogue.xlsx")
35
+ WORKBOOK_HOME_DIR = Path("data/workbook_home")
36
+ ACTIVE_WORKBOOK_PATH = WORKBOOK_HOME_DIR / "active_workbook.xlsx"
37
 
38
 
39
  def _patch_gradio_schema_bool_compat() -> None:
 
1797
  return path
1798
 
1799
 
1800
+ def _ensure_workbook_home() -> None:
1801
+ WORKBOOK_HOME_DIR.mkdir(parents=True, exist_ok=True)
1802
+
1803
+
1804
+ def _persist_uploaded_workbook(uploaded_file) -> Path:
1805
+ """
1806
+ Copy workbook upload to a stable local 'home' path so users do not need
1807
+ to re-upload each session.
1808
+ """
1809
+ src = _validate_workbook_path(_clean_path(uploaded_file))
1810
+ _ensure_workbook_home()
1811
+ copy2(src, ACTIVE_WORKBOOK_PATH)
1812
+ return ACTIVE_WORKBOOK_PATH
1813
+
1814
+
1815
+ def _preferred_workbook_path() -> Path:
1816
+ """
1817
+ Resolve workbook 'home' priority:
1818
+ 1) persisted uploaded workbook
1819
+ 2) bundled default workbook
1820
+ """
1821
+ if ACTIVE_WORKBOOK_PATH.exists():
1822
+ return ACTIVE_WORKBOOK_PATH
1823
+ return DEFAULT_WORKBOOK
1824
+
1825
+
1826
  def _score_summary(log_df: pd.DataFrame | None) -> str:
1827
  if log_df is None or log_df.empty:
1828
  return "No scored rows yet."
 
2714
 
2715
 
2716
  def load_default():
2717
+ return load_workbook(_preferred_workbook_path(), "Workbook reloaded from persistent home.")
2718
 
2719
 
2720
  def load_uploaded(uploaded_file):
2721
  if uploaded_file is None:
2722
  raise gr.Error("Choose an .xlsx or .xlsm workbook first.")
2723
+ persisted = _persist_uploaded_workbook(uploaded_file)
2724
+ return load_workbook(persisted, f"Workbook uploaded and saved to {persisted}.")
2725
 
2726
 
2727
  def load_local_path(path_text: str):
 
2732
 
2733
 
2734
  def run_analysis(active_path: str):
2735
+ path = _validate_workbook_path(Path(active_path) if active_path else _preferred_workbook_path())
2736
  log_df = score_log(path)
2737
  state = _build_dashboard_state_from_workbook(path, "complete", "TOTEM analysis refreshed.")
2738
  return render_dashboard(state), log_df, _score_summary(log_df)
2739
 
2740
 
2741
+ def analyze_manuscript_and_refresh(
2742
+ file_obj,
2743
+ author_name: str,
2744
+ author_id: str,
2745
+ works_sampled: str,
2746
+ active_path: str,
2747
+ ):
2748
+ """
2749
+ Dashboard bridge:
2750
+ - run manuscript extraction
2751
+ - refresh workbook dashboard view so scoring context stays synced
2752
+ """
2753
+ report, json_out, status = run_codex_extraction(file_obj, author_name, author_id, works_sampled)
2754
+ try:
2755
+ dashboard_html, log_df, summary = run_analysis(active_path)
2756
+ except Exception:
2757
+ dashboard_html = initial_dashboard_html()
2758
+ log_df = pd.DataFrame(columns=LOG_COLUMNS)
2759
+ summary = "Workbook refresh skipped because active workbook path is invalid."
2760
+ return report, json_out, status, dashboard_html, log_df, summary
2761
+
2762
+
2763
+ def analyze_manuscript_for_dashboard(file_obj, active_path: str):
2764
+ """
2765
+ Minimal dashboard-facing manuscript pipeline:
2766
+ - accepts a manuscript upload directly on Dashboard
2767
+ - runs Codex extraction
2768
+ - refreshes workbook dashboard state
2769
+ """
2770
+ file_path = _extract_uploaded_path(file_obj)
2771
+ if not file_path:
2772
+ raise gr.Error("Upload a manuscript (.txt or .pdf) first.")
2773
+
2774
+ stem = Path(file_path).stem.strip() or "Unknown Author"
2775
+ author_name = stem.replace("_", " ")[:120]
2776
+ works_sampled = stem.replace("_", " ")[:180]
2777
+ author_id = "CA-XXX"
2778
+ report, json_out, status, dashboard_html, log_df, summary = analyze_manuscript_and_refresh(
2779
+ file_obj=file_obj,
2780
+ author_name=author_name,
2781
+ author_id=author_id,
2782
+ works_sampled=works_sampled,
2783
+ active_path=active_path,
2784
+ )
2785
+ # Keep dashboard status compact while preserving full report in dedicated output.
2786
+ dashboard_status = f"{status} | Workbook refresh: {summary}"
2787
+ return report, json_out, dashboard_status, dashboard_html, log_df, summary
2788
+
2789
+
2790
  def recalc_log(log_df, active_path: str):
2791
+ path = _validate_workbook_path(Path(active_path) if active_path else _preferred_workbook_path())
2792
  recalculated = recalculate_log(log_df, path)
2793
  state = _build_dashboard_state_from_workbook(path, "complete", "Gates recalculated.")
2794
  return render_dashboard(state), recalculated, _score_summary(recalculated)
2795
 
2796
 
2797
  def export_log(log_df, active_path: str):
2798
+ path = _validate_workbook_path(Path(active_path) if active_path else _preferred_workbook_path())
2799
  return export_updated_workbook(log_df, path)
2800
 
2801
 
2802
  def single_score(active_path, sequence, stanza_id, draft_pass, clarity, rhythm, flow,
2803
  emotional_truth, visual_strength, commercial, notes):
2804
+ path = _validate_workbook_path(Path(active_path) if active_path else _preferred_workbook_path())
2805
  df = score_single_row(path, sequence, stanza_id, draft_pass, clarity, rhythm,
2806
  flow, emotional_truth, visual_strength, commercial, notes)
2807
  return df, _score_summary(df)
 
2813
  Falls back gracefully if workbook read fails.
2814
  """
2815
  state = get_initial_dashboard_state()
2816
+ state["workbook_loaded"] = bool(_preferred_workbook_path().exists())
2817
  state["analysis_status"] = "ready" if state["workbook_loaded"] else "idle"
2818
  state["project_name"] = "Editorial Workspace"
2819
  state["totem_signal"] = compute_totem_signal(
 
2827
  # ── GRADIO INTERFACE ──────────────────────────────────────────────────────────
2828
 
2829
  with gr.Blocks(title="TOTEM Studio") as demo:
2830
+ active_path = gr.State(str(_preferred_workbook_path()))
2831
  log_state = gr.State(pd.DataFrame(columns=LOG_COLUMNS))
2832
 
2833
  with gr.Tabs():
 
2847
  )
2848
  run_button = gr.Button("Run TOTEM Analysis", variant="secondary", scale=1)
2849
 
2850
+ with gr.Accordion("Manuscript Intake (score pipeline)", open=False, elem_id="codex-panel"):
2851
+ with gr.Column(elem_classes=["codex-body"]):
2852
+ gr.Markdown(
2853
+ "Upload manuscript here to run extraction and refresh workbook scoring context."
2854
+ )
2855
+ dashboard_manuscript_file = gr.File(
2856
+ label="Manuscript (.txt or .pdf)",
2857
+ file_types=[".txt", ".pdf"],
2858
+ type="filepath",
2859
+ )
2860
+ dashboard_manuscript_analyze = gr.Button(
2861
+ "Analyze Manuscript + Refresh Dashboard",
2862
+ variant="primary",
2863
+ )
2864
+ dashboard_manuscript_status = gr.Textbox(
2865
+ label="Pipeline status",
2866
+ interactive=False,
2867
+ lines=2,
2868
+ value="Ready.",
2869
+ )
2870
+ dashboard_manuscript_json = gr.Textbox(
2871
+ label="Extracted fingerprint (JSON)",
2872
+ interactive=False,
2873
+ lines=8,
2874
+ )
2875
+ dashboard_manuscript_report = gr.Textbox(
2876
+ label="Extraction report",
2877
+ interactive=False,
2878
+ lines=10,
2879
+ )
2880
+
2881
  with gr.Row(elem_id="path-panel"):
2882
  with gr.Column(elem_classes=["wrap"]):
2883
+ path_input = gr.Textbox(label="Local workbook path", value=str(_preferred_workbook_path()))
2884
  path_button = gr.Button("Load Local Path", variant="primary")
2885
 
2886
  with gr.Accordion("Private scoring controls", open=False, elem_id="score-panel"):
 
3036
  outputs=[dashboard, log_state, score_status])
3037
  path_button.click(load_local_path, inputs=[path_input],
3038
  outputs=[active_path, dashboard, log_state, score_status])
3039
+ dashboard_manuscript_analyze.click(
3040
+ analyze_manuscript_for_dashboard,
3041
+ inputs=[dashboard_manuscript_file, active_path],
3042
+ outputs=[
3043
+ dashboard_manuscript_report,
3044
+ dashboard_manuscript_json,
3045
+ dashboard_manuscript_status,
3046
+ dashboard,
3047
+ log_state,
3048
+ score_status,
3049
+ ],
3050
+ )
3051
  recalc_button.click(recalc_log, inputs=[log_state, active_path],
3052
  outputs=[dashboard, log_state, score_status])
3053
  export_button.click(export_log, inputs=[log_state, active_path], outputs=[exported_file])