gvlktejaswi commited on
Commit
571d400
Β·
verified Β·
1 Parent(s): a895626

Update page_files/Upload_Data.py

Browse files
Files changed (1) hide show
  1. page_files/Upload_Data.py +333 -694
page_files/Upload_Data.py CHANGED
@@ -23,7 +23,6 @@ import requests
23
  import streamlit as st
24
  from PIL import Image
25
 
26
-
27
  from dotenv import load_dotenv
28
  load_dotenv()
29
 
@@ -31,7 +30,9 @@ _GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
31
  if not _GEMINI_API_KEY:
32
  raise RuntimeError("GEMINI_API_KEY not set in environment")
33
 
34
- # ── imports from doctodb_rag (data extraction) ────────────────────────────────
 
 
35
  from categorized.Backend.PDF_DataExtraction import (
36
  call_gemini_from_bytes,
37
  convert_to_dataframe,
@@ -39,29 +40,59 @@ from categorized.Backend.PDF_DataExtraction import (
39
  verify_dataframe,
40
  )
41
 
42
- # ── imports from figure_extractor (image extraction) ─────────────────────────
43
- from categorized.Backend.Pdf_ImageExtraction import (
44
- GEMINI_MODEL as GEMINI_MODEL,
45
- get_plot_data_from_llm,
46
- extract_plots,
47
- )
48
-
 
 
 
 
49
  from data_loader import insert_material_rows
50
- from categorized.Backend.plot_property_mapper import (
51
- batch_map_plots,
52
- fetch_properties_for_material,
53
- save_plot_image_mapping,
54
- save_plot_image_to_db,
55
- )
56
- from db import fetch_all
57
 
58
 
59
  # ─────────────────────────────────────────────────────────────────────────────
60
- # Helpers that were previously in upload_backend
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  # ─────────────────────────────────────────────────────────────────────────────
62
 
63
  def _df_to_meta(df: pd.DataFrame) -> dict:
64
- """Re-create the flat metadata dict that the UI previously got from Gemini."""
65
  if df.empty:
66
  return {}
67
  row0 = df.iloc[0]
@@ -71,151 +102,35 @@ def _df_to_meta(df: pd.DataFrame) -> dict:
71
  "material_abbreviation": str(row0.get("material_abbreviation", "")),
72
  "trade_grade": str(row0.get("trade_grade", "")),
73
  "manufacturer": str(row0.get("manufacturer", "")),
 
74
  "mechanical_properties": props,
75
  }
76
 
77
 
78
- def create_zip(image_results: list, include_json: bool = True) -> bytes:
79
- """
80
- Pack extracted plot images (and optional JSON metadata) into a ZIP.
81
- Each item in image_results has: caption, page, image_data (list of dicts
82
- with 'array' (BGR ndarray) and 'filename').
83
- """
84
- buf = io.BytesIO()
85
- with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
86
- meta = []
87
- for item in image_results:
88
- caption = item.get("caption", "")
89
- page = item.get("page", "?")
90
- for img_dict in item.get("image_data", []):
91
- bgr = img_dict.get("array")
92
- filename = img_dict.get("filename", "plot.png")
93
- if bgr is not None:
94
- ok, enc = cv2.imencode(".png", bgr)
95
- if ok:
96
- zf.writestr(filename, enc.tobytes())
97
- if include_json:
98
- meta.append({
99
- "caption": caption,
100
- "page": page,
101
- "image_count": len(item.get("image_data", [])),
102
- "images": [d.get("filename") for d in item.get("image_data", [])],
103
- })
104
- if include_json and meta:
105
- zf.writestr("metadata.json", json.dumps(meta, indent=4))
106
- return buf.getvalue()
107
-
108
-
109
- def save_matched_images(
110
- df: pd.DataFrame,
111
- image_results: list,
112
- save_dir: str = "images",
113
- ) -> list:
114
- """
115
- Heuristically match extracted plot captions to property names in df and
116
- save matched images to disk. Returns list of match-info dicts.
117
- """
118
- os.makedirs(save_dir, exist_ok=True)
119
- saved = []
120
- props = df["property_name"].str.lower().tolist() if "property_name" in df.columns else []
121
-
122
- for item in image_results:
123
- caption = (item.get("caption") or "").lower()
124
- best_prop = None
125
- best_score = 0
126
- for prop in props:
127
- # simple overlap score: shared words
128
- cap_words = set(re.findall(r"\w+", caption))
129
- prop_words = set(re.findall(r"\w+", prop))
130
- score = len(cap_words & prop_words)
131
- if score > best_score:
132
- best_score = score
133
- best_prop = prop
134
-
135
- if best_prop and best_score > 0:
136
- for idx, img_dict in enumerate(item.get("image_data", [])):
137
- bgr = img_dict.get("array")
138
- if bgr is None:
139
- continue
140
- safe_prop = re.sub(r"[^\w\-]", "_", best_prop)
141
- filename = f"{safe_prop}_{idx}.png"
142
- filepath = os.path.join(save_dir, filename)
143
- cv2.imwrite(filepath, bgr)
144
- saved.append({
145
- "property": best_prop,
146
- "caption": item.get("caption", ""),
147
- "path": filepath,
148
- })
149
- return saved
150
-
151
-
152
- def save_single_image_with_property(
153
- bgr: np.ndarray,
154
- property_name: str,
155
- save_dir: str = "images",
156
- ) -> str:
157
- """Save a single BGR image tagged with a property name. Returns filepath."""
158
- os.makedirs(save_dir, exist_ok=True)
159
- safe = re.sub(r"[^\w\-]", "_", property_name)
160
- filepath = os.path.join(save_dir, f"{safe}.png")
161
- cv2.imwrite(filepath, bgr)
162
- return filepath
163
-
164
-
165
  # ─────────────────────────────────────────────────────────────────────────────
166
- # extract_images adapter
167
- # Bridges figure_extractor's extract_plots API to the image_results list shape
168
- # expected by the rest of the UI (list of {caption, page, image_data}).
 
 
169
  # ─────────────────────────────────────────────────────────────────────────────
170
 
171
- _GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "AIzaSyBzyMFKEqcjsWpR-OGAY42T250o1O39v3Y")
172
-
173
  def extract_images(pdf_path: str) -> list:
174
- """
175
- Use figure_extractor to detect and crop plot images from a PDF path.
176
- Returns a list compatible with the image_results shape used throughout the UI:
177
- [{ "caption": str, "page": int, "image_data": [{"array": bgr_ndarray, "filename": str}] }]
178
- """
179
  try:
180
- # gemini_model = init_gemini(_GEMINI_API_KEY)
181
- plot_data = get_plot_data_from_llm( GEMINI_MODEL, pdf_path)
182
- raw_plots = extract_plots(
183
- pdf_path=pdf_path,
184
- plot_data=plot_data,
185
- pad=22,
186
- score_thresh=0.35,
187
  )
 
 
 
 
188
  except Exception as e:
189
  log.error(f"extract_images failed: {e}")
 
190
  return []
191
-
192
-
193
-
194
-
195
- # raw_plots items: {caption, page, path, plot_score, plot_type}
196
- # Convert to image_results shape
197
- image_results = []
198
- for item in raw_plots:
199
- bgr = cv2.imread(item["path"]) if item.get("path") else None
200
- # clean up temp file written by extract_plots
201
- if item.get("path") and os.path.exists(item["path"]):
202
- try:
203
- os.remove(item["path"])
204
- except Exception:
205
- pass
206
-
207
- page = item.get("page", 1)
208
- caption = item.get("caption", f"Figure (page {page})")
209
- safe = re.sub(r"[^\w\-]", "_", caption)[:40]
210
- filename = f"page{page}_{safe}.png"
211
-
212
- image_results.append({
213
- "caption": caption,
214
- "page": page,
215
- "image_data": [{"array": bgr, "filename": filename}] if bgr is not None else [],
216
- })
217
-
218
- return image_results
219
 
220
 
221
  # ─────────────────────────────────────────────────────────────────────────────
@@ -415,20 +330,30 @@ def render_top_bar():
415
 
416
 
417
  # ─────────────────────────────────────────────────────────────────────────────
418
- # Helpers for tab2 mapping UI
419
  # ─────────────────────────────────────────────────────────────────────────────
420
 
421
  def _confidence_badge(conf: str) -> str:
422
  colors = {"high": "#16a34a", "medium": "#d97706", "low": "#dc2626"}
423
  c = colors.get((conf or "low").lower(), "#6b7280")
424
- return (
425
- f"<span class='conf-badge' style='background:{c}'>"
426
- f"{conf.upper()}</span>"
427
- )
 
 
 
 
 
 
 
 
 
 
428
 
429
 
430
  # ─────────────────────────────────────────────────────────────────────────────
431
- # Manual input form
432
  # ─────────────────────────────────────────────────────────────────────────────
433
 
434
  def input_form():
@@ -576,101 +501,18 @@ def input_form():
576
 
577
 
578
  # ─────────────────────────────────────────────────────────────────────────────
579
- # Tab 1: Material Data
580
- # Uses run_pipeline from doctodb_rag instead of call_gemini_from_bytes
581
  # ─────────────────────────────────────────────────────────────────────────────
582
 
583
- # def render_material_data_tab(pdf_path: str):
584
- # st.subheader("Material Properties Data")
585
-
586
- # if not st.session_state.pdf_data_extracted:
587
- # with st.spinner("Extracting material data…"):
588
- # with open(pdf_path, "rb") as f:
589
- # pdf_bytes = f.read()
590
-
591
-
592
- # df, df_gemini, df_gpt, _chunks, api_errors, meta = run_pipeline(pdf_bytes)
593
-
594
- # if api_errors:
595
- # for err in api_errors:
596
- # st.warning(err)
597
-
598
- # if not df.empty:
599
- # # Build the metadata dict that the rest of the UI expects
600
- # data = _df_to_meta(df)
601
- # st.session_state.pdf_extracted_df = df
602
- # st.session_state.pdf_data_extracted = True
603
- # st.session_state.pdf_extracted_meta = data
604
- # else:
605
- # st.warning("No data extracted from PDF.")
606
-
607
- # df = st.session_state.pdf_extracted_df
608
-
609
- # if df.empty:
610
- # return
611
-
612
- # meta = st.session_state.get("pdf_extracted_meta", {})
613
- # st.success(f"Extracted {len(df)} properties")
614
-
615
- # col1, col2 = st.columns(2)
616
- # col1.metric("Material", meta.get("material_name", "N/A"))
617
- # col2.metric("Abbreviation", meta.get("material_abbreviation", "N/A"))
618
-
619
- # st.dataframe(df, use_container_width=True, height=400)
620
- # st.subheader("Assign Material Category")
621
-
622
- # extracted_material_class = st.selectbox(
623
- # "Select category for this material",
624
- # ["Polymer", "Fiber", "Composite"],
625
- # index=None,
626
- # placeholder="Required before adding to database",
627
- # key="tab1_material_class",
628
- # )
629
-
630
- # if st.button("+ Add to Database"):
631
- # if not extracted_material_class:
632
- # st.error("Please select a material category before adding.")
633
- # return
634
-
635
- # df["material_class"] = extracted_material_class
636
- # df["material_type"] = extracted_material_class
637
-
638
- # if st.session_state.image_results:
639
- # with st.spinner("Saving matched plot images…"):
640
- # saved_images = save_matched_images(
641
- # df, st.session_state.image_results, save_dir="images"
642
- # )
643
- # if saved_images:
644
- # st.success(f"Saved {len(saved_images)} plot image(s)")
645
- # with st.expander("View saved images"):
646
- # for img_info in saved_images:
647
- # st.write(f"**{img_info['property']}** β†’ {img_info['caption']}")
648
- # st.write(f"Saved to: `{img_info['path']}`")
649
- # else:
650
- # st.info("No plots matched the extracted properties automatically.")
651
-
652
- # st.session_state.setdefault("user_uploaded_data", pd.DataFrame())
653
- # st.session_state["user_uploaded_data"] = pd.concat(
654
- # [st.session_state["user_uploaded_data"], df], ignore_index=True
655
- # )
656
- # st.success(f"Added to {extracted_material_class} database!")
657
- # ── Stage labels and estimated durations for the progress display ─────────────
658
  _STAGE_LABELS = {
659
  0.00: ("Checking cache", 2),
660
- 0.05: ("Extracting tables & text", 15),
661
- 0.20: ("Extraction complete", 0),
662
- 0.25: ("Indexing into ChromaDB", 8),
663
- 0.40: ("Ranking chunks", 5),
664
- 0.50: ("Ranking complete", 0),
665
- 0.55: ("Building batches", 2),
666
- 0.60: ("Running Gemini + GPT-4o", 30),
667
- 0.90: ("Merging results", 3),
668
- 0.95: ("Consensus filtering", 4),
669
  1.00: ("Done", 0),
670
  }
671
 
672
- def _nearest_stage_label(pct: float) -> tuple[str, int]:
673
- """Return (label, est_seconds_remaining) for the closest stage."""
674
  best_key = min(_STAGE_LABELS, key=lambda k: abs(k - pct))
675
  return _STAGE_LABELS[best_key]
676
 
@@ -679,28 +521,23 @@ def render_material_data_tab(pdf_path: str):
679
  st.subheader("Material Properties Data")
680
 
681
  if not st.session_state.pdf_data_extracted:
682
-
683
- bar = st.progress(0.0)
684
- status = st.empty() # stage label + ETA
685
- timer = st.empty() # elapsed clock
686
-
687
  start_ts = time.time()
688
 
689
  def _cb(msg: str, pct: float):
690
- elapsed = time.time() - start_ts
691
  label, est_remaining = _nearest_stage_label(pct)
692
  bar.progress(min(pct, 1.0))
693
  status.markdown(
694
  f"**{label}** &nbsp;Β·&nbsp; <span style='color:#64748b'>{msg}</span>",
695
  unsafe_allow_html=True,
696
  )
697
- if est_remaining > 0:
698
- timer.caption(
699
- f"⏱ Elapsed: {elapsed:.0f}s &nbsp;·&nbsp; "
700
- f"Est. remaining: ~{est_remaining}s"
701
- )
702
- else:
703
- timer.caption(f"⏱ Elapsed: {elapsed:.0f}s")
704
 
705
  with open(pdf_path, "rb") as f:
706
  pdf_bytes = f.read()
@@ -708,37 +545,31 @@ def render_material_data_tab(pdf_path: str):
708
  _cb("Extracting via Gemini…", 0.30)
709
  data = call_gemini_from_bytes(pdf_bytes)
710
  df = convert_to_dataframe(data)
711
- api_errors = []
 
 
 
 
 
 
712
 
713
  if not df.empty:
714
  _cb("Verifying against source PDF…", 0.70)
715
  sentences = _extract_sentences(pdf_bytes)
716
  df = verify_dataframe(df, sentences)
717
 
718
- row0 = df.iloc[0] if not df.empty else {}
719
- meta = {
720
- "material_name": str(row0.get("material_name", "")) if not df.empty else "",
721
- "material_abbreviation": str(row0.get("material_abbreviation", "")) if not df.empty else "",
722
- }
723
  _cb("Done.", 1.0)
724
  elapsed_total = time.time() - start_ts
725
  bar.progress(1.0)
726
  status.empty()
727
  timer.empty()
728
 
729
- if api_errors:
730
- for err in api_errors:
731
- st.warning(err)
732
-
733
  if not df.empty:
734
- data = _df_to_meta(df)
735
  st.session_state.pdf_extracted_df = df
736
  st.session_state.pdf_data_extracted = True
737
- st.session_state.pdf_extracted_meta = data
738
- st.success(
739
- f"βœ… Extracted {len(df)} properties in {elapsed_total:.0f}s"
740
- + (f" Β· {meta.get('batches', '?')} batch(es)" if meta.get('batches') else "")
741
- )
742
  else:
743
  st.warning("No data extracted from PDF.")
744
  return
@@ -748,444 +579,258 @@ def render_material_data_tab(pdf_path: str):
748
  return
749
 
750
  meta = st.session_state.get("pdf_extracted_meta", {})
 
751
 
752
- col1, col2 = st.columns(2)
753
- col1.metric("Material", meta.get("material_name", "N/A"))
754
- col2.metric("Abbreviation", meta.get("material_abbreviation", "N/A"))
 
755
 
756
  st.dataframe(df, use_container_width=True, height=400)
757
- st.subheader("Assign Material Category")
758
 
759
- extracted_material_class = st.selectbox(
760
- "Select category for this material",
761
- ["Polymer", "Fiber", "Composite"],
 
762
  index=None,
763
- placeholder="Required before adding to database",
764
- key="tab1_material_class",
 
 
765
  )
 
 
 
 
 
766
 
767
- if st.button("+ Add to Database"):
768
- if not extracted_material_class:
769
- st.error("Please select a material category before adding.")
770
- return
771
 
772
- df["material_class"] = extracted_material_class
773
- df["material_type"] = extracted_material_class
 
774
 
775
- if st.session_state.image_results:
776
- with st.spinner("Saving matched plot images…"):
777
- saved_images = save_matched_images(
778
- df, st.session_state.image_results, save_dir="images"
779
- )
780
- if saved_images:
781
- st.success(f"Saved {len(saved_images)} plot image(s)")
782
- with st.expander("View saved images"):
783
- for img_info in saved_images:
784
- st.write(f"**{img_info['property']}** β†’ {img_info['caption']}")
785
- st.write(f"Saved to: `{img_info['path']}`")
786
- else:
787
- st.info("No plots matched the extracted properties automatically.")
788
 
789
- st.session_state.setdefault("user_uploaded_data", pd.DataFrame())
790
- st.session_state["user_uploaded_data"] = pd.concat(
791
- [st.session_state["user_uploaded_data"], df], ignore_index=True
792
- )
793
- st.success(f"Added to {extracted_material_class} database!")
794
 
795
- # ───────────────────────��─────────────────────────────────────────────────────
796
- # Tab 2: Extracted Plots + AI Property Mapping
797
- # Uses extract_images (adapter above) instead of upload_backend's version
798
- # ─────────────────────────────────────────────────────────────────────────────
 
799
 
800
  def render_plots_tab(pdf_path: str, paper_id: str):
801
  st.subheader("Extracted Plot Images & Property Mapping")
802
 
803
-
804
  if not st.session_state.pdf_processed:
805
  with st.spinner("Extracting plots from PDF…"):
806
- st.session_state.image_results = extract_images(pdf_path)
807
  st.session_state.pdf_processed = True
808
  st.session_state.mapping_done = False
 
 
 
809
 
810
- image_results = st.session_state.image_results
811
-
812
- if not image_results:
813
  st.warning("No plots found in this PDF.")
814
  return
815
 
816
- has_data = not st.session_state.pdf_extracted_df.empty
 
 
817
 
 
818
  if has_data:
819
- mat_abbr = st.session_state.pdf_extracted_df.iloc[0]["material_abbreviation"]
820
- property_list = st.session_state.pdf_extracted_df["property_name"].unique().tolist()
821
  st.info(
822
- f"**{len(image_results)} plots** extracted | "
823
- f"Material: **{mat_abbr}** | "
824
- f"{len(property_list)} properties available for mapping"
825
  )
826
  else:
827
- st.warning(
828
- "Extract material data in the **Material Data** tab first "
829
- "to enable AI property mapping."
830
- )
831
-
832
- subtab_images, subtab_json = st.tabs(["πŸ–Ό Images & Mapping", "{ } JSON Preview"])
833
-
834
- # ════════════════════════════════════════════════════════════════════════
835
- with subtab_images:
836
-
837
- col_img, col_json_dl, col_all = st.columns(3)
838
- with col_img:
839
- st.download_button(
840
- "⬇ Images Only",
841
- data=create_zip(image_results, include_json=False),
842
- file_name=f"{paper_id}_images.zip",
843
- mime="application/zip",
844
- use_container_width=True,
845
- key="dl_images",
846
- )
847
- with col_json_dl:
848
- json_meta = [
849
- {"caption": r["caption"], "page": r["page"],
850
- "image_count": len(r["image_data"])}
851
- for r in image_results
852
- ]
853
- st.download_button(
854
- "⬇ JSON",
855
- data=json.dumps(json_meta, indent=4),
856
- file_name=f"{paper_id}_metadata.json",
857
- mime="application/json",
858
- use_container_width=True,
859
- key="dl_json",
860
- )
861
- with col_all:
862
- st.download_button(
863
- "⬇ Download All",
864
- data=create_zip(image_results, include_json=True),
865
- file_name=f"{paper_id}_complete.zip",
866
- mime="application/zip",
867
- use_container_width=True,
868
- key="dl_all",
869
- )
870
-
871
- st.divider()
872
-
873
- if has_data:
874
- col_cls, col_btn = st.columns([0.45, 0.55])
875
-
876
- with col_cls:
877
- map_class = st.selectbox(
878
- "Material class for DB lookup",
879
- ["Polymer", "Fiber", "Composite"],
880
- key="mapping_material_class",
881
- help="Routes to the correct PostgreSQL table.",
882
- )
883
-
884
- with col_btn:
885
- st.write("")
886
- st.write("")
887
- run_mapping = st.button(
888
- "πŸ€– Run AI Property Mapping",
889
- type="primary",
890
- disabled=st.session_state.get("mapping_done", False),
891
- use_container_width=True,
892
- )
893
-
894
- if run_mapping:
895
- df = st.session_state.pdf_extracted_df
896
- mat_abbr = df.iloc[0]["material_abbreviation"]
897
- extracted_json = st.session_state.get("pdf_extracted_meta", {})
898
-
899
- with st.spinner("Fetching properties from PostgreSQL…"):
900
- try:
901
- db_properties = fetch_properties_for_material(
902
- mat_abbr, map_class, fetch_all
903
- )
904
- except Exception as exc:
905
- st.error(f"DB error: {exc}")
906
- db_properties = []
907
-
908
- if not db_properties:
909
- st.warning(
910
- f"No DB rows found for **{mat_abbr}** in the **{map_class}** table. "
911
- "Mapping will use all available properties from the extracted data."
912
- )
913
-
914
- prog = st.progress(0, text="Starting…")
915
-
916
- def _on_progress(i, total, caption):
917
- pct = int((i / max(total, 1)) * 100)
918
- prog.progress(pct, text=f"Mapping {i+1}/{total}: {caption[:55]}…")
919
-
920
- with st.spinner("AI is analysing plots…"):
921
- mapped = batch_map_plots(
922
- image_results=image_results,
923
- extracted_json=extracted_json,
924
- db_properties=db_properties,
925
- progress_callback=_on_progress,
926
- )
927
-
928
- prog.progress(100, text="Done βœ“")
929
- st.session_state.mapped_results = mapped
930
- st.session_state.mapping_done = True
931
- st.success(f"βœ… Mapped {len(mapped)} plots β€” review below.")
932
- st.rerun()
933
-
934
- if st.session_state.get("mapping_done"):
935
- col_info, col_reset = st.columns([0.78, 0.22])
936
- col_info.caption(
937
- "AI mapping complete. The dropdown for each plot is pre-filled "
938
- "with the suggestion β€” override freely, then hit **Save**."
939
- )
940
- if col_reset.button("β†Ί Re-run Mapping", use_container_width=True):
941
- st.session_state.mapping_done = False
942
- st.session_state.mapped_results = []
943
- st.rerun()
944
 
945
- st.divider()
946
 
947
- use_mapped = (
948
- has_data
949
- and st.session_state.get("mapping_done", False)
950
- and bool(st.session_state.get("mapped_results"))
951
- )
952
- display_list = (
953
- st.session_state.mapped_results if use_mapped else image_results
 
954
  )
 
 
 
 
 
 
 
955
 
956
- for idx in range(len(display_list)):
957
- if idx >= len(display_list):
958
- break
959
-
960
- item = display_list[idx]
961
- caption = item.get("caption", f"Figure {idx+1}")
962
- page = item.get("page", "?")
963
- img_list = item.get("image_data", [])
964
- mapping = item.get("mapping_result") if use_mapped else None
965
-
966
- with st.container(border=True):
967
-
968
- col_cap, col_del = st.columns([0.87, 0.13])
969
- col_cap.markdown(f"**Page {page}** β€” {caption}")
970
- if col_del.button("πŸ—‘", key=f"del_grp_{idx}", help="Delete this figure"):
971
- display_list.pop(idx)
972
- if use_mapped:
973
- st.session_state.mapped_results = display_list
974
- else:
975
- st.session_state.image_results = display_list
976
- st.rerun()
977
-
978
- if mapping:
979
- prop_name = mapping.get("property_name", "")
980
- section = mapping.get("section", "")
981
- confidence = mapping.get("confidence", "low")
982
- reasoning = mapping.get("reasoning", "")
983
- db_row = mapping.get("db_row")
984
- candidates = mapping.get("all_candidates", [])
985
-
986
- if prop_name:
987
- badge = _confidence_badge(confidence)
988
- st.markdown(
989
- f"πŸ”— **AI Match:** `{section}` β€Ί **{prop_name}** &nbsp; {badge}",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
990
  unsafe_allow_html=True,
991
  )
992
- if reasoning:
993
- st.caption(f"πŸ’¬ {reasoning}")
994
-
995
- if db_row:
996
- with st.expander("πŸ“‹ Matched DB row", expanded=False):
997
- c1, c2, c3 = st.columns(3)
998
- c1.metric("Value", db_row.get("value", "β€”"))
999
- c2.metric("Unit", db_row.get("unit", "β€”"))
1000
- c3.metric("Condition", db_row.get("test_condition", "β€”"))
1001
- if db_row.get("comments"):
1002
- st.caption(f"Comments: {db_row['comments']}")
1003
- if db_row.get("english"):
1004
- st.caption(f"English units: {db_row['english']}")
1005
-
1006
- if candidates:
1007
- with st.expander("πŸ”„ All candidates", expanded=False):
1008
- for c in candidates:
1009
- st.markdown(
1010
- f"{c.get('rank','?')}. `{c.get('section','?')}` β€Ί "
1011
- f"**{c.get('property_name','?')}** &nbsp; "
1012
- f"{_confidence_badge(c.get('confidence','low'))}",
1013
- unsafe_allow_html=True,
1014
- )
1015
- else:
1016
- st.warning("⚠️ AI could not match this plot to any DB property.")
1017
-
1018
- for p_idx in range(len(img_list)):
1019
- if p_idx >= len(item.get("image_data", [])):
1020
- break
1021
-
1022
- img_data = item["image_data"][p_idx]
1023
- bgr = img_data.get("array")
1024
- if bgr is None:
1025
- continue
1026
-
1027
- img_key = f"{idx}_{p_idx}_{page}"
1028
- st.image(bgr, channels="BGR", width=420)
1029
-
1030
- if has_data:
1031
- df = st.session_state.pdf_extracted_df
1032
- mat_abbr = df.iloc[0]["material_abbreviation"]
1033
- property_list = df["property_name"].unique().tolist()
1034
- options = ["β€” Select property β€”"] + property_list
1035
-
1036
- ai_prop = mapping.get("property_name", "") if mapping else ""
1037
- ai_section = mapping.get("section", "") if mapping else ""
1038
- default_idx = (
1039
- property_list.index(ai_prop) + 1
1040
- if ai_prop in property_list else 0
1041
- )
1042
-
1043
- col_sel, col_sec, col_save, col_rem = st.columns(
1044
- [0.40, 0.20, 0.20, 0.20]
1045
- )
1046
-
1047
- with col_sel:
1048
- selected = st.selectbox(
1049
- "Property",
1050
- options=options,
1051
- index=default_idx,
1052
- key=f"prop_sel_{img_key}",
1053
- label_visibility="collapsed",
1054
  )
1055
-
1056
- with col_sec:
1057
- section_options = [
1058
- "Mechanical",
1059
- "Thermal",
1060
- "Processing",
1061
- "Physical",
1062
- "Descriptive",
1063
- "Composition / Reinforcement",
1064
- "Architecture / Structure",
1065
- ]
1066
- section_default = (
1067
- section_options.index(ai_section)
1068
- if ai_section in section_options
1069
- else 0
1070
- )
1071
- section_val = st.selectbox(
1072
- "Section",
1073
- options=section_options,
1074
- index=section_default,
1075
- key=f"sec_{img_key}",
1076
- label_visibility="collapsed",
1077
  )
 
1078
 
1079
- with col_save:
1080
- if st.button("πŸ’Ύ Save", key=f"save_{img_key}",
1081
- use_container_width=True):
1082
- if selected and selected != "β€” Select property β€”":
1083
-
1084
- filepath = save_plot_image_mapping(
1085
- mat_abbr, selected, section_val,
1086
- bgr, save_dir="images",
1087
- )
1088
-
1089
- try:
1090
- from db import execute_query
1091
- saved_to_db = save_plot_image_to_db(
1092
- material_abbr=mat_abbr,
1093
- property_name=selected,
1094
- image_bgr=bgr,
1095
- material_class=st.session_state.get(
1096
- "mapping_material_class", "Polymer"
1097
- ),
1098
- execute_query_fn=execute_query,
1099
- )
1100
- if saved_to_db:
1101
- st.success(
1102
- f"βœ… Saved to DB & disk β†’ "
1103
- f"`{os.path.basename(filepath)}`"
1104
- )
1105
- else:
1106
- st.warning(
1107
- "⚠️ Saved to disk only β€” "
1108
- "no matching DB row found for this property."
1109
- )
1110
- except Exception as e:
1111
- st.error(f"DB save failed: {e}")
1112
- st.info(f"Saved locally β†’ `{os.path.basename(filepath)}`")
1113
-
1114
- st.session_state.saved_image_mapping[img_key] = {
1115
- "property": selected,
1116
- "section": section_val,
1117
- "caption": caption,
1118
- "filename": os.path.basename(filepath),
1119
- "path": filepath,
1120
- }
1121
- st.rerun()
1122
- else:
1123
- st.warning("Select a property first.")
1124
-
1125
- with col_rem:
1126
- if st.button("βœ•", key=f"rem_{img_key}",
1127
- use_container_width=True, help="Remove image"):
1128
- if img_key in st.session_state.saved_image_mapping:
1129
- del st.session_state.saved_image_mapping[img_key]
1130
- item["image_data"].pop(p_idx)
1131
- if not item["image_data"]:
1132
- display_list.pop(idx)
1133
- if use_mapped:
1134
- st.session_state.mapped_results = display_list
1135
- else:
1136
- st.session_state.image_results = display_list
1137
- st.rerun()
1138
-
1139
- if img_key in st.session_state.saved_image_mapping:
1140
- saved_m = st.session_state.saved_image_mapping[img_key]
1141
- st.info(
1142
- f"βœ… Saved as **{saved_m['property']}** β†’ "
1143
- f"`{saved_m['filename']}`"
1144
- )
1145
 
1146
- else:
1147
- col_msg, col_rem = st.columns([0.80, 0.20])
1148
- col_msg.caption(
1149
- "Go to **Material Data** tab to extract properties and enable mapping."
1150
- )
1151
- if col_rem.button("βœ•", key=f"rem_nd_{img_key}", help="Remove"):
1152
- item["image_data"].pop(p_idx)
1153
- if not item["image_data"]:
1154
- st.session_state.image_results.pop(idx)
1155
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1156
 
1157
- st.divider()
1158
-
1159
- saved_map = st.session_state.saved_image_mapping
1160
- if saved_map:
1161
- with st.expander(f"πŸ“ Saved mappings ({len(saved_map)})", expanded=False):
1162
- for key, info in saved_map.items():
1163
- st.markdown(
1164
- f"**{info['property']}** &nbsp;β€Ί&nbsp; `{info['filename']}` \n"
1165
- f"<small style='color:#64748b'>Caption: {info['caption']}</small>",
1166
- unsafe_allow_html=True,
1167
- )
1168
-
1169
- # ═════��══════════════════════════════════════════════════════════════════
1170
- with subtab_json:
1171
- st.subheader("Metadata Preview")
1172
- json_data = [
1173
- {
1174
- "caption": r["caption"],
1175
- "page": r["page"],
1176
- "image_count": len(r["image_data"]),
1177
- "images": [img["filename"] for img in r["image_data"]],
1178
- }
1179
- for r in image_results
1180
- ]
1181
- st.download_button(
1182
- "⬇ Download JSON",
1183
- data=json.dumps(json_data, indent=4),
1184
- file_name="metadata.json",
1185
- mime="application/json",
1186
- key="dl_json_bottom",
1187
  )
1188
- st.json(json_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1189
 
1190
 
1191
  # ─────────────────────────────────────────────────────────────────────────────
@@ -1200,16 +845,19 @@ def main():
1200
  st.caption("Provide technical data and research documentation for the central repository.")
1201
 
1202
  defaults = {
1203
- "image_results": [],
1204
- "mapped_results": [],
1205
- "pdf_processed": False,
1206
- "mapping_done": False,
1207
- "current_pdf_name": None,
1208
- "form_submitted": False,
1209
- "pdf_data_extracted": False,
1210
- "pdf_extracted_df": pd.DataFrame(),
1211
- "pdf_extracted_meta": {},
1212
- "saved_image_mapping": {},
 
 
 
1213
  }
1214
  for k, v in defaults.items():
1215
  if k not in st.session_state:
@@ -1246,19 +894,13 @@ def main():
1246
 
1247
  if st.session_state.form_submitted:
1248
  st.session_state.form_submitted = False
1249
- st.info(
1250
- "Form submitted. Previously extracted data has been saved. "
1251
- "Upload again to process a new PDF."
1252
- )
1253
  st.tabs(["Material Data", "Extracted Plots"])
1254
  return
1255
 
1256
- tab1, tab2 = st.tabs(["πŸ“Š Material Data", "πŸ–Ό Extracted Plots"])
1257
 
1258
- # Write to a stable temp file (avoids Windows WinError 267 on cleanup)
1259
- tmp_file = tempfile.NamedTemporaryFile(
1260
- suffix=".pdf", delete=False, prefix="matdb_"
1261
- )
1262
  try:
1263
  tmp_file.write(uploaded_file.getbuffer())
1264
  tmp_file.flush()
@@ -1267,10 +909,8 @@ def main():
1267
 
1268
  with tab1:
1269
  render_material_data_tab(pdf_path)
1270
-
1271
  with tab2:
1272
  render_plots_tab(pdf_path, paper_id)
1273
-
1274
  finally:
1275
  try:
1276
  os.unlink(tmp_file.name)
@@ -1278,5 +918,4 @@ def main():
1278
  pass
1279
 
1280
 
1281
- main()
1282
-
 
23
  import streamlit as st
24
  from PIL import Image
25
 
 
26
  from dotenv import load_dotenv
27
  load_dotenv()
28
 
 
30
  if not _GEMINI_API_KEY:
31
  raise RuntimeError("GEMINI_API_KEY not set in environment")
32
 
33
+ # ── Data extraction (Code 1) ─────────────────────────────────────────────────
34
+ # NOTE: DOI is injected locally below (see _extract_doi_from_pdf). If you want
35
+ # the DOI produced inside PDF_DataExtraction itself, port the same regex there.
36
  from categorized.Backend.PDF_DataExtraction import (
37
  call_gemini_from_bytes,
38
  convert_to_dataframe,
 
40
  verify_dataframe,
41
  )
42
 
43
+ # ── New integrated stack: image2 β†’ mapper5 β†’ category_push ────────────────────
44
+ # These are imported as top-level modules (the same way mapper5 imports image2
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
+ import image2
50
+ import mapper5
51
+ import category_push
52
+
53
+ # Manual-entry path (unchanged β€” writes via the existing data_loader).
54
  from data_loader import insert_material_rows
 
 
 
 
 
 
 
55
 
56
 
57
  # ─────────────────────────────────────────────────────────────────────────────
58
+ # DOI extraction β€” regex over raw PDF text (format-verified, precision-first)
59
+ # ─────────────────────────────────────────────────────────────────────────────
60
+
61
+ _DOI_CORE_RE = re.compile(r'10\.\d{4,9}/[^\s"<>\]\)]+', re.IGNORECASE)
62
+
63
+
64
+ def _clean_doi(doi: str) -> str:
65
+ if not doi:
66
+ return ""
67
+ doi = doi.strip()
68
+ doi = re.sub(r'^(?:https?://)?(?:dx\.)?doi\.org/', '', doi, flags=re.IGNORECASE)
69
+ doi = re.sub(r'^doi[:\s]+', '', doi, flags=re.IGNORECASE)
70
+ return doi.rstrip(' .,;:)]}>"\'')
71
+
72
+
73
+ def _extract_doi_from_pdf(pdf_bytes: bytes) -> str:
74
+ """Prefer a doi.org URL, then a 'doi:' label, then any bare 10.xxxx/ token."""
75
+ try:
76
+ with fitz.open(stream=pdf_bytes, filetype="pdf") as doc:
77
+ text = "\n".join((p.get_text("text") or "") for p in doc)
78
+ except Exception:
79
+ return ""
80
+ m = re.search(r'(?:https?://)?(?:dx\.)?doi\.org/(10\.\d{4,9}/[^\s"<>\]\)]+)', text, re.IGNORECASE)
81
+ if m:
82
+ return _clean_doi(m.group(1))
83
+ m = re.search(r'\bdoi[:\s]+\s*(10\.\d{4,9}/[^\s"<>\]\)]+)', text, re.IGNORECASE)
84
+ if m:
85
+ return _clean_doi(m.group(1))
86
+ m = _DOI_CORE_RE.search(text)
87
+ return _clean_doi(m.group(0)) if m else ""
88
+
89
+
90
+ # ─────────────────────────────────────────────────────────────────────────────
91
+ # Metadata helper
92
  # ─────────────────────────────────────────────────────────────────────────────
93
 
94
  def _df_to_meta(df: pd.DataFrame) -> dict:
95
+ """Re-create the flat metadata dict the UI expects."""
96
  if df.empty:
97
  return {}
98
  row0 = df.iloc[0]
 
102
  "material_abbreviation": str(row0.get("material_abbreviation", "")),
103
  "trade_grade": str(row0.get("trade_grade", "")),
104
  "manufacturer": str(row0.get("manufacturer", "")),
105
+ "doi": str(row0.get("doi", "")),
106
  "mechanical_properties": props,
107
  }
108
 
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  # ─────────────────────────────────────────────────────────────────────────────
111
+ # extract_images adapter β€” now a thin wrapper over image2.extract_and_verify_plots
112
+ # Returns image2's native shape:
113
+ # [{caption, page, image_data:[{array, bytes, filename, subplot_label,
114
+ # subplot_caption, source, verification}]}]
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
+ plot_results, coverage = image2.extract_and_verify_plots(
123
+ pdf_bytes, verify_engines=["gemini"]
 
 
 
124
  )
125
+ # Drop captions naming photos/micrographs/logos and crops the verifier
126
+ # marked "discard" (keeps "keep"/"recrop"). Pure selection, non-mutating.
127
+ plot_results = mapper5.select_real_plots(plot_results)
128
+ st.session_state["plot_coverage"] = coverage
129
  except Exception as e:
130
  log.error(f"extract_images failed: {e}")
131
+ st.session_state["plot_coverage"] = {}
132
  return []
133
+ return plot_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
 
136
  # ─────────────────────────────────────────────────────────────────────────────
 
330
 
331
 
332
  # ─────────────────────────────────────────────────────────────────────────────
333
+ # Helpers for the mapping UI
334
  # ─────────────────────────────────────────────────────────────────────────────
335
 
336
  def _confidence_badge(conf: str) -> str:
337
  colors = {"high": "#16a34a", "medium": "#d97706", "low": "#dc2626"}
338
  c = colors.get((conf or "low").lower(), "#6b7280")
339
+ return f"<span class='conf-badge' style='background:{c}'>{(conf or '').upper()}</span>"
340
+
341
+
342
+ def _score_confidence(score) -> str:
343
+ """Map a numeric match_score to a coarse confidence label for the badge."""
344
+ try:
345
+ s = float(score)
346
+ except (TypeError, ValueError):
347
+ return "low"
348
+ if s >= 0.75:
349
+ return "high"
350
+ if s >= 0.45:
351
+ return "medium"
352
+ return "low"
353
 
354
 
355
  # ─────────────────────────────────────────────────────────────────────────────
356
+ # Manual input form (unchanged β€” writes via data_loader.insert_material_rows)
357
  # ─────────────────────────────────────────────────────────────────────────────
358
 
359
  def input_form():
 
501
 
502
 
503
  # ─────────────────────────────────────────────────────────────────────────────
504
+ # Tab 1: Material Data (extraction + DOI + RDS category selection)
 
505
  # ─────────────────────────────────────────────────────────────────────────────
506
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  _STAGE_LABELS = {
508
  0.00: ("Checking cache", 2),
509
+ 0.30: ("Extracting via Gemini", 20),
510
+ 0.70: ("Verifying against source", 8),
 
 
 
 
 
 
 
511
  1.00: ("Done", 0),
512
  }
513
 
514
+
515
+ def _nearest_stage_label(pct: float):
516
  best_key = min(_STAGE_LABELS, key=lambda k: abs(k - pct))
517
  return _STAGE_LABELS[best_key]
518
 
 
521
  st.subheader("Material Properties Data")
522
 
523
  if not st.session_state.pdf_data_extracted:
524
+ bar = st.progress(0.0)
525
+ status = st.empty()
526
+ timer = st.empty()
 
 
527
  start_ts = time.time()
528
 
529
  def _cb(msg: str, pct: float):
530
+ elapsed = time.time() - start_ts
531
  label, est_remaining = _nearest_stage_label(pct)
532
  bar.progress(min(pct, 1.0))
533
  status.markdown(
534
  f"**{label}** &nbsp;Β·&nbsp; <span style='color:#64748b'>{msg}</span>",
535
  unsafe_allow_html=True,
536
  )
537
+ timer.caption(
538
+ f"⏱ Elapsed: {elapsed:.0f}s"
539
+ + (f" Β· Est. remaining: ~{est_remaining}s" if est_remaining > 0 else "")
540
+ )
 
 
 
541
 
542
  with open(pdf_path, "rb") as f:
543
  pdf_bytes = f.read()
 
545
  _cb("Extracting via Gemini…", 0.30)
546
  data = call_gemini_from_bytes(pdf_bytes)
547
  df = convert_to_dataframe(data)
548
+
549
+ # DOI β€” regex over the PDF text, attached to every row. doi_url is the
550
+ # column category_push preserves into <table>_extras.
551
+ doi = _extract_doi_from_pdf(pdf_bytes)
552
+ if not df.empty:
553
+ df["doi"] = doi
554
+ df["doi_url"] = doi
555
 
556
  if not df.empty:
557
  _cb("Verifying against source PDF…", 0.70)
558
  sentences = _extract_sentences(pdf_bytes)
559
  df = verify_dataframe(df, sentences)
560
 
 
 
 
 
 
561
  _cb("Done.", 1.0)
562
  elapsed_total = time.time() - start_ts
563
  bar.progress(1.0)
564
  status.empty()
565
  timer.empty()
566
 
 
 
 
 
567
  if not df.empty:
 
568
  st.session_state.pdf_extracted_df = df
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
 
579
  return
580
 
581
  meta = st.session_state.get("pdf_extracted_meta", {})
582
+ doi = st.session_state.get("pdf_doi", "")
583
 
584
+ c1, c2, c3 = st.columns(3)
585
+ c1.metric("Material", meta.get("material_name", "N/A"))
586
+ c2.metric("Abbreviation", meta.get("material_abbreviation", "N/A"))
587
+ c3.metric("DOI", doi or "β€”")
588
 
589
  st.dataframe(df, use_container_width=True, height=400)
 
590
 
591
+ st.subheader("Assign Material Category")
592
+ st.selectbox(
593
+ "Category table (routes to the RDS table on push)",
594
+ category_push.CATEGORY_TABLES, # Composites_materials / Fibers / Polymers
595
  index=None,
596
+ placeholder="Required before pushing to the database",
597
+ key="push_category",
598
+ help="This is the RDS table the mapped rows are pushed into (from the "
599
+ "Extracted Plots tab).",
600
  )
601
+ if st.session_state.get("push_category"):
602
+ st.caption(
603
+ f"Category **{st.session_state['push_category']}** selected. "
604
+ "Go to the **Extracted Plots** tab to map figures and push to RDS."
605
+ )
606
 
 
 
 
 
607
 
608
+ # ─────────────────────────────────────────────────────────────────────────────
609
+ # Tab 2: Extracted Plots β†’ map (mapper5) β†’ store (SQLite) β†’ push (category_push)
610
+ # ─────────────────────────────────────────────────────────────────────────────
611
 
612
+ _OUT_DIR = os.getenv("AIM_OUT_DIR", os.path.abspath("./aim_efrc_db"))
613
+ _SOURCE_TABLE = "gemini_verified" # provenance label for the stored rows
 
 
 
 
 
 
 
 
 
 
 
614
 
 
 
 
 
 
615
 
616
+ def _links_for_group(links_df: pd.DataFrame, gi: int) -> pd.DataFrame:
617
+ if links_df is None or links_df.empty or "group_idx" not in links_df.columns:
618
+ return 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
+ # 1) Extract plots once (image2 detection + recovery + crop verification)
626
  if not st.session_state.pdf_processed:
627
  with st.spinner("Extracting plots from PDF…"):
628
+ st.session_state.plot_results = extract_images(pdf_path)
629
  st.session_state.pdf_processed = True
630
  st.session_state.mapping_done = False
631
+ st.session_state.links_df = pd.DataFrame()
632
+ st.session_state.df_aug = pd.DataFrame()
633
+ st.session_state.store = None
634
 
635
+ plot_results = st.session_state.plot_results
636
+ if not plot_results:
 
637
  st.warning("No plots found in this PDF.")
638
  return
639
 
640
+ df = st.session_state.pdf_extracted_df
641
+ has_data = not df.empty
642
+ n_imgs = sum(len(g.get("image_data", [])) for g in plot_results)
643
 
644
+ cov = st.session_state.get("plot_coverage", {}) or {}
645
  if has_data:
646
+ mat_abbr = df.iloc[0]["material_abbreviation"]
 
647
  st.info(
648
+ f"**{len(plot_results)} figures / {n_imgs} crops** extracted | "
649
+ f"Material: **{mat_abbr}** | {df['property_name'].nunique()} properties | "
650
+ f"recovered: {cov.get('total_recovered', 0)}"
651
  )
652
  else:
653
+ st.warning("Extract material data in the **Material Data** tab first to enable mapping.")
654
+
655
+ # Downloads (image2's native zipper)
656
+ d1, d2 = st.columns(2)
657
+ d1.download_button(
658
+ "⬇ Images + metadata (ZIP)",
659
+ data=image2.create_plot_zip(plot_results, include_json=True),
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
+ "⬇ Images only (ZIP)",
665
+ data=image2.create_plot_zip(plot_results, include_json=False),
666
+ file_name=f"{paper_id}_images.zip", mime="application/zip",
667
+ use_container_width=True, key="dl_images_zip",
668
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
 
670
+ st.divider()
671
 
672
+ # 2) Map properties β†’ figures (mapper5 four-signal cascade)
673
+ if has_data:
674
+ cA, cB = st.columns([0.6, 0.4])
675
+ run_map = cA.button(
676
+ " Map properties β†’ figures",
677
+ type="primary",
678
+ disabled=st.session_state.get("mapping_done", False),
679
+ use_container_width=True,
680
  )
681
+ if st.session_state.get("mapping_done"):
682
+ if cB.button("β†Ί Re-run mapping", use_container_width=True):
683
+ st.session_state.mapping_done = False
684
+ st.session_state.links_df = pd.DataFrame()
685
+ st.session_state.df_aug = pd.DataFrame()
686
+ st.session_state.store = None
687
+ st.rerun()
688
 
689
+ if run_map:
690
+ with st.spinner("Fusing figure-citation β†’ page β†’ SciBERT β†’ token signals…"):
691
+ links_df, df_aug = mapper5.map_plots_to_properties(df, plot_results)
692
+ st.session_state.links_df = links_df
693
+ st.session_state.df_aug = df_aug
694
+ st.session_state.mapping_done = True
695
+ n_mapped = int((df_aug.get("map_score", pd.Series(dtype=str)).astype(str) != "").sum()) \
696
+ if not df_aug.empty else 0
697
+ st.success(f" {n_mapped}/{len(df)} property rows linked to a figure "
698
+ f"({len(links_df)} total link(s)).")
699
+ st.rerun()
700
+
701
+ links_df = st.session_state.get("links_df", pd.DataFrame())
702
+ mapping_done = st.session_state.get("mapping_done", False)
703
+
704
+ st.divider()
705
+
706
+ # 3) Figure-centric review β€” each figure with the property rows linked to it
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.markdown(f"**Page {page}** β€” {caption}")
714
+
715
+ icols = st.columns(min(len(imgs), 4) or 1)
716
+ for pos, im in enumerate(imgs):
717
+ with icols[pos % len(icols)]:
718
+ arr = im.get("array")
719
+ if arr is not None:
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)
730
+ if grp.empty:
731
+ st.caption("No property linked to this figure.")
732
+ else:
733
+ hdr = st.columns([3, 1.4, 1, 2, 1.2, 1, 0.7])
734
+ for h, t in zip(hdr, ["property", "value", "unit", "material",
735
+ "subplot", "score", ""]):
736
+ h.caption(t)
737
+ for _, l in grp.iterrows():
738
+ row = st.columns([3, 1.4, 1, 2, 1.2, 1, 0.7])
739
+ row[0].write(str(l.get("property_name", "")))
740
+ row[1].write(str(l.get("value", "")))
741
+ row[2].write(str(l.get("unit", "")))
742
+ row[3].write(str(l.get("material_name", "")))
743
+ row[4].write(str(l.get("matched_subplot") or ""))
744
+ row[5].markdown(
745
+ _confidence_badge(_score_confidence(l.get("match_score"))),
746
  unsafe_allow_html=True,
747
  )
748
+ key = f"rmlink_{gi}_{int(l.get('prop_row', 0))}_{int(l.get('match_rank', 0))}"
749
+ if row[6].button("βœ•", key=key, help="Remove this link"):
750
+ mask = ~(
751
+ (links_df["group_idx"] == gi)
752
+ & (links_df["prop_row"] == l["prop_row"])
753
+ & (links_df["match_rank"] == l["match_rank"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  )
755
+ st.session_state.links_df = links_df[mask].reset_index(drop=True)
756
+ # rebuild mapped_* columns from the pruned links
757
+ st.session_state.df_aug = mapper5._apply_links(
758
+ df, st.session_state.links_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  )
760
+ st.rerun()
761
 
762
+ if not (has_data and mapping_done):
763
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
 
765
+ st.divider()
766
+
767
+ # 4) Store to SQLite (saves crops + attaches plot_image_path), then push to RDS
768
+ st.markdown("**Store the mapped result, then push to RDS**")
769
+ st.caption("Store writes the mapped rows + saved crops to a local SQLite DB and "
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(" Store to database (SQLite)", use_container_width=True):
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,
777
+ st.session_state.links_df,
778
+ plot_results,
779
+ out_dir=_OUT_DIR,
780
+ pdf_stem=paper_id,
781
+ source_table=_SOURCE_TABLE,
782
+ )
783
+ s = st.session_state.store
784
+ st.success(
785
+ f"Stored {s['n_properties']} row(s) β†’ {s['db_filename']}; "
786
+ f"{s['n_properties_with_plot']} with a plot, {s['n_images_saved']} crop(s)."
787
+ )
788
+ st.rerun()
789
 
790
+ store = st.session_state.get("store")
791
+
792
+ st.markdown("** Push to RDS database**")
793
+ if not mapper5.db_configured():
794
+ st.caption("Set DB_NAME / DB_USER / DB_PASSWORD in your `.env` "
795
+ "(host is already configured) to enable the RDS push.")
796
+ return
797
+
798
+ category = st.session_state.get("push_category")
799
+ if not category:
800
+ category = st.selectbox(
801
+ "Category table (routes by material type)",
802
+ category_push.CATEGORY_TABLES,
803
+ key="push_category_tab2",
804
+ help="Rows conform to this table's 33-col schema; columns it can't hold "
805
+ "go to <table>_extras; matched crops copy to rds_plots/<category>/<stem>/.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
  )
807
+ embed_img = st.checkbox("Embed matched plot into the table's image column", value=True)
808
+
809
+ cT, cP = st.columns(2)
810
+ if cT.button(" Test connection", use_container_width=True):
811
+ try:
812
+ mapper5.db_healthcheck()
813
+ st.success("Connected to RDS.")
814
+ except Exception as e:
815
+ st.error(f"Connection failed: {e}")
816
+
817
+ push_ready = bool(store) and store.get("n_properties", 0) > 0
818
+ if cP.button(f" Push to {category}", type="primary",
819
+ use_container_width=True, disabled=not push_ready):
820
+ try:
821
+ with st.spinner(f"Conforming + writing rows to '{category}'…"):
822
+ res = category_push.push_by_category(
823
+ store, category, mapper5.get_db_engine(), embed_image=embed_img
824
+ )
825
+ st.success(
826
+ f"Pushed {res['pushed']} row(s) to '{res['table']}', "
827
+ f"{res['extras']} to '{res['extras_table']}', "
828
+ f"{res['plots_copied']} crop(s) β†’ {res['plots_dir']}."
829
+ )
830
+ except Exception as e:
831
+ st.error(f"Push failed: {e}")
832
+ if not push_ready:
833
+ st.caption("Click ** Store to database (SQLite)** first β€” the push uploads those stored rows.")
834
 
835
 
836
  # ─────────────────────────────────────────────────────────────────────────────
 
845
  st.caption("Provide technical data and research documentation for the central repository.")
846
 
847
  defaults = {
848
+ "plot_results": [],
849
+ "plot_coverage": {},
850
+ "links_df": pd.DataFrame(),
851
+ "df_aug": pd.DataFrame(),
852
+ "store": None,
853
+ "pdf_processed": False,
854
+ "mapping_done": False,
855
+ "current_pdf_name": None,
856
+ "form_submitted": False,
857
+ "pdf_data_extracted": False,
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:
 
894
 
895
  if st.session_state.form_submitted:
896
  st.session_state.form_submitted = False
897
+ st.info("Form submitted. Upload again to process a new PDF.")
 
 
 
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:
905
  tmp_file.write(uploaded_file.getbuffer())
906
  tmp_file.flush()
 
909
 
910
  with tab1:
911
  render_material_data_tab(pdf_path)
 
912
  with tab2:
913
  render_plots_tab(pdf_path, paper_id)
 
914
  finally:
915
  try:
916
  os.unlink(tmp_file.name)
 
918
  pass
919
 
920
 
921
+ main()