Spaces:
Sleeping
Sleeping
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.
|
|
|
|
|
|
|
|
|
|
| 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 "
|