starfriend commited on
Commit
534e8d0
·
verified ·
1 Parent(s): 4977aa4

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 did not produce machine-readable JSON; CSV not generated"
24
- if not isinstance(obj, dict) or not isinstance(obj.get("records"), list):
25
- return answer, [], "Model returned no structured records; CSV not generated"
26
- rows = obj["records"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  if len(rows) > 250 or any(not isinstance(r,dict) for r in rows):
28
- return str(obj.get("answer") or ""), [], "Invalid record structure"
29
- return str(obj.get("answer") or ""), rows, None
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
- fields = sorted(allowed - reserved) + sorted(reserved)
 
 
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
- " Return ONLY valid JSON with keys answer (brief readable summary) and records "
120
- "(array containing exactly the requested extracted rows). For a request to extract "
121
- "all data, include every parameter and every classification subrow, standard range, "
122
- "percentage, reference, units and descriptive statistics, without silently omitting "
123
- "rows. In repeated subrows, do not invent or repeat descriptive statistics that "
124
- "occur only in the first source row: use null. "
125
- "Use stable field names: parameter, unit, minimum, maximum, mean, std, "
126
- "standard_range, class_label, classification, percent_samples, references, "
127
- "estimated (boolean), extraction_method (printed_table|positioned_text|visual_estimate), "
128
- "and source_quality. Preserve original printed parameter names, units, inequality "
129
- "signs, and numeric precision. Numeric values must be JSON numbers (no thousands "
130
- "separators); use *_printed fields if preservation of original formatting matters. "
131
- "For ambiguous OCR signs, compare against image; if still unclear use null and "
132
- "explain ambiguity. Source-quality is descriptive, never a proof of verification. "
133
- "When extracting multiple sources include paper_id and media_label per record. "
134
- "The answer must not claim a complete extraction if rows are omitted."
 
 
 
 
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 = []