Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit Β·
600a5ed
1
Parent(s): 26d3c0e
feat: persist workbook home and add dashboard manuscript intake bridge
Browse files- .gitattributes +0 -3
- 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(
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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(
|
| 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(
|
| 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=
|
| 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])
|