Pointf5ive commited on
Commit
1c88ac1
Β·
verified Β·
1 Parent(s): 6345ebe

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +167 -10
app.py CHANGED
@@ -1,6 +1,8 @@
1
  from __future__ import annotations
2
 
 
3
  import json
 
4
  from html import escape
5
  from pathlib import Path
6
 
@@ -26,6 +28,7 @@ from src.totem_workbook import (
26
  from src.codex_extractor import process_upload, format_fingerprint_report
27
 
28
  ORIGINAL_WORKBOOK_PATH = "data/order69_macmillan_totem_rebuilt.xlsx"
 
29
 
30
 
31
  def _patch_gradio_schema_bool_compat() -> None:
@@ -833,6 +836,164 @@ def dashboard_html(path: Path, notice: str = "") -> str:
833
 
834
  # ── CODEX EXTRACTOR FUNCTIONS ─────────────────────────────────────────────────
835
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  def run_codex_extraction(
837
  file_obj,
838
  author_name: str,
@@ -858,11 +1019,7 @@ def run_codex_extraction(
858
  )
859
 
860
  # Gradio may pass a filepath string or a file-like payload depending on runtime.
861
- file_path = file_obj
862
- if isinstance(file_obj, dict):
863
- file_path = file_obj.get("path") or file_obj.get("name")
864
- elif hasattr(file_obj, "name"):
865
- file_path = file_obj.name
866
 
867
  if not file_path:
868
  return (
@@ -1073,7 +1230,7 @@ with gr.Blocks(title="TOTEM Studio", css=CSS) as demo:
1073
  color: #5d4037;
1074
  ">
1075
  <b>File requirements:</b><br>
1076
- β€’ PDF must contain selectable text (not scanned images)<br>
1077
  β€’ Minimum 1,000 words for HIGH confidence fingerprint<br>
1078
  β€’ Combine multiple works in one file to increase sample size<br>
1079
  β€’ Visual-primary books (Van Allsburg, Jeffers) will flag LOW confidence
@@ -1145,13 +1302,13 @@ with gr.Blocks(title="TOTEM Studio", css=CSS) as demo:
1145
  queue=False,
1146
  trigger_mode="multiple",
1147
  )
1148
- # Fallback trigger: run extraction immediately after file upload as well.
1149
  codex_file.upload(
1150
- run_codex_extraction,
1151
  inputs=[codex_file, codex_author_name, codex_author_id, codex_works],
1152
- outputs=[codex_report, codex_json, codex_status],
1153
  queue=False,
1154
- trigger_mode="multiple",
1155
  )
1156
  codex_clear_btn.click(
1157
  clear_codex_form,
 
1
  from __future__ import annotations
2
 
3
+ import hashlib
4
  import json
5
+ import re
6
  from html import escape
7
  from pathlib import Path
8
 
 
28
  from src.codex_extractor import process_upload, format_fingerprint_report
29
 
30
  ORIGINAL_WORKBOOK_PATH = "data/order69_macmillan_totem_rebuilt.xlsx"
31
+ CODEX_CATALOGUE_PATH = Path("data/codex_catalogue.xlsx")
32
 
33
 
34
  def _patch_gradio_schema_bool_compat() -> None:
 
836
 
837
  # ── CODEX EXTRACTOR FUNCTIONS ─────────────────────────────────────────────────
838
 
839
+ def _extract_uploaded_path(file_obj) -> str | None:
840
+ """Handle Gradio file payload variants and return a filesystem path."""
841
+ if file_obj is None:
842
+ return None
843
+ if isinstance(file_obj, str):
844
+ return file_obj
845
+ if isinstance(file_obj, dict):
846
+ return file_obj.get("path") or file_obj.get("name")
847
+ if hasattr(file_obj, "name"):
848
+ return file_obj.name
849
+ return None
850
+
851
+
852
+ def _normalise_lookup_key(value: str) -> str:
853
+ text = str(value or "").lower()
854
+ text = re.sub(r"[_\-]+", " ", text)
855
+ text = re.sub(r"[^a-z0-9 ]+", " ", text)
856
+ return re.sub(r"\s+", " ", text).strip()
857
+
858
+
859
+ def _generate_codex_author_id(author_name: str) -> str:
860
+ """
861
+ Deterministic fallback author ID when missing in catalogue.
862
+ Format: CA-<3 letters>-<3 digits>
863
+ """
864
+ cleaned = re.sub(r"[^A-Za-z]", "", author_name or "").upper()
865
+ prefix = (cleaned[:3] or "AUT").ljust(3, "X")
866
+ digest = hashlib.md5((author_name or "").strip().lower().encode("utf-8")).hexdigest()
867
+ suffix = int(digest[:4], 16) % 1000
868
+ return f"CA-{prefix}-{suffix:03d}"
869
+
870
+
871
+ def _empty_catalogue_df() -> pd.DataFrame:
872
+ return pd.DataFrame(columns=["authour_id", "author_name", "title"])
873
+
874
+
875
+ def _load_codex_catalogue(path: Path = CODEX_CATALOGUE_PATH) -> pd.DataFrame:
876
+ """
877
+ Load catalogue workbook with required columns:
878
+ `authour_id`, `author_name`, `title`
879
+ """
880
+ if not path.exists():
881
+ return _empty_catalogue_df()
882
+ try:
883
+ raw = pd.read_excel(path)
884
+ except Exception:
885
+ return _empty_catalogue_df()
886
+ if raw is None or raw.empty:
887
+ return _empty_catalogue_df()
888
+
889
+ col_lookup = {str(col).strip().lower(): col for col in raw.columns}
890
+ id_col = col_lookup.get("authour_id") or col_lookup.get("author_id")
891
+ name_col = col_lookup.get("author_name")
892
+ title_col = col_lookup.get("title")
893
+
894
+ if name_col is None or title_col is None:
895
+ return _empty_catalogue_df()
896
+
897
+ if id_col is None:
898
+ raw["__authour_id"] = ""
899
+ id_col = "__authour_id"
900
+
901
+ cat = raw[[id_col, name_col, title_col]].copy()
902
+ cat.columns = ["authour_id", "author_name", "title"]
903
+
904
+ for col in ["authour_id", "author_name", "title"]:
905
+ cat[col] = cat[col].fillna("").astype(str).str.strip()
906
+
907
+ cat = cat[(cat["author_name"] != "") & (cat["title"] != "")]
908
+ return cat
909
+
910
+
911
+ def _match_catalogue_row(file_path: str, catalogue: pd.DataFrame) -> pd.Series | None:
912
+ if catalogue.empty:
913
+ return None
914
+ stem_key = _normalise_lookup_key(Path(file_path).stem)
915
+ if not stem_key:
916
+ return None
917
+
918
+ title_keys = catalogue["title"].map(_normalise_lookup_key)
919
+
920
+ exact = catalogue[title_keys == stem_key]
921
+ if not exact.empty:
922
+ return exact.iloc[0]
923
+
924
+ contains = catalogue[
925
+ title_keys.apply(lambda t: bool(t) and (t in stem_key or stem_key in t))
926
+ ]
927
+ if not contains.empty:
928
+ return contains.assign(_key_len=contains["title"].map(lambda t: len(_normalise_lookup_key(t)))) \
929
+ .sort_values("_key_len", ascending=False) \
930
+ .iloc[0]
931
+ return None
932
+
933
+
934
+ def autofill_codex_details(
935
+ file_obj,
936
+ current_author_name: str,
937
+ current_author_id: str,
938
+ current_works: str,
939
+ ) -> tuple[str, str, str, str]:
940
+ """
941
+ Auto-populate author fields from data/codex_catalogue.xlsx on file upload.
942
+ Expected columns: authour_id, author_name, title.
943
+ """
944
+ file_path = _extract_uploaded_path(file_obj)
945
+ if not file_path:
946
+ return (
947
+ current_author_name,
948
+ current_author_id or "CA-XXX",
949
+ current_works,
950
+ "Ready.",
951
+ )
952
+
953
+ catalogue = _load_codex_catalogue()
954
+ if catalogue.empty:
955
+ return (
956
+ current_author_name,
957
+ current_author_id or "CA-XXX",
958
+ current_works,
959
+ "No catalogue match: add rows to data/codex_catalogue.xlsx with authour_id, author_name, title.",
960
+ )
961
+
962
+ row = _match_catalogue_row(file_path, catalogue)
963
+ if row is None:
964
+ return (
965
+ current_author_name,
966
+ current_author_id or "CA-XXX",
967
+ current_works,
968
+ "No title match found in catalogue for this filename. You can still fill fields manually.",
969
+ )
970
+
971
+ author_name = str(row["author_name"]).strip()
972
+ author_id = str(row["authour_id"]).strip() or _generate_codex_author_id(author_name)
973
+
974
+ by_author = catalogue[
975
+ catalogue["author_name"].map(_normalise_lookup_key) == _normalise_lookup_key(author_name)
976
+ ]
977
+ if author_id:
978
+ by_id = catalogue[
979
+ catalogue["authour_id"].fillna("").astype(str).str.strip() == author_id
980
+ ]
981
+ if not by_id.empty:
982
+ by_author = by_id
983
+
984
+ works = [w for w in by_author["title"].astype(str).str.strip().tolist() if w]
985
+ works_sampled = ", ".join(pd.Series(works).drop_duplicates().tolist())
986
+ if not works_sampled:
987
+ works_sampled = str(row["title"]).strip()
988
+
989
+ return (
990
+ author_name,
991
+ author_id,
992
+ works_sampled,
993
+ f"Auto-filled from catalogue: {author_name} ({author_id}).",
994
+ )
995
+
996
+
997
  def run_codex_extraction(
998
  file_obj,
999
  author_name: str,
 
1019
  )
1020
 
1021
  # Gradio may pass a filepath string or a file-like payload depending on runtime.
1022
+ file_path = _extract_uploaded_path(file_obj)
 
 
 
 
1023
 
1024
  if not file_path:
1025
  return (
 
1230
  color: #5d4037;
1231
  ">
1232
  <b>File requirements:</b><br>
1233
+ β€’ Selectable text preferred; scanned PDFs are OCR-processed automatically<br>
1234
  β€’ Minimum 1,000 words for HIGH confidence fingerprint<br>
1235
  β€’ Combine multiple works in one file to increase sample size<br>
1236
  β€’ Visual-primary books (Van Allsburg, Jeffers) will flag LOW confidence
 
1302
  queue=False,
1303
  trigger_mode="multiple",
1304
  )
1305
+ # Auto-fill author metadata from catalogue on file upload.
1306
  codex_file.upload(
1307
+ autofill_codex_details,
1308
  inputs=[codex_file, codex_author_name, codex_author_id, codex_works],
1309
+ outputs=[codex_author_name, codex_author_id, codex_works, codex_status],
1310
  queue=False,
1311
+ trigger_mode="always_last",
1312
  )
1313
  codex_clear_btn.click(
1314
  clear_codex_form,