Pointf5ive commited on
Commit
e7b8c1d
Β·
1 Parent(s): a38f6a7

Switch ingest IDs to code+year and handle duplicate ID rename

Browse files
Files changed (1) hide show
  1. smoke_signal_tab.py +167 -6
smoke_signal_tab.py CHANGED
@@ -770,6 +770,141 @@ def next_book_id(df: pd.DataFrame) -> str:
770
  return "SS-BOOK-9999"
771
 
772
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
773
  def load_queue_df() -> pd.DataFrame:
774
  if not QUEUE_CSV.exists():
775
  return pd.DataFrame()
@@ -899,7 +1034,7 @@ def save_book_scope(book_id: str, include_spec: str, exclude_spec: str, safe_tit
899
 
900
 
901
  # ── Step 1: INGEST ─────────────────────────────────────────────────────────────
902
- def ingest_pdfs(files, rights_class: str, notes: str) -> tuple:
903
  """Register uploaded PDFs into the source manifest."""
904
  if not files:
905
  return _ingest_status_html("idle"), pd.DataFrame(), "No files uploaded."
@@ -918,11 +1053,25 @@ def ingest_pdfs(files, rights_class: str, notes: str) -> tuple:
918
 
919
  file_hash = sha256_file(path)
920
 
 
 
 
 
 
 
 
 
921
  # Check duplicate β€” update rights/notes if changed
922
  if not df.empty and file_hash in df["sha256"].values:
923
  existing_book_id = df.loc[df["sha256"] == file_hash, "book_id"].values[0]
924
  existing_status = df.loc[df["sha256"] == file_hash, "status"].values[0]
925
  existing_rights = df.loc[df["sha256"] == file_hash, "rights_class"].values[0]
 
 
 
 
 
 
926
  if rights_class != "unknown" and existing_rights != rights_class:
927
  df.loc[df["sha256"] == file_hash, "rights_class"] = rights_class
928
  df.loc[df["sha256"] == file_hash, "notes"] = notes
@@ -930,7 +1079,7 @@ def ingest_pdfs(files, rights_class: str, notes: str) -> tuple:
930
  if existing_status == "pending":
931
  auto_profile_ids.append(existing_book_id)
932
  else:
933
- log.append(log_line(f"↩ Duplicate: {path.name} (rights={existing_rights})"))
934
  dup_count += 1
935
  continue
936
 
@@ -949,7 +1098,9 @@ def ingest_pdfs(files, rights_class: str, notes: str) -> tuple:
949
  except Exception:
950
  pass
951
 
952
- book_id = next_book_id(df)
 
 
953
  new_row = pd.DataFrame([{
954
  "book_id": book_id,
955
  "filename": path.name,
@@ -2390,6 +2541,16 @@ def smoke_signal_tab():
2390
  choices=["public-domain","licensed-owned","controlled-internal","unknown"],
2391
  value="unknown",
2392
  )
 
 
 
 
 
 
 
 
 
 
2393
  ingest_notes = gr.Textbox(label="Notes", placeholder="Source, edition, etc.", lines=2)
2394
  ingest_btn = gr.Button("Register + Auto-Profile β†’", elem_classes=["ss-btn-run"])
2395
 
@@ -2411,7 +2572,7 @@ def smoke_signal_tab():
2411
 
2412
  gr.Markdown("**Update rights class for existing book:**")
2413
  with gr.Row():
2414
- update_book_id = gr.Textbox(label="Book ID", placeholder="SS-BOOK-0001", scale=1)
2415
  update_rights_dd = gr.Dropdown(
2416
  label="New Rights Class",
2417
  choices=["public-domain","licensed-owned","controlled-internal","unknown"],
@@ -2427,7 +2588,7 @@ def smoke_signal_tab():
2427
 
2428
  gr.Markdown("**Save Book Pages + Safe Title (persisted):**")
2429
  with gr.Row():
2430
- scope_book_id = gr.Textbox(label="Book ID", placeholder="SS-BOOK-0001", scale=1)
2431
  scope_include = gr.Textbox(label="Story Pages Include", placeholder="all or 7-27 or 7,8,9,11-27", scale=1)
2432
  scope_exclude = gr.Textbox(label="Story Pages Exclude", placeholder="e.g. 1,2,3,17,25", scale=1)
2433
  scope_title = gr.Textbox(label="Safe Book Title", placeholder="e.g. the-gruffalo", scale=1)
@@ -2440,7 +2601,7 @@ def smoke_signal_tab():
2440
 
2441
  ingest_btn.click(
2442
  ingest_pdfs,
2443
- inputs=[pdf_upload, rights_dd, ingest_notes],
2444
  outputs=[ingest_status, manifest_table, ingest_log],
2445
  )
2446
  gr.HTML('<div style="height:16px"></div>')
 
770
  return "SS-BOOK-9999"
771
 
772
 
773
+ _TITLE_STOPWORDS = {"the", "a", "an"}
774
+
775
+
776
+ def _derive_book_code(title: str, code_hint: str = "") -> str:
777
+ hint = re.sub(r"[^a-z]", "", str(code_hint or "").lower())
778
+ if len(hint) >= 3:
779
+ return hint[:3]
780
+
781
+ words = re.findall(r"[a-z]+", str(title or "").lower())
782
+ if words and words[0] in _TITLE_STOPWORDS and len(words) > 1:
783
+ words = words[1:]
784
+ letters = "".join(words)
785
+ if not letters:
786
+ return "bok"
787
+ consonants = "".join(ch for ch in letters if ch not in "aeiou")
788
+ base = consonants[:3] if len(consonants) >= 3 else letters[:3]
789
+ return (base + "xxx")[:3]
790
+
791
+
792
+ def _extract_year(*parts: str) -> str:
793
+ for part in parts:
794
+ match = re.search(r"\b(1[6-9]\d{2}|20\d{2})\b", str(part or ""))
795
+ if match:
796
+ return match.group(1)
797
+ return ""
798
+
799
+
800
+ def _suggest_book_id(
801
+ title: str,
802
+ notes: str = "",
803
+ code_hint: str = "",
804
+ year_hint: str = "",
805
+ existing_ids: Optional[set[str]] = None,
806
+ current_id: str = "",
807
+ ) -> str:
808
+ code = _derive_book_code(title, code_hint=code_hint)
809
+ year = _extract_year(year_hint, notes, title) or "0000"
810
+ base = f"{code}{year}"
811
+ if existing_ids is None:
812
+ return base
813
+ if base not in existing_ids or base == current_id:
814
+ return base
815
+ for suffix in "abcdefghijklmnopqrstuvwxyz":
816
+ candidate = f"{base}{suffix}"
817
+ if candidate not in existing_ids or candidate == current_id:
818
+ return candidate
819
+ return base
820
+
821
+
822
+ def _rename_book_id_references(old_id: str, new_id: str) -> None:
823
+ if not old_id or not new_id or old_id == new_id:
824
+ return
825
+
826
+ # Profile JSON
827
+ old_profile = PROFILES_DIR / f"{old_id}_page_profile.json"
828
+ new_profile = PROFILES_DIR / f"{new_id}_page_profile.json"
829
+ if old_profile.exists():
830
+ try:
831
+ data = json.load(open(old_profile, encoding="utf-8"))
832
+ data["book_id"] = new_id
833
+ with open(new_profile, "w", encoding="utf-8") as f:
834
+ json.dump(data, f, indent=2)
835
+ old_profile.unlink(missing_ok=True)
836
+ except Exception:
837
+ pass
838
+
839
+ # Render directory
840
+ old_render_dir = RENDERS_DIR / old_id
841
+ new_render_dir = RENDERS_DIR / new_id
842
+ if old_render_dir.exists() and not new_render_dir.exists():
843
+ old_render_dir.rename(new_render_dir)
844
+
845
+ # OCR raw directory + file
846
+ old_ocr_dir = OCR_RAW_DIR / old_id
847
+ new_ocr_dir = OCR_RAW_DIR / new_id
848
+ if old_ocr_dir.exists() and not new_ocr_dir.exists():
849
+ old_ocr_dir.rename(new_ocr_dir)
850
+ if new_ocr_dir.exists():
851
+ old_raw = new_ocr_dir / f"{old_id}_ocr_raw.json"
852
+ new_raw = new_ocr_dir / f"{new_id}_ocr_raw.json"
853
+ if old_raw.exists() and not new_raw.exists():
854
+ old_raw.rename(new_raw)
855
+ if new_raw.exists():
856
+ try:
857
+ raw_data = json.load(open(new_raw, encoding="utf-8"))
858
+ raw_data["book_id"] = new_id
859
+ with open(new_raw, "w", encoding="utf-8") as f:
860
+ json.dump(raw_data, f, indent=2)
861
+ except Exception:
862
+ pass
863
+
864
+ # Queue and decision CSVs
865
+ for csv_path in [QUEUE_CSV, DECISIONS_CSV]:
866
+ if not csv_path.exists():
867
+ continue
868
+ try:
869
+ cdf = pd.read_csv(csv_path)
870
+ if "book_id" in cdf.columns:
871
+ cdf.loc[cdf["book_id"] == old_id, "book_id"] = new_id
872
+ if "region_id" in cdf.columns:
873
+ region_series = cdf["region_id"].astype(str)
874
+ mask = region_series.str.startswith(f"{old_id}_")
875
+ cdf.loc[mask, "region_id"] = region_series[mask].str.replace(
876
+ f"{old_id}_", f"{new_id}_", n=1, regex=False
877
+ )
878
+ cdf.to_csv(csv_path, index=False)
879
+ except Exception:
880
+ pass
881
+
882
+ # Gold JSONL
883
+ if GOLD_FILE.exists():
884
+ tmp_path = GOLD_FILE.with_suffix(".tmp")
885
+ try:
886
+ with open(GOLD_FILE, "r", encoding="utf-8") as src, open(tmp_path, "w", encoding="utf-8") as dst:
887
+ for line in src:
888
+ line = line.strip()
889
+ if not line:
890
+ continue
891
+ try:
892
+ obj = json.loads(line)
893
+ except Exception:
894
+ dst.write(line + "\n")
895
+ continue
896
+ if obj.get("book_id") == old_id:
897
+ obj["book_id"] = new_id
898
+ region_id = str(obj.get("region_id", ""))
899
+ if region_id.startswith(f"{old_id}_"):
900
+ obj["region_id"] = region_id.replace(f"{old_id}_", f"{new_id}_", 1)
901
+ dst.write(json.dumps(obj, ensure_ascii=False) + "\n")
902
+ tmp_path.replace(GOLD_FILE)
903
+ except Exception:
904
+ if tmp_path.exists():
905
+ tmp_path.unlink(missing_ok=True)
906
+
907
+
908
  def load_queue_df() -> pd.DataFrame:
909
  if not QUEUE_CSV.exists():
910
  return pd.DataFrame()
 
1034
 
1035
 
1036
  # ── Step 1: INGEST ─────────────────────────────────────────────────────────────
1037
+ def ingest_pdfs(files, rights_class: str, notes: str, book_code_hint: str = "", publication_year: str = "") -> tuple:
1038
  """Register uploaded PDFs into the source manifest."""
1039
  if not files:
1040
  return _ingest_status_html("idle"), pd.DataFrame(), "No files uploaded."
 
1053
 
1054
  file_hash = sha256_file(path)
1055
 
1056
+ desired_book_id = _suggest_book_id(
1057
+ Path(path.name).stem,
1058
+ notes=notes,
1059
+ code_hint=book_code_hint,
1060
+ year_hint=publication_year,
1061
+ existing_ids=set(df["book_id"].tolist()) if not df.empty else set(),
1062
+ )
1063
+
1064
  # Check duplicate β€” update rights/notes if changed
1065
  if not df.empty and file_hash in df["sha256"].values:
1066
  existing_book_id = df.loc[df["sha256"] == file_hash, "book_id"].values[0]
1067
  existing_status = df.loc[df["sha256"] == file_hash, "status"].values[0]
1068
  existing_rights = df.loc[df["sha256"] == file_hash, "rights_class"].values[0]
1069
+ if desired_book_id and desired_book_id != existing_book_id and desired_book_id not in set(df["book_id"].tolist()):
1070
+ _rename_book_id_references(existing_book_id, desired_book_id)
1071
+ df.loc[df["sha256"] == file_hash, "book_id"] = desired_book_id
1072
+ existing_book_id = desired_book_id
1073
+ log.append(log_line(f"↻ Renamed duplicate book ID β†’ {desired_book_id}"))
1074
+
1075
  if rights_class != "unknown" and existing_rights != rights_class:
1076
  df.loc[df["sha256"] == file_hash, "rights_class"] = rights_class
1077
  df.loc[df["sha256"] == file_hash, "notes"] = notes
 
1079
  if existing_status == "pending":
1080
  auto_profile_ids.append(existing_book_id)
1081
  else:
1082
+ log.append(log_line(f"↩ Duplicate: {path.name} ({existing_book_id}, rights={existing_rights})"))
1083
  dup_count += 1
1084
  continue
1085
 
 
1098
  except Exception:
1099
  pass
1100
 
1101
+ book_id = desired_book_id
1102
+ if (not book_id) or (book_id in set(df["book_id"].tolist())):
1103
+ book_id = next_book_id(df)
1104
  new_row = pd.DataFrame([{
1105
  "book_id": book_id,
1106
  "filename": path.name,
 
2541
  choices=["public-domain","licensed-owned","controlled-internal","unknown"],
2542
  value="unknown",
2543
  )
2544
+ book_code_input = gr.Textbox(
2545
+ label="Book Code (3 letters)",
2546
+ placeholder="e.g. grf",
2547
+ lines=1,
2548
+ )
2549
+ pub_year_input = gr.Textbox(
2550
+ label="Publishing Year",
2551
+ placeholder="e.g. 1999",
2552
+ lines=1,
2553
+ )
2554
  ingest_notes = gr.Textbox(label="Notes", placeholder="Source, edition, etc.", lines=2)
2555
  ingest_btn = gr.Button("Register + Auto-Profile β†’", elem_classes=["ss-btn-run"])
2556
 
 
2572
 
2573
  gr.Markdown("**Update rights class for existing book:**")
2574
  with gr.Row():
2575
+ update_book_id = gr.Textbox(label="Book ID", placeholder="grf1999", scale=1)
2576
  update_rights_dd = gr.Dropdown(
2577
  label="New Rights Class",
2578
  choices=["public-domain","licensed-owned","controlled-internal","unknown"],
 
2588
 
2589
  gr.Markdown("**Save Book Pages + Safe Title (persisted):**")
2590
  with gr.Row():
2591
+ scope_book_id = gr.Textbox(label="Book ID", placeholder="grf1999", scale=1)
2592
  scope_include = gr.Textbox(label="Story Pages Include", placeholder="all or 7-27 or 7,8,9,11-27", scale=1)
2593
  scope_exclude = gr.Textbox(label="Story Pages Exclude", placeholder="e.g. 1,2,3,17,25", scale=1)
2594
  scope_title = gr.Textbox(label="Safe Book Title", placeholder="e.g. the-gruffalo", scale=1)
 
2601
 
2602
  ingest_btn.click(
2603
  ingest_pdfs,
2604
+ inputs=[pdf_upload, rights_dd, ingest_notes, book_code_input, pub_year_input],
2605
  outputs=[ingest_status, manifest_table, ingest_log],
2606
  )
2607
  gr.HTML('<div style="height:16px"></div>')