starfriend commited on
Commit
0ea5698
·
verified ·
1 Parent(s): b3c3675

Upload 7 files

Browse files
backend/media_analysis/figure_analysis.py CHANGED
@@ -158,6 +158,12 @@ def analyze_media_question(question: str, selected_papers: list[dict], *, enable
158
  records, normalization_warnings = normalize_records(records)
159
  result["structured_data"] = records
160
  result["normalization_warnings"] = normalization_warnings
 
 
 
 
 
 
161
  result["csv_attachment"] = csv_attachment(records, result.get("sources") or [])
162
  if warning:
163
  result["export_warning"] = warning
 
158
  records, normalization_warnings = normalize_records(records)
159
  result["structured_data"] = records
160
  result["normalization_warnings"] = normalization_warnings
161
+ from .record_checks import validate_extraction
162
+ records, validation_warnings, coverage = validate_extraction(records, items if evidence_mode != "vision" else [])
163
+ result["structured_data"] = records
164
+ result["validation_warnings"] = validation_warnings
165
+ result["extraction_coverage"] = coverage
166
+
167
  result["csv_attachment"] = csv_attachment(records, result.get("sources") or [])
168
  if warning:
169
  result["export_warning"] = warning
backend/media_analysis/record_checks.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Non-destructive checks for model-produced tabular rows; warnings are not corrections."""
2
+ from __future__ import annotations
3
+ import re
4
+ from collections import Counter
5
+
6
+ _CLASS = re.compile(r"\b(Class\s+(?:I{1,3}|IV|V|[1-9]\d*))\b", re.I)
7
+
8
+ def validate_extraction(records: list[dict], table_evidence: list[dict] | None = None) -> tuple[list[dict], list[str], dict]:
9
+ """Return standardized copies plus specific warnings and observable coverage.
10
+
11
+ Does not invent missing values or signs and does not treat model notes as verification.
12
+ """
13
+ output=[]; warnings=[]
14
+ for i, row in enumerate(records, 1):
15
+ r=dict(row)
16
+ parameter=str(r.get('parameter') or '').strip()
17
+ if not parameter:
18
+ warnings.append(f'Record {i}: missing parameter')
19
+ std=r.get('standard_range')
20
+ label=r.get('class_label')
21
+ if isinstance(std,str):
22
+ matches=list(_CLASS.finditer(std))
23
+ if matches:
24
+ inferred=matches[0].group(1)
25
+ if not label:
26
+ r['class_label']=inferred
27
+ elif re.sub(r'\s+',' ',str(label).strip()).casefold()!=inferred.casefold():
28
+ warnings.append(f'Record {i} ({parameter}): class_label conflicts with standard_range')
29
+ remainder=_CLASS.sub('',std).strip(' ;,:')
30
+ if remainder:
31
+ r['standard_range']=remainder
32
+ else:
33
+ warnings.append(f'Record {i} ({parameter}): standard_range only contained a class label')
34
+ if isinstance(r.get('source_quality'),str) and len(r['source_quality'])>100:
35
+ # Never delete model-provided evidence notes; keep intact, downstream presentation may summarize.
36
+ pass
37
+ output.append(r)
38
+ # Detect omissions only if a source contains a word at the percentage x-position
39
+ # on the same parameter row; source row layout may be complex, so report as review.
40
+ known_source = {}
41
+ for ev in table_evidence or []:
42
+ data=ev.get('table_data') or {}
43
+ for row in data.get('text_rows') or []:
44
+ words=row.get('positioned_words') or row.get('words') or []
45
+ text=str(row.get('text') or '')
46
+ for w in words:
47
+ try: x=float(w[1] if isinstance(w,list) else w.get('x',w.get('bbox_points',[None])[0]))
48
+ except (TypeError,ValueError,IndexError):continue
49
+ token=str(w[0] if isinstance(w,list) else w.get('text') or '')
50
+ if 442<=x<=460 and re.fullmatch(r'(?:\d+(?:\.\d+)?%?|0)',token):
51
+ known_source.setdefault('percent_rows',[]).append((text,token))
52
+ # Contextual null is valid for blank printed cells; never auto-fill null as zero.
53
+ counts=Counter(str(r.get('parameter') or '') for r in output)
54
+ for i,r in enumerate(output,1):
55
+ if r.get('classification') and r.get('percent_samples') is None:
56
+ warnings.append(f'Record {i} ({r.get("parameter")}, {r.get("classification")}): percentage is null; check whether source cell is blank or extraction omitted it')
57
+ if r.get('standard_range') is None and r.get('classification'):
58
+ warnings.append(f'Record {i} ({r.get("parameter")}): classification has no standard_range')
59
+ # Check parameter coverage from first-column starts in positional data.
60
+ source_names=[]
61
+ for ev in table_evidence or []:
62
+ data=ev.get('table_data') or {}
63
+ for row in data.get('text_rows') or []:
64
+ words=row.get('positioned_words') or row.get('words') or []
65
+ if not words: continue
66
+ first=words[0]
67
+ try:x=float(first[1] if isinstance(first,list) else first.get('x',first.get('bbox_points',[None])[0]))
68
+ except (TypeError,ValueError,IndexError):continue
69
+ token=str(first[0] if isinstance(first,list) else first.get('text') or '')
70
+ if 55<=x<=85 and token and token.casefold() not in ('parameters','sample') and token not in source_names:
71
+ source_names.append(token)
72
+ present={x.casefold().rstrip('%') for x in counts if x}
73
+ missing=[n for n in source_names if n.casefold().rstrip('%') not in present]
74
+ if missing:
75
+ warnings.append('Possible missing source parameters (positional heuristic): '+', '.join(missing))
76
+ summary={'record_count':len(output),'parameter_count':len(counts)-int('' in counts),
77
+ 'source_parameter_candidates':source_names,'possibly_missing_parameters':missing,
78
+ 'null_percentage_count':sum(r.get('classification') is not None and r.get('percent_samples') is None for r in output),
79
+ 'warning_count':len(warnings)}
80
+ return output,warnings,summary
backend/media_analysis/vision_client.py CHANGED
@@ -125,7 +125,10 @@ def analyze_images(question: str, evidence: list[dict], *, model: str | None = N
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 "
 
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. Keep source_quality brief (e.g., image_checked, ambiguous, unverified). "
129
+ "For class labels (Class I, II, III), populate class_label separately and "
130
+ "exclude the class name from standard_range. Never guess blank percentages as zero. "
131
+ "Preserve original printed parameter names, units, inequality "
132
  "signs, and numeric precision. Numeric values must be JSON numbers (no thousands "
133
  "separators); use *_printed fields if preservation of original formatting matters. "
134
  "For ambiguous OCR signs, compare against image; if still unclear use null and "