Update page_files/Upload_Data.py
Browse files- page_files/Upload_Data.py +102 -25
page_files/Upload_Data.py
CHANGED
|
@@ -45,9 +45,8 @@ from categorized.Backend.PDF_DataExtraction import (
|
|
| 45 |
# and category_push). Ensure image2.py / mapper5.py / category_push.py are on
|
| 46 |
# the import path (same directory or added to sys.path above). If you keep them
|
| 47 |
# under categorized/Backend/, change these three lines to:
|
| 48 |
-
# from categorized.Backend import
|
| 49 |
-
|
| 50 |
-
from categorized.Backend import Pdf_ImageExtraction
|
| 51 |
from categorized.Backend import mapper5, category_push
|
| 52 |
|
| 53 |
# Manual-entry path (unchanged β writes via the existing data_loader).
|
|
@@ -115,15 +114,24 @@ def _df_to_meta(df: pd.DataFrame) -> dict:
|
|
| 115 |
# which is exactly what mapper5.map_plots_to_properties and the display expect.
|
| 116 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 117 |
|
| 118 |
-
def extract_images(pdf_path: str) -> list:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
try:
|
| 120 |
with open(pdf_path, "rb") as f:
|
| 121 |
pdf_bytes = f.read()
|
| 122 |
-
|
| 123 |
-
pdf_bytes,
|
|
|
|
|
|
|
|
|
|
| 124 |
)
|
| 125 |
# Drop captions naming photos/micrographs/logos and crops the verifier
|
| 126 |
-
# marked "discard" (keeps "keep"/"recrop").
|
| 127 |
plot_results = mapper5.select_real_plots(plot_results)
|
| 128 |
st.session_state["plot_coverage"] = coverage
|
| 129 |
except Exception as e:
|
|
@@ -535,7 +543,7 @@ def render_material_data_tab(pdf_path: str):
|
|
| 535 |
unsafe_allow_html=True,
|
| 536 |
)
|
| 537 |
timer.caption(
|
| 538 |
-
f"
|
| 539 |
+ (f" Β· Est. remaining: ~{est_remaining}s" if est_remaining > 0 else "")
|
| 540 |
)
|
| 541 |
|
|
@@ -569,7 +577,7 @@ def render_material_data_tab(pdf_path: str):
|
|
| 569 |
st.session_state.pdf_data_extracted = True
|
| 570 |
st.session_state.pdf_extracted_meta = _df_to_meta(df)
|
| 571 |
st.session_state.pdf_doi = doi
|
| 572 |
-
st.success(f"Extracted {len(df)} properties in {elapsed_total:.0f}s")
|
| 573 |
else:
|
| 574 |
st.warning("No data extracted from PDF.")
|
| 575 |
return
|
|
@@ -619,18 +627,57 @@ def _links_for_group(links_df: pd.DataFrame, gi: int) -> pd.DataFrame:
|
|
| 619 |
return links_df[links_df["group_idx"] == gi]
|
| 620 |
|
| 621 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
def render_plots_tab(pdf_path: str, paper_id: str):
|
| 623 |
st.subheader("Extracted Plot Images & Property Mapping")
|
| 624 |
|
| 625 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 626 |
if not st.session_state.pdf_processed:
|
| 627 |
-
|
| 628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
st.session_state.pdf_processed = True
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
|
|
|
|
|
|
| 634 |
|
| 635 |
plot_results = st.session_state.plot_results
|
| 636 |
if not plot_results:
|
|
@@ -655,14 +702,14 @@ def render_plots_tab(pdf_path: str, paper_id: str):
|
|
| 655 |
# Downloads (image2's native zipper)
|
| 656 |
d1, d2 = st.columns(2)
|
| 657 |
d1.download_button(
|
| 658 |
-
"
|
| 659 |
-
data=
|
| 660 |
file_name=f"{paper_id}_plots.zip", mime="application/zip",
|
| 661 |
use_container_width=True, key="dl_plots_zip",
|
| 662 |
)
|
| 663 |
d2.download_button(
|
| 664 |
-
"
|
| 665 |
-
data=
|
| 666 |
file_name=f"{paper_id}_images.zip", mime="application/zip",
|
| 667 |
use_container_width=True, key="dl_images_zip",
|
| 668 |
)
|
|
@@ -703,14 +750,24 @@ def render_plots_tab(pdf_path: str, paper_id: str):
|
|
| 703 |
|
| 704 |
st.divider()
|
| 705 |
|
| 706 |
-
# 3) Figure-centric review β each figure with
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
for gi, group in enumerate(plot_results):
|
| 708 |
caption = group.get("caption", f"Figure {gi+1}")
|
| 709 |
page = group.get("page", "?")
|
| 710 |
imgs = group.get("image_data", [])
|
| 711 |
|
| 712 |
with st.container(border=True):
|
| 713 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
|
| 715 |
icols = st.columns(min(len(imgs), 4) or 1)
|
| 716 |
for pos, im in enumerate(imgs):
|
|
@@ -720,10 +777,28 @@ def render_plots_tab(pdf_path: str, paper_id: str):
|
|
| 720 |
sub = im.get("subplot_label") or ""
|
| 721 |
st.image(arr, channels="BGR", width=200,
|
| 722 |
caption=(sub or None))
|
|
|
|
| 723 |
v = im.get("verification") or {}
|
| 724 |
act = v.get("majority_action") if isinstance(v, dict) else None
|
| 725 |
if act and act != "keep":
|
| 726 |
-
st.caption(f"crop verdict: {act}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
if mapping_done:
|
| 729 |
grp = _links_for_group(links_df, gi)
|
|
@@ -770,7 +845,7 @@ def render_plots_tab(pdf_path: str, paper_id: str):
|
|
| 770 |
"attaches each crop's path. Push conforms those rows to the chosen "
|
| 771 |
"category table's 33-column schema and uploads them.")
|
| 772 |
|
| 773 |
-
if st.button("
|
| 774 |
with st.spinner("Saving crops and writing SQLiteβ¦"):
|
| 775 |
st.session_state.store = mapper5.store_properties_with_plots(
|
| 776 |
st.session_state.df_aug,
|
|
@@ -858,6 +933,8 @@ def main():
|
|
| 858 |
"pdf_extracted_df": pd.DataFrame(),
|
| 859 |
"pdf_extracted_meta": {},
|
| 860 |
"pdf_doi": "",
|
|
|
|
|
|
|
| 861 |
}
|
| 862 |
for k, v in defaults.items():
|
| 863 |
if k not in st.session_state:
|
|
@@ -898,7 +975,7 @@ def main():
|
|
| 898 |
st.tabs(["Material Data", "Extracted Plots"])
|
| 899 |
return
|
| 900 |
|
| 901 |
-
tab1, tab2 = st.tabs([" Material Data", "Extracted Plots"])
|
| 902 |
|
| 903 |
tmp_file = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False, prefix="matdb_")
|
| 904 |
try:
|
|
|
|
| 45 |
# and category_push). Ensure image2.py / mapper5.py / category_push.py are on
|
| 46 |
# the import path (same directory or added to sys.path above). If you keep them
|
| 47 |
# under categorized/Backend/, change these three lines to:
|
| 48 |
+
# from categorized.Backend import image2, mapper5, category_push
|
| 49 |
+
from categorized.Backend import Pdf_ImageExtraction as image2 # image2.py renamed
|
|
|
|
| 50 |
from categorized.Backend import mapper5, category_push
|
| 51 |
|
| 52 |
# Manual-entry path (unchanged β writes via the existing data_loader).
|
|
|
|
| 114 |
# which is exactly what mapper5.map_plots_to_properties and the display expect.
|
| 115 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
|
| 117 |
+
def extract_images(pdf_path: str, engines=None, check_missed: bool = True) -> list:
|
| 118 |
+
"""Detect + (optionally) recover + crop-verify plots.
|
| 119 |
+
`engines` selects the verifier LLMs: ["gemini"] (default, fast) or
|
| 120 |
+
["gemini","gpt","claude"] for a majority-vote cross-check that rejects more
|
| 121 |
+
non-plots (needs OPENAI_API_KEY / ANTHROPIC_API_KEY; missing keys just make
|
| 122 |
+
that engine abstain). `check_missed` toggles the per-page Gemini recovery scan."""
|
| 123 |
+
engines = engines or ["gemini"]
|
| 124 |
try:
|
| 125 |
with open(pdf_path, "rb") as f:
|
| 126 |
pdf_bytes = f.read()
|
| 127 |
+
plot_results, coverage = image2.extract_and_verify_plots(
|
| 128 |
+
pdf_bytes,
|
| 129 |
+
check_missed_plots=check_missed,
|
| 130 |
+
verify_crops=True,
|
| 131 |
+
verify_engines=engines,
|
| 132 |
)
|
| 133 |
# Drop captions naming photos/micrographs/logos and crops the verifier
|
| 134 |
+
# marked "discard" (keeps "keep"/"recrop"/"unverified"). Non-mutating.
|
| 135 |
plot_results = mapper5.select_real_plots(plot_results)
|
| 136 |
st.session_state["plot_coverage"] = coverage
|
| 137 |
except Exception as e:
|
|
|
|
| 543 |
unsafe_allow_html=True,
|
| 544 |
)
|
| 545 |
timer.caption(
|
| 546 |
+
f" Elapsed: {elapsed:.0f}s"
|
| 547 |
+ (f" Β· Est. remaining: ~{est_remaining}s" if est_remaining > 0 else "")
|
| 548 |
)
|
| 549 |
|
|
|
|
| 577 |
st.session_state.pdf_data_extracted = True
|
| 578 |
st.session_state.pdf_extracted_meta = _df_to_meta(df)
|
| 579 |
st.session_state.pdf_doi = doi
|
| 580 |
+
st.success(f" Extracted {len(df)} properties in {elapsed_total:.0f}s")
|
| 581 |
else:
|
| 582 |
st.warning("No data extracted from PDF.")
|
| 583 |
return
|
|
|
|
| 627 |
return links_df[links_df["group_idx"] == gi]
|
| 628 |
|
| 629 |
|
| 630 |
+
def _reset_plot_mapping():
|
| 631 |
+
"""Structural edits (removing a crop/figure) shift the positional group_idx
|
| 632 |
+
that links_df is keyed on, so any edit invalidates the current mapping.
|
| 633 |
+
Clearing it forces a clean re-map on the pruned set."""
|
| 634 |
+
st.session_state.mapping_done = False
|
| 635 |
+
st.session_state.links_df = pd.DataFrame()
|
| 636 |
+
st.session_state.df_aug = pd.DataFrame()
|
| 637 |
+
st.session_state.store = None
|
| 638 |
+
|
| 639 |
+
|
| 640 |
def render_plots_tab(pdf_path: str, paper_id: str):
|
| 641 |
st.subheader("Extracted Plot Images & Property Mapping")
|
| 642 |
|
| 643 |
+
# ββ Extraction settings: multi-LLM cross-check + recovery toggle ββββββββββ
|
| 644 |
+
with st.expander("β Extraction settings", expanded=not st.session_state.pdf_processed):
|
| 645 |
+
multi = st.checkbox(
|
| 646 |
+
"Cross-check each figure with GPT + Claude (majority vote β rejects more "
|
| 647 |
+
"non-plots, but ~3Γ slower and needs OPENAI_API_KEY / ANTHROPIC_API_KEY)",
|
| 648 |
+
value=st.session_state.get("verify_multi", False),
|
| 649 |
+
key="verify_multi",
|
| 650 |
+
)
|
| 651 |
+
recover = st.checkbox(
|
| 652 |
+
"Missed-plot recovery (per-page Gemini scan β catches plots OpenCV missed, slower)",
|
| 653 |
+
value=st.session_state.get("recover_missed", True),
|
| 654 |
+
key="recover_missed",
|
| 655 |
+
)
|
| 656 |
+
if multi and not (os.getenv("OPENAI_API_KEY") and os.getenv("ANTHROPIC_API_KEY")):
|
| 657 |
+
st.caption(" One or both of OPENAI_API_KEY / ANTHROPIC_API_KEY is not set β "
|
| 658 |
+
"that engine will abstain; the vote falls back to the ones available.")
|
| 659 |
+
if st.session_state.pdf_processed and st.button(" Re-extract with these settings"):
|
| 660 |
+
st.session_state.pdf_processed = False
|
| 661 |
+
_reset_plot_mapping()
|
| 662 |
+
st.rerun()
|
| 663 |
+
|
| 664 |
+
# 1) Extract plots once (detection + recovery + crop verification)
|
| 665 |
if not st.session_state.pdf_processed:
|
| 666 |
+
engines = ["gemini", "gpt", "claude"] if st.session_state.get("verify_multi") else ["gemini"]
|
| 667 |
+
with st.spinner("Extracting plots from PDFβ¦"
|
| 668 |
+
+ (" (cross-checking with GPT + Claude β this is slower)"
|
| 669 |
+
if st.session_state.get("verify_multi") else "")):
|
| 670 |
+
st.session_state.plot_results = extract_images(
|
| 671 |
+
pdf_path, engines=engines,
|
| 672 |
+
check_missed=st.session_state.get("recover_missed", True),
|
| 673 |
+
)
|
| 674 |
st.session_state.pdf_processed = True
|
| 675 |
+
_reset_plot_mapping()
|
| 676 |
+
|
| 677 |
+
plot_results = st.session_state.plot_results
|
| 678 |
+
if not plot_results:
|
| 679 |
+
st.warning("No plots found in this PDF.")
|
| 680 |
+
return
|
| 681 |
|
| 682 |
plot_results = st.session_state.plot_results
|
| 683 |
if not plot_results:
|
|
|
|
| 702 |
# Downloads (image2's native zipper)
|
| 703 |
d1, d2 = st.columns(2)
|
| 704 |
d1.download_button(
|
| 705 |
+
" Images + metadata (ZIP)",
|
| 706 |
+
data=image2.create_plot_zip(plot_results, include_json=True),
|
| 707 |
file_name=f"{paper_id}_plots.zip", mime="application/zip",
|
| 708 |
use_container_width=True, key="dl_plots_zip",
|
| 709 |
)
|
| 710 |
d2.download_button(
|
| 711 |
+
" Images only (ZIP)",
|
| 712 |
+
data=image2.create_plot_zip(plot_results, include_json=False),
|
| 713 |
file_name=f"{paper_id}_images.zip", mime="application/zip",
|
| 714 |
use_container_width=True, key="dl_images_zip",
|
| 715 |
)
|
|
|
|
| 750 |
|
| 751 |
st.divider()
|
| 752 |
|
| 753 |
+
# 3) Figure-centric review β each figure with delete/remove controls and
|
| 754 |
+
# (after mapping) the property rows linked to it.
|
| 755 |
+
st.caption("Review the crops below. Remove a bad crop with **β**, or drop a whole "
|
| 756 |
+
"figure with **π**. Editing figures clears the current mapping β re-run "
|
| 757 |
+
"mapping afterwards.")
|
| 758 |
for gi, group in enumerate(plot_results):
|
| 759 |
caption = group.get("caption", f"Figure {gi+1}")
|
| 760 |
page = group.get("page", "?")
|
| 761 |
imgs = group.get("image_data", [])
|
| 762 |
|
| 763 |
with st.container(border=True):
|
| 764 |
+
head, dele = st.columns([0.85, 0.15])
|
| 765 |
+
head.markdown(f"**Page {page}** β {caption}")
|
| 766 |
+
if dele.button(" Delete ", key=f"delfig_{gi}", use_container_width=True):
|
| 767 |
+
plot_results.pop(gi)
|
| 768 |
+
st.session_state.plot_results = plot_results
|
| 769 |
+
_reset_plot_mapping()
|
| 770 |
+
st.rerun()
|
| 771 |
|
| 772 |
icols = st.columns(min(len(imgs), 4) or 1)
|
| 773 |
for pos, im in enumerate(imgs):
|
|
|
|
| 777 |
sub = im.get("subplot_label") or ""
|
| 778 |
st.image(arr, channels="BGR", width=200,
|
| 779 |
caption=(sub or None))
|
| 780 |
+
|
| 781 |
v = im.get("verification") or {}
|
| 782 |
act = v.get("majority_action") if isinstance(v, dict) else None
|
| 783 |
if act and act != "keep":
|
| 784 |
+
st.caption(f"crop verdict: **{act}**")
|
| 785 |
+
# per-engine detail when the multi-LLM cross-check ran
|
| 786 |
+
by = v.get("by_engine") if isinstance(v, dict) else None
|
| 787 |
+
if isinstance(by, dict):
|
| 788 |
+
flagged = [e for e, vd in by.items()
|
| 789 |
+
if isinstance(vd, dict) and vd.get("recommended_action") == "discard"]
|
| 790 |
+
if flagged:
|
| 791 |
+
st.caption("flagged non-plot by: " + ", ".join(flagged))
|
| 792 |
+
elif act in (None, "unverified"):
|
| 793 |
+
st.caption("crop verdict: unverified")
|
| 794 |
+
|
| 795 |
+
if st.button("β Remove", key=f"rmimg_{gi}_{pos}", use_container_width=True):
|
| 796 |
+
imgs.pop(pos)
|
| 797 |
+
if not imgs:
|
| 798 |
+
plot_results.pop(gi)
|
| 799 |
+
st.session_state.plot_results = plot_results
|
| 800 |
+
_reset_plot_mapping()
|
| 801 |
+
st.rerun()
|
| 802 |
|
| 803 |
if mapping_done:
|
| 804 |
grp = _links_for_group(links_df, gi)
|
|
|
|
| 845 |
"attaches each crop's path. Push conforms those rows to the chosen "
|
| 846 |
"category table's 33-column schema and uploads them.")
|
| 847 |
|
| 848 |
+
if st.button("Store to database (SQLite)", use_container_width=True):
|
| 849 |
with st.spinner("Saving crops and writing SQLiteβ¦"):
|
| 850 |
st.session_state.store = mapper5.store_properties_with_plots(
|
| 851 |
st.session_state.df_aug,
|
|
|
|
| 933 |
"pdf_extracted_df": pd.DataFrame(),
|
| 934 |
"pdf_extracted_meta": {},
|
| 935 |
"pdf_doi": "",
|
| 936 |
+
"verify_multi": False,
|
| 937 |
+
"recover_missed": True,
|
| 938 |
}
|
| 939 |
for k, v in defaults.items():
|
| 940 |
if k not in st.session_state:
|
|
|
|
| 975 |
st.tabs(["Material Data", "Extracted Plots"])
|
| 976 |
return
|
| 977 |
|
| 978 |
+
tab1, tab2 = st.tabs([" Material Data", " Extracted Plots"])
|
| 979 |
|
| 980 |
tmp_file = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False, prefix="matdb_")
|
| 981 |
try:
|