Update page_files/categorized/Backend/PDF_DataExtraction.py
Browse files
page_files/categorized/Backend/PDF_DataExtraction.py
CHANGED
|
@@ -6,8 +6,8 @@ source verification: every extracted property's value must be found in the PDF
|
|
| 6 |
first (tolerant numeric match), then its property+material meaning is checked
|
| 7 |
against the sentence(s) that contained that value.
|
| 8 |
|
| 9 |
-
Output: one table β Gemini's extracted properties, each
|
| 10 |
-
True/False, downloadable as CSV.
|
| 11 |
|
| 12 |
Run: streamlit run FinalVerdict.py
|
| 13 |
Needs GEMINI_API_KEY in .env
|
|
@@ -36,6 +36,11 @@ EMBED_MODEL_NAME = "allenai/scibert_scivocab_uncased" # matches DocToDB_eval
|
|
| 36 |
VALUE_REL_TOL = 0.005 # 0.5% relative tolerance for numeric matching
|
| 37 |
MEANING_THRESHOLD = 0.35 # cosine similarity floor for property/material meaning check
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
# SCHEMA / PROMPT
|
| 41 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -45,6 +50,7 @@ SCHEMA = {
|
|
| 45 |
"properties": {
|
| 46 |
"material_name": {"type": "STRING"},
|
| 47 |
"material_abbreviation": {"type": "STRING"},
|
|
|
|
| 48 |
"mechanical_properties": {
|
| 49 |
"type": "ARRAY",
|
| 50 |
"items": {
|
|
@@ -66,8 +72,11 @@ SCHEMA = {
|
|
| 66 |
|
| 67 |
EXTRACTION_PROMPT = (
|
| 68 |
"You are an expert materials scientist. From the attached PDF, extract the material name, "
|
| 69 |
-
"abbreviation, and ALL properties across categories (Mechanical, Thermal,
|
| 70 |
-
"Optical, Rheological, etc.). Return
|
|
|
|
|
|
|
|
|
|
| 71 |
"For each property, you MUST extract:\n"
|
| 72 |
"- section (category)\n- property_name\n- value (or range)\n- unit\n"
|
| 73 |
"- english (converted or alternate units, e.g., psi, Β°F, inches; write '' if not provided)\n"
|
|
@@ -78,6 +87,7 @@ EXTRACTION_PROMPT = (
|
|
| 78 |
"{\n"
|
| 79 |
' "material_name": "",\n'
|
| 80 |
' "material_abbreviation": "",\n'
|
|
|
|
| 81 |
' "mechanical_properties": [\n'
|
| 82 |
' {"section":"","property_name":"","value":"","unit":"","english":"","test_condition":"","comments":""}\n'
|
| 83 |
" ]\n"
|
|
@@ -93,6 +103,58 @@ def make_abbreviation(name: str) -> str:
|
|
| 93 |
return abbr or name[:6].upper()
|
| 94 |
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 97 |
# GEMINI CALL β single call, whole PDF, no chunking
|
| 98 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -137,7 +199,7 @@ def call_gemini_from_bytes(pdf_bytes: bytes) -> Optional[Dict[str, Any]]:
|
|
| 137 |
return None
|
| 138 |
|
| 139 |
|
| 140 |
-
def convert_to_dataframe(data: Dict[str, Any]) -> pd.DataFrame:
|
| 141 |
if not data:
|
| 142 |
return pd.DataFrame()
|
| 143 |
mat_name = data.get("material_name", "") or ""
|
|
@@ -148,6 +210,7 @@ def convert_to_dataframe(data: Dict[str, Any]) -> pd.DataFrame:
|
|
| 148 |
rows = []
|
| 149 |
for item in data.get("mechanical_properties", []):
|
| 150 |
rows.append({
|
|
|
|
| 151 |
"material_name": mat_name,
|
| 152 |
"material_abbreviation": mat_abbr,
|
| 153 |
"section": item.get("section", "") or "Mechanical",
|
|
@@ -271,10 +334,11 @@ def verify_dataframe(df: pd.DataFrame, sentences: List[Dict[str, Any]]) -> pd.Da
|
|
| 271 |
|
| 272 |
def main():
|
| 273 |
st.set_page_config(page_title="Gemini Extraction β Source Verified", layout="wide")
|
| 274 |
-
st.title("
|
| 275 |
st.caption(
|
| 276 |
"Straight one-call Gemini extraction β no chunking, no ranking, no batching. "
|
| 277 |
-
"Every property is checked against the PDF: value found first, then meaning matched."
|
|
|
|
| 278 |
)
|
| 279 |
|
| 280 |
pdf_file = st.file_uploader("PDF to extract", type=["pdf"], key="fv_pdf")
|
|
@@ -291,24 +355,30 @@ def main():
|
|
| 291 |
st.session_state["fv_sig"] = sig
|
| 292 |
st.session_state["fv_result"] = None
|
| 293 |
|
| 294 |
-
if st.button("
|
| 295 |
with st.spinner("Extractingβ¦"):
|
| 296 |
data = call_gemini_from_bytes(pdf_bytes)
|
| 297 |
-
|
|
|
|
| 298 |
sentences = _extract_sentences(pdf_bytes)
|
| 299 |
st.session_state["fv_result"] = verify_dataframe(df, sentences)
|
|
|
|
| 300 |
|
| 301 |
result = st.session_state.get("fv_result")
|
| 302 |
if result is None:
|
| 303 |
st.info("Click **Run Extraction** to start.")
|
| 304 |
return
|
| 305 |
|
|
|
|
| 306 |
n_verified = int(result["source_verified"].sum()) if "source_verified" in result.columns else 0
|
| 307 |
-
st.caption(
|
|
|
|
|
|
|
|
|
|
| 308 |
st.dataframe(result, use_container_width=True, hide_index=True)
|
| 309 |
|
| 310 |
if not result.empty:
|
| 311 |
-
st.download_button("
|
| 312 |
f"{stem}_gemini_verified.csv", "text/csv")
|
| 313 |
|
| 314 |
|
|
|
|
| 6 |
first (tolerant numeric match), then its property+material meaning is checked
|
| 7 |
against the sentence(s) that contained that value.
|
| 8 |
|
| 9 |
+
Output: one table β Gemini's extracted properties, each with the paper DOI and
|
| 10 |
+
flagged source_verified True/False, downloadable as CSV.
|
| 11 |
|
| 12 |
Run: streamlit run FinalVerdict.py
|
| 13 |
Needs GEMINI_API_KEY in .env
|
|
|
|
| 36 |
VALUE_REL_TOL = 0.005 # 0.5% relative tolerance for numeric matching
|
| 37 |
MEANING_THRESHOLD = 0.35 # cosine similarity floor for property/material meaning check
|
| 38 |
|
| 39 |
+
# DOIs follow a strict format (10.<registrant>/<suffix>), so regex over the raw
|
| 40 |
+
# PDF text is far more reliable than an LLM guess. Used as the primary source;
|
| 41 |
+
# Gemini's own DOI (from the schema) is only a fallback.
|
| 42 |
+
DOI_CORE_RE = re.compile(r'10\.\d{4,9}/[^\s"<>\]\)]+', re.IGNORECASE)
|
| 43 |
+
|
| 44 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
# SCHEMA / PROMPT
|
| 46 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 50 |
"properties": {
|
| 51 |
"material_name": {"type": "STRING"},
|
| 52 |
"material_abbreviation": {"type": "STRING"},
|
| 53 |
+
"doi": {"type": "STRING"},
|
| 54 |
"mechanical_properties": {
|
| 55 |
"type": "ARRAY",
|
| 56 |
"items": {
|
|
|
|
| 72 |
|
| 73 |
EXTRACTION_PROMPT = (
|
| 74 |
"You are an expert materials scientist. From the attached PDF, extract the material name, "
|
| 75 |
+
"abbreviation, the paper's DOI, and ALL properties across categories (Mechanical, Thermal, "
|
| 76 |
+
"Electrical, Physical, Optical, Rheological, etc.). Return the properties as "
|
| 77 |
+
"'mechanical_properties' (a single list). "
|
| 78 |
+
"For the DOI, extract the Digital Object Identifier exactly as printed (e.g., "
|
| 79 |
+
"10.1021/acsami.0c01234); write '' if it is not present. "
|
| 80 |
"For each property, you MUST extract:\n"
|
| 81 |
"- section (category)\n- property_name\n- value (or range)\n- unit\n"
|
| 82 |
"- english (converted or alternate units, e.g., psi, Β°F, inches; write '' if not provided)\n"
|
|
|
|
| 87 |
"{\n"
|
| 88 |
' "material_name": "",\n'
|
| 89 |
' "material_abbreviation": "",\n'
|
| 90 |
+
' "doi": "",\n'
|
| 91 |
' "mechanical_properties": [\n'
|
| 92 |
' {"section":"","property_name":"","value":"","unit":"","english":"","test_condition":"","comments":""}\n'
|
| 93 |
" ]\n"
|
|
|
|
| 103 |
return abbr or name[:6].upper()
|
| 104 |
|
| 105 |
|
| 106 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 107 |
+
# DOI EXTRACTION β regex over raw PDF text (primary), Gemini doi (fallback)
|
| 108 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
|
| 110 |
+
def _clean_doi(doi: str) -> str:
|
| 111 |
+
"""Strip common prefixes and trailing punctuation that regex can over-capture."""
|
| 112 |
+
if not doi:
|
| 113 |
+
return ""
|
| 114 |
+
doi = doi.strip()
|
| 115 |
+
doi = re.sub(r'^(?:https?://)?(?:dx\.)?doi\.org/', '', doi, flags=re.IGNORECASE)
|
| 116 |
+
doi = re.sub(r'^doi[:\s]+', '', doi, flags=re.IGNORECASE)
|
| 117 |
+
doi = doi.rstrip(' .,;:)]}>"\'')
|
| 118 |
+
return doi
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _extract_doi_from_pdf(pdf_bytes: bytes) -> str:
|
| 122 |
+
"""Find the paper DOI in the raw PDF text. Prefers an explicit doi.org URL or
|
| 123 |
+
a 'doi:' label, then falls back to the first bare 10.xxxx/... token."""
|
| 124 |
+
try:
|
| 125 |
+
with fitz.open(stream=pdf_bytes, filetype="pdf") as doc:
|
| 126 |
+
text = "\n".join((page.get_text("text") or "") for page in doc)
|
| 127 |
+
except Exception:
|
| 128 |
+
return ""
|
| 129 |
+
|
| 130 |
+
# 1) explicit doi.org URL
|
| 131 |
+
m = re.search(r'(?:https?://)?(?:dx\.)?doi\.org/(10\.\d{4,9}/[^\s"<>\]\)]+)', text, re.IGNORECASE)
|
| 132 |
+
if m:
|
| 133 |
+
return _clean_doi(m.group(1))
|
| 134 |
+
|
| 135 |
+
# 2) 'doi:' or 'DOI ' labelled
|
| 136 |
+
m = re.search(r'\bdoi[:\s]+\s*(10\.\d{4,9}/[^\s"<>\]\)]+)', text, re.IGNORECASE)
|
| 137 |
+
if m:
|
| 138 |
+
return _clean_doi(m.group(1))
|
| 139 |
+
|
| 140 |
+
# 3) any bare DOI token
|
| 141 |
+
m = DOI_CORE_RE.search(text)
|
| 142 |
+
if m:
|
| 143 |
+
return _clean_doi(m.group(0))
|
| 144 |
+
|
| 145 |
+
return ""
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def resolve_doi(pdf_bytes: bytes, gemini_data: Optional[Dict[str, Any]]) -> str:
|
| 149 |
+
"""Prefer the regex-extracted DOI (format-verified). Fall back to Gemini's."""
|
| 150 |
+
doi = _extract_doi_from_pdf(pdf_bytes)
|
| 151 |
+
if doi:
|
| 152 |
+
return doi
|
| 153 |
+
if gemini_data:
|
| 154 |
+
return _clean_doi(str(gemini_data.get("doi", "") or ""))
|
| 155 |
+
return ""
|
| 156 |
+
|
| 157 |
+
|
| 158 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 159 |
# GEMINI CALL β single call, whole PDF, no chunking
|
| 160 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 199 |
return None
|
| 200 |
|
| 201 |
|
| 202 |
+
def convert_to_dataframe(data: Dict[str, Any], doi: str = "") -> pd.DataFrame:
|
| 203 |
if not data:
|
| 204 |
return pd.DataFrame()
|
| 205 |
mat_name = data.get("material_name", "") or ""
|
|
|
|
| 210 |
rows = []
|
| 211 |
for item in data.get("mechanical_properties", []):
|
| 212 |
rows.append({
|
| 213 |
+
"doi": doi,
|
| 214 |
"material_name": mat_name,
|
| 215 |
"material_abbreviation": mat_abbr,
|
| 216 |
"section": item.get("section", "") or "Mechanical",
|
|
|
|
| 334 |
|
| 335 |
def main():
|
| 336 |
st.set_page_config(page_title="Gemini Extraction β Source Verified", layout="wide")
|
| 337 |
+
st.title(" Gemini Extraction β Source Verified")
|
| 338 |
st.caption(
|
| 339 |
"Straight one-call Gemini extraction β no chunking, no ranking, no batching. "
|
| 340 |
+
"Every property is checked against the PDF: value found first, then meaning matched. "
|
| 341 |
+
"Paper DOI is pulled from the PDF text."
|
| 342 |
)
|
| 343 |
|
| 344 |
pdf_file = st.file_uploader("PDF to extract", type=["pdf"], key="fv_pdf")
|
|
|
|
| 355 |
st.session_state["fv_sig"] = sig
|
| 356 |
st.session_state["fv_result"] = None
|
| 357 |
|
| 358 |
+
if st.button(" Run Extraction", type="primary", use_container_width=True):
|
| 359 |
with st.spinner("Extractingβ¦"):
|
| 360 |
data = call_gemini_from_bytes(pdf_bytes)
|
| 361 |
+
doi = resolve_doi(pdf_bytes, data)
|
| 362 |
+
df = convert_to_dataframe(data, doi=doi)
|
| 363 |
sentences = _extract_sentences(pdf_bytes)
|
| 364 |
st.session_state["fv_result"] = verify_dataframe(df, sentences)
|
| 365 |
+
st.session_state["fv_doi"] = doi
|
| 366 |
|
| 367 |
result = st.session_state.get("fv_result")
|
| 368 |
if result is None:
|
| 369 |
st.info("Click **Run Extraction** to start.")
|
| 370 |
return
|
| 371 |
|
| 372 |
+
doi = st.session_state.get("fv_doi", "")
|
| 373 |
n_verified = int(result["source_verified"].sum()) if "source_verified" in result.columns else 0
|
| 374 |
+
st.caption(
|
| 375 |
+
f"DOI: {doi or '(not found)'} β "
|
| 376 |
+
f"{len(result)} properties extracted β {n_verified} source-verified."
|
| 377 |
+
)
|
| 378 |
st.dataframe(result, use_container_width=True, hide_index=True)
|
| 379 |
|
| 380 |
if not result.empty:
|
| 381 |
+
st.download_button(" Download CSV", result.to_csv(index=False).encode(),
|
| 382 |
f"{stem}_gemini_verified.csv", "text/csv")
|
| 383 |
|
| 384 |
|