Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files
backend/media_analysis/data_export.py
CHANGED
|
@@ -12,6 +12,14 @@ import json
|
|
| 12 |
import re
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def decode_structured_answer(answer: str) -> tuple[str, list[dict], str | None]:
|
| 16 |
s = (answer or "").strip()
|
| 17 |
if s.startswith("```"):
|
|
@@ -20,13 +28,27 @@ def decode_structured_answer(answer: str) -> tuple[str, list[dict], str | None]:
|
|
| 20 |
try:
|
| 21 |
obj = json.loads(s)
|
| 22 |
except (ValueError, TypeError):
|
| 23 |
-
return answer, [], "Model
|
| 24 |
-
if not isinstance(obj, dict)
|
| 25 |
-
return answer, [], "Model
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
if len(rows) > 250 or any(not isinstance(r,dict) for r in rows):
|
| 28 |
-
return
|
| 29 |
-
return
|
| 30 |
|
| 31 |
|
| 32 |
NUMERIC_FIELDS = {"minimum", "maximum", "mean", "std", "value", "x", "y", "percent_value"}
|
|
@@ -81,7 +103,9 @@ def csv_attachment(records: list[dict], sources: list[dict]) -> dict | None:
|
|
| 81 |
allowed.add(key)
|
| 82 |
if not allowed:
|
| 83 |
return None
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
output = io.StringIO(newline="")
|
| 86 |
writer = csv.DictWriter(output, fieldnames=fields, extrasaction="ignore")
|
| 87 |
writer.writeheader()
|
|
@@ -92,7 +116,9 @@ def csv_attachment(records: list[dict], sources: list[dict]) -> dict | None:
|
|
| 92 |
for k, source_key in [("paper_id","paper_id"),("paper_title","paper_title"),("doi","doi"),("paper_key","paper_key"),("media_label","label"),("page","page")]:
|
| 93 |
row[k] = src.get(source_key) or ""
|
| 94 |
# Never silently claim exactness for unverified positioned text.
|
| 95 |
-
row.setdefault("extraction_method", "model_assisted")
|
|
|
|
|
|
|
| 96 |
writer.writerow({k:("'"+v if isinstance(v,str) and v[:1] in ("=","+","-","@") else v)
|
| 97 |
for k,v in row.items()})
|
| 98 |
data=output.getvalue().encode("utf-8-sig")
|
|
|
|
| 12 |
import re
|
| 13 |
|
| 14 |
|
| 15 |
+
# Only accepted tabular fields become output columns; other fields may be kept
|
| 16 |
+
# for backward compatibility with earlier full-record responses.
|
| 17 |
+
TABLE_COLUMNS = ["parameter", "unit", "minimum", "maximum", "mean", "std",
|
| 18 |
+
"standard_range", "class_label", "classification",
|
| 19 |
+
"percent_samples", "references"]
|
| 20 |
+
CHART_COLUMNS = ["point_id", "x", "y", "unit_x", "unit_y", "estimated"]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
def decode_structured_answer(answer: str) -> tuple[str, list[dict], str | None]:
|
| 24 |
s = (answer or "").strip()
|
| 25 |
if s.startswith("```"):
|
|
|
|
| 28 |
try:
|
| 29 |
obj = json.loads(s)
|
| 30 |
except (ValueError, TypeError):
|
| 31 |
+
return answer, [], "Model output was not complete JSON; CSV not generated"
|
| 32 |
+
if not isinstance(obj, dict):
|
| 33 |
+
return answer, [], "Model output must be a JSON object"
|
| 34 |
+
readable = str(obj.get("answer") or "")
|
| 35 |
+
if isinstance(obj.get("columns"), list) and isinstance(obj.get("rows"), list):
|
| 36 |
+
columns = obj["columns"]
|
| 37 |
+
if columns not in (TABLE_COLUMNS, CHART_COLUMNS):
|
| 38 |
+
return readable, [], "Unsupported or incorrectly ordered data columns; CSV not generated"
|
| 39 |
+
data_rows = obj["rows"]
|
| 40 |
+
if len(data_rows) > 250 or any(not isinstance(r, list) or len(r) != len(columns) for r in data_rows):
|
| 41 |
+
return readable, [], "Invalid row length or record count; CSV not generated"
|
| 42 |
+
if any(any(not isinstance(v, (str, int, float, bool, type(None))) for v in row) for row in data_rows):
|
| 43 |
+
return readable, [], "Nested values in data rows; CSV not generated"
|
| 44 |
+
return readable, [dict(zip(columns, r)) for r in data_rows], None
|
| 45 |
+
# Legacy full-record schema remains supported for existing clients.
|
| 46 |
+
rows = obj.get("records")
|
| 47 |
+
if not isinstance(rows, list):
|
| 48 |
+
return readable, [], "No structured records or compact rows; CSV not generated"
|
| 49 |
if len(rows) > 250 or any(not isinstance(r,dict) for r in rows):
|
| 50 |
+
return readable, [], "Invalid legacy record structure"
|
| 51 |
+
return readable, rows, None
|
| 52 |
|
| 53 |
|
| 54 |
NUMERIC_FIELDS = {"minimum", "maximum", "mean", "std", "value", "x", "y", "percent_value"}
|
|
|
|
| 103 |
allowed.add(key)
|
| 104 |
if not allowed:
|
| 105 |
return None
|
| 106 |
+
preferred = TABLE_COLUMNS + CHART_COLUMNS
|
| 107 |
+
fields = [x for x in preferred if x in allowed] + sorted(allowed - reserved - set(preferred)) + [
|
| 108 |
+
"paper_id", "paper_title", "doi", "paper_key", "media_label", "page", "extraction_method", "estimated", "source_quality"]
|
| 109 |
output = io.StringIO(newline="")
|
| 110 |
writer = csv.DictWriter(output, fieldnames=fields, extrasaction="ignore")
|
| 111 |
writer.writeheader()
|
|
|
|
| 116 |
for k, source_key in [("paper_id","paper_id"),("paper_title","paper_title"),("doi","doi"),("paper_key","paper_key"),("media_label","label"),("page","page")]:
|
| 117 |
row[k] = src.get(source_key) or ""
|
| 118 |
# Never silently claim exactness for unverified positioned text.
|
| 119 |
+
row.setdefault("extraction_method", "printed_table_transcription" if "parameter" in rec else "model_assisted")
|
| 120 |
+
row.setdefault("estimated", False if "parameter" in rec else "")
|
| 121 |
+
row.setdefault("source_quality", "model_extracted_unverified")
|
| 122 |
writer.writerow({k:("'"+v if isinstance(v,str) and v[:1] in ("=","+","-","@") else v)
|
| 123 |
for k,v in row.items()})
|
| 124 |
data=output.getvalue().encode("utf-8-sig")
|
backend/media_analysis/figure_analysis.py
CHANGED
|
@@ -154,7 +154,10 @@ def analyze_media_question(question: str, selected_papers: list[dict], *, enable
|
|
| 154 |
if export_csv:
|
| 155 |
from .data_export import decode_structured_answer, csv_attachment, normalize_records
|
| 156 |
answer, records, warning = decode_structured_answer(result.get("answer", ""))
|
| 157 |
-
result["answer"] = answer
|
|
|
|
|
|
|
|
|
|
| 158 |
records, normalization_warnings = normalize_records(records)
|
| 159 |
result["structured_data"] = records
|
| 160 |
result["normalization_warnings"] = normalization_warnings
|
|
|
|
| 154 |
if export_csv:
|
| 155 |
from .data_export import decode_structured_answer, csv_attachment, normalize_records
|
| 156 |
answer, records, warning = decode_structured_answer(result.get("answer", ""))
|
| 157 |
+
result["answer"] = answer if not warning else (
|
| 158 |
+
"Structured extraction did not complete; no downloadable CSV was generated. "
|
| 159 |
+
"Please retry or narrow the request."
|
| 160 |
+
)
|
| 161 |
records, normalization_warnings = normalize_records(records)
|
| 162 |
result["structured_data"] = records
|
| 163 |
result["normalization_warnings"] = normalization_warnings
|
backend/media_analysis/vision_client.py
CHANGED
|
@@ -116,22 +116,26 @@ def analyze_images(question: str, evidence: list[dict], *, model: str | None = N
|
|
| 116 |
)
|
| 117 |
if structured_output:
|
| 118 |
instructions += (
|
| 119 |
-
"
|
| 120 |
-
"
|
| 121 |
-
"
|
| 122 |
-
"
|
| 123 |
-
"
|
| 124 |
-
"
|
| 125 |
-
"
|
| 126 |
-
"
|
| 127 |
-
"
|
| 128 |
-
"
|
| 129 |
-
"
|
| 130 |
-
"
|
| 131 |
-
"
|
| 132 |
-
"
|
| 133 |
-
"
|
| 134 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
)
|
| 136 |
content: list[dict] = [{"type": "input_text", "text": f"Question: {question.strip()}"}]
|
| 137 |
used = []
|
|
|
|
| 116 |
)
|
| 117 |
if structured_output:
|
| 118 |
instructions += (
|
| 119 |
+
" For downloadable data extraction return ONLY one compact valid JSON object with "
|
| 120 |
+
"keys 'answer', 'columns', and 'rows'. 'answer' MUST be one short sentence, "
|
| 121 |
+
"NOT a reproduction of the table. 'columns' MUST be exactly "
|
| 122 |
+
"['parameter','unit','minimum','maximum','mean','std','standard_range',"
|
| 123 |
+
"'class_label','classification','percent_samples','references']. "
|
| 124 |
+
"'rows' MUST be an array of positional arrays following those columns, with "
|
| 125 |
+
"one array per ACTUAL classification/data subrow in source order. "
|
| 126 |
+
"Preserve ALL requested cells, parameters, subrows, units, percentage values, "
|
| 127 |
+
"inequalities and reference text. For stats/units/reference spanning multiple "
|
| 128 |
+
"subrows, give the value only on its printed first row and null thereafter. "
|
| 129 |
+
"Use null for genuinely blank/unreadable cells; never infer a missing percentage. "
|
| 130 |
+
"Numbers must be JSON numbers with no thousands separators, preserving signs. "
|
| 131 |
+
"Class I/II/III goes in class_label, not standard_range. "
|
| 132 |
+
"Print-table transcriptions are NOT visual estimates. Do not add repeated "
|
| 133 |
+
"paper IDs, sources, confidence descriptions, provenance, CSV text, or any "
|
| 134 |
+
"other fields to rows: the server adds metadata and generates the CSV. "
|
| 135 |
+
"Do NOT include Markdown fences or a duplicate CSV in the answer. "
|
| 136 |
+
"If source is an image plot rather than a table, use columns "
|
| 137 |
+
"['point_id','x','y','unit_x','unit_y','estimated'] and data rows; "
|
| 138 |
+
"numerical visual estimates must have estimated=true."
|
| 139 |
)
|
| 140 |
content: list[dict] = [{"type": "input_text", "text": f"Question: {question.strip()}"}]
|
| 141 |
used = []
|