Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit Β·
e7b8c1d
1
Parent(s): a38f6a7
Switch ingest IDs to code+year and handle duplicate ID rename
Browse files- 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 =
|
|
|
|
|
|
|
| 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="
|
| 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="
|
| 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>')
|