Commit ·
e128e0e
1
Parent(s): 57104c1
Phase A: Add v2.0 3-level test harness (L1 raw, L2 solo, L3 integrated + master matrix)
Browse files- tests/v2/__init__.py +1 -0
- tests/v2/benchmark_matrix.py +210 -0
- tests/v2/test_level1_raw.py +278 -0
- tests/v2/test_level2_solo.py +289 -0
- tests/v2/test_level3_integrated.py +297 -0
tests/v2/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# BAYAN v2.0 Test Suite
|
tests/v2/benchmark_matrix.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN v2.0 — Benchmark Matrix Runner
|
| 3 |
+
======================================
|
| 4 |
+
Master script that runs all 3 test levels and produces a side-by-side
|
| 5 |
+
comparison matrix showing raw model vs solo API vs integrated pipeline.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
# Full matrix (all 3 levels)
|
| 9 |
+
python tests/v2/benchmark_matrix.py --url URL
|
| 10 |
+
|
| 11 |
+
# Single level only
|
| 12 |
+
python tests/v2/benchmark_matrix.py --url URL --level 1
|
| 13 |
+
python tests/v2/benchmark_matrix.py --url URL --level 3
|
| 14 |
+
|
| 15 |
+
# Single dataset
|
| 16 |
+
python tests/v2/benchmark_matrix.py --url URL --dataset spelling
|
| 17 |
+
|
| 18 |
+
# Compare saved results
|
| 19 |
+
python tests/v2/benchmark_matrix.py --compare
|
| 20 |
+
"""
|
| 21 |
+
import argparse
|
| 22 |
+
import json
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
REPORT_DIR = Path(__file__).parent / "reports"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def run_level(level: int, url: str, dataset: str = None):
|
| 31 |
+
"""Run a specific test level."""
|
| 32 |
+
import subprocess
|
| 33 |
+
scripts = {
|
| 34 |
+
1: "tests/v2/test_level1_raw.py",
|
| 35 |
+
2: "tests/v2/test_level2_solo.py",
|
| 36 |
+
3: "tests/v2/test_level3_integrated.py",
|
| 37 |
+
}
|
| 38 |
+
script = scripts[level]
|
| 39 |
+
cmd = [sys.executable, script, "--url", url]
|
| 40 |
+
if dataset:
|
| 41 |
+
cmd.extend(["--dataset", dataset])
|
| 42 |
+
|
| 43 |
+
print(f"\n{'#'*70}")
|
| 44 |
+
print(f"# RUNNING LEVEL {level}: {script}")
|
| 45 |
+
print(f"{'#'*70}\n")
|
| 46 |
+
|
| 47 |
+
result = subprocess.run(cmd, cwd=str(Path(__file__).parent.parent.parent))
|
| 48 |
+
return result.returncode == 0
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def load_report(level: int) -> dict:
|
| 52 |
+
"""Load saved report for a level."""
|
| 53 |
+
filenames = {
|
| 54 |
+
1: "level1_raw_results.json",
|
| 55 |
+
2: "level2_solo_results.json",
|
| 56 |
+
3: "level3_integrated_results.json",
|
| 57 |
+
}
|
| 58 |
+
path = REPORT_DIR / filenames[level]
|
| 59 |
+
if path.exists():
|
| 60 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 61 |
+
return json.load(f)
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def print_comparison_matrix():
|
| 66 |
+
"""Load all 3 reports and print side-by-side comparison."""
|
| 67 |
+
l1 = load_report(1)
|
| 68 |
+
l2 = load_report(2)
|
| 69 |
+
l3 = load_report(3)
|
| 70 |
+
|
| 71 |
+
print(f"\n{'='*70}")
|
| 72 |
+
print("BAYAN v2.0 — BENCHMARK COMPARISON MATRIX")
|
| 73 |
+
print(f"{'='*70}")
|
| 74 |
+
|
| 75 |
+
# Timestamps
|
| 76 |
+
for level, report, name in [(1, l1, "L1-Raw"), (2, l2, "L2-Solo"), (3, l3, "L3-Pipeline")]:
|
| 77 |
+
if report:
|
| 78 |
+
ts = report.get("timestamp", "N/A")
|
| 79 |
+
print(f" {name}: {ts}")
|
| 80 |
+
else:
|
| 81 |
+
print(f" {name}: NOT RUN")
|
| 82 |
+
|
| 83 |
+
# ── Level 1 Summary ──
|
| 84 |
+
if l1:
|
| 85 |
+
print(f"\n{'─'*70}")
|
| 86 |
+
print("Level 1: RAW MODEL OUTPUT (no filters)")
|
| 87 |
+
print(f"{'─'*70}")
|
| 88 |
+
a = l1.get("analysis", {})
|
| 89 |
+
by_model = a.get("by_model", {})
|
| 90 |
+
for model, data in by_model.items():
|
| 91 |
+
changed = data.get("changed", 0)
|
| 92 |
+
unchanged = data.get("unchanged", 0)
|
| 93 |
+
total = changed + unchanged + data.get("errors", 0)
|
| 94 |
+
rate = changed / total * 100 if total else 0
|
| 95 |
+
print(f" {model:<12}: {changed}/{total} texts modified ({rate:.1f}%)")
|
| 96 |
+
|
| 97 |
+
# ── Level 2 Summary ──
|
| 98 |
+
if l2:
|
| 99 |
+
print(f"\n{'─'*70}")
|
| 100 |
+
print("Level 2: SOLO API (single model + filters, no integration)")
|
| 101 |
+
print(f"{'─'*70}")
|
| 102 |
+
a = l2.get("analysis", {})
|
| 103 |
+
by_model = a.get("by_model", {})
|
| 104 |
+
print(f" {'Model':<12} {'TP':>4} {'TN':>4} {'FP':>4} {'FN':>4} {'Pass%':>7}")
|
| 105 |
+
print(f" {'-'*12} {'-'*4} {'-'*4} {'-'*4} {'-'*4} {'-'*7}")
|
| 106 |
+
for model, data in by_model.items():
|
| 107 |
+
pr = data.get("pass_rate", 0) * 100
|
| 108 |
+
print(f" {model:<12} {data.get('TP',0):>4} {data.get('TN',0):>4} "
|
| 109 |
+
f"{data.get('FP',0):>4} {data.get('FN',0):>4} {pr:>6.1f}%")
|
| 110 |
+
|
| 111 |
+
# ── Level 3 Summary ──
|
| 112 |
+
if l3:
|
| 113 |
+
print(f"\n{'─'*70}")
|
| 114 |
+
print("Level 3: INTEGRATED PIPELINE (full Spelling→Grammar→Punctuation)")
|
| 115 |
+
print(f"{'─'*70}")
|
| 116 |
+
a = l3.get("analysis", {})
|
| 117 |
+
agg = a.get("aggregate", {})
|
| 118 |
+
total = a.get("total", 0)
|
| 119 |
+
pr = agg.get("pass_rate", 0) * 100
|
| 120 |
+
print(f" Overall: {pr:.1f}% pass ({total} tests)")
|
| 121 |
+
print(f" TP={agg.get('TP',0)} TN={agg.get('TN',0)} "
|
| 122 |
+
f"FP={agg.get('FP',0)} FN={agg.get('FN',0)}")
|
| 123 |
+
|
| 124 |
+
print(f"\n {'Dataset':<14} {'Pass%':>7} {'TP':>4} {'TN':>4} {'FP':>4} {'FN':>4}")
|
| 125 |
+
print(f" {'-'*14} {'-'*7} {'-'*4} {'-'*4} {'-'*4} {'-'*4}")
|
| 126 |
+
by_ds = a.get("by_dataset", {})
|
| 127 |
+
for ds in sorted(by_ds.keys()):
|
| 128 |
+
d = by_ds[ds]
|
| 129 |
+
dp = d.get("pass_rate", 0) * 100
|
| 130 |
+
print(f" {ds:<14} {dp:>6.1f}% {d.get('TP',0):>4} {d.get('TN',0):>4} "
|
| 131 |
+
f"{d.get('FP',0):>4} {d.get('FN',0):>4}")
|
| 132 |
+
|
| 133 |
+
# ── Cross-Level Comparison ──
|
| 134 |
+
if l2 and l3:
|
| 135 |
+
print(f"\n{'─'*70}")
|
| 136 |
+
print("CROSS-LEVEL COMPARISON: Solo vs Integrated")
|
| 137 |
+
print(f"{'─'*70}")
|
| 138 |
+
|
| 139 |
+
l2_ds = l2.get("analysis", {}).get("by_dataset", {})
|
| 140 |
+
l3_ds = l3.get("analysis", {}).get("by_dataset", {})
|
| 141 |
+
|
| 142 |
+
all_datasets = sorted(set(list(l2_ds.keys()) + list(l3_ds.keys())))
|
| 143 |
+
|
| 144 |
+
print(f" {'Dataset':<14} {'L2-Spell':>10} {'L2-Gram':>10} {'L2-Punct':>10} {'L3-Pipeline':>12} {'Delta':>7}")
|
| 145 |
+
print(f" {'-'*14} {'-'*10} {'-'*10} {'-'*10} {'-'*12} {'-'*7}")
|
| 146 |
+
|
| 147 |
+
for ds in all_datasets:
|
| 148 |
+
l2d = l2_ds.get(ds, {})
|
| 149 |
+
l3d = l3_ds.get(ds, {})
|
| 150 |
+
|
| 151 |
+
l2_s = l2d.get("spelling", {}).get("pass_rate", 0) * 100
|
| 152 |
+
l2_g = l2d.get("grammar", {}).get("pass_rate", 0) * 100
|
| 153 |
+
l2_p = l2d.get("punctuation", {}).get("pass_rate", 0) * 100
|
| 154 |
+
l3_p = l3d.get("pass_rate", 0) * 100
|
| 155 |
+
|
| 156 |
+
# Best solo model vs pipeline
|
| 157 |
+
best_solo = max(l2_s, l2_g, l2_p)
|
| 158 |
+
delta = l3_p - best_solo
|
| 159 |
+
|
| 160 |
+
delta_str = f"{delta:+.1f}%"
|
| 161 |
+
print(f" {ds:<14} {l2_s:>9.1f}% {l2_g:>9.1f}% {l2_p:>9.1f}% {l3_p:>11.1f}% {delta_str:>7}")
|
| 162 |
+
|
| 163 |
+
# Save comparison
|
| 164 |
+
comparison = {
|
| 165 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 166 |
+
"l1_timestamp": l1.get("timestamp") if l1 else None,
|
| 167 |
+
"l2_timestamp": l2.get("timestamp") if l2 else None,
|
| 168 |
+
"l3_timestamp": l3.get("timestamp") if l3 else None,
|
| 169 |
+
"l2_summary": l2.get("analysis", {}).get("by_model") if l2 else None,
|
| 170 |
+
"l3_summary": l3.get("analysis", {}).get("aggregate") if l3 else None,
|
| 171 |
+
}
|
| 172 |
+
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
| 173 |
+
with open(REPORT_DIR / "benchmark_matrix.json", 'w', encoding='utf-8') as f:
|
| 174 |
+
json.dump(comparison, f, ensure_ascii=False, indent=2)
|
| 175 |
+
print(f"\n[MATRIX] Comparison → {REPORT_DIR / 'benchmark_matrix.json'}")
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def main():
|
| 179 |
+
parser = argparse.ArgumentParser(description="BAYAN v2.0 Benchmark Matrix")
|
| 180 |
+
parser.add_argument("--url", default="https://bayan10-bayan-api.hf.space")
|
| 181 |
+
parser.add_argument("--level", type=int, default=None, help="Run specific level (1/2/3)")
|
| 182 |
+
parser.add_argument("--dataset", default=None, help="Filter to single dataset")
|
| 183 |
+
parser.add_argument("--compare", action="store_true", help="Compare saved results only")
|
| 184 |
+
args = parser.parse_args()
|
| 185 |
+
|
| 186 |
+
if args.compare:
|
| 187 |
+
print_comparison_matrix()
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
levels = [args.level] if args.level else [1, 2, 3]
|
| 191 |
+
|
| 192 |
+
print(f"\n{'#'*70}")
|
| 193 |
+
print(f"# BAYAN v2.0 — BENCHMARK MATRIX")
|
| 194 |
+
print(f"# Levels: {levels}")
|
| 195 |
+
print(f"# Target: {args.url}")
|
| 196 |
+
print(f"{'#'*70}")
|
| 197 |
+
|
| 198 |
+
for level in levels:
|
| 199 |
+
success = run_level(level, args.url, args.dataset)
|
| 200 |
+
if not success:
|
| 201 |
+
print(f"\n❌ Level {level} failed!")
|
| 202 |
+
sys.exit(1)
|
| 203 |
+
print(f"\n✅ Level {level} complete!")
|
| 204 |
+
|
| 205 |
+
# Print comparison
|
| 206 |
+
print_comparison_matrix()
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
if __name__ == "__main__":
|
| 210 |
+
main()
|
tests/v2/test_level1_raw.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN v2.0 — Level 1: Raw Model Tests
|
| 3 |
+
======================================
|
| 4 |
+
Tests each ML model DIRECTLY — no filters, no pipeline, no StageLocker.
|
| 5 |
+
Measures the raw model ceiling: what's the best each model can do?
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python tests/v2/test_level1_raw.py --url URL [--dataset DATASET]
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import json
|
| 12 |
+
import re
|
| 13 |
+
import time
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from dataclasses import dataclass, field, asdict
|
| 17 |
+
from typing import List, Dict, Optional
|
| 18 |
+
import requests
|
| 19 |
+
|
| 20 |
+
# Datasets
|
| 21 |
+
DATASETS_DIR = Path(__file__).parent.parent / "phase10" / "gold_datasets"
|
| 22 |
+
|
| 23 |
+
REPORT_DIR = Path(__file__).parent / "reports"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class RawModelResult:
|
| 28 |
+
id: str
|
| 29 |
+
dataset: str
|
| 30 |
+
category: str
|
| 31 |
+
input_text: str
|
| 32 |
+
expected: str # What the benchmark expects
|
| 33 |
+
severity: str
|
| 34 |
+
# Raw model outputs
|
| 35 |
+
spelling_raw: str = ""
|
| 36 |
+
spelling_ms: int = 0
|
| 37 |
+
grammar_raw: str = ""
|
| 38 |
+
grammar_ms: int = 0
|
| 39 |
+
punctuation_raw: str = ""
|
| 40 |
+
punctuation_ms: int = 0
|
| 41 |
+
# Analysis
|
| 42 |
+
spelling_changed: bool = False
|
| 43 |
+
grammar_changed: bool = False
|
| 44 |
+
punctuation_changed: bool = False
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class APIClient:
|
| 48 |
+
"""Minimal client to call individual model endpoints."""
|
| 49 |
+
def __init__(self, base_url):
|
| 50 |
+
self.base = base_url.rstrip('/')
|
| 51 |
+
self.session = requests.Session()
|
| 52 |
+
self.session.headers['Content-Type'] = 'application/json'
|
| 53 |
+
|
| 54 |
+
def _post(self, endpoint, text, timeout=120):
|
| 55 |
+
t0 = time.time()
|
| 56 |
+
try:
|
| 57 |
+
r = self.session.post(
|
| 58 |
+
f"{self.base}{endpoint}",
|
| 59 |
+
json={"text": text},
|
| 60 |
+
timeout=timeout
|
| 61 |
+
)
|
| 62 |
+
ms = int((time.time() - t0) * 1000)
|
| 63 |
+
data = r.json()
|
| 64 |
+
return data, ms
|
| 65 |
+
except Exception as e:
|
| 66 |
+
ms = int((time.time() - t0) * 1000)
|
| 67 |
+
return {"error": str(e)}, ms
|
| 68 |
+
|
| 69 |
+
def spelling_raw(self, text):
|
| 70 |
+
"""Call /api/spelling — raw spelling model through API."""
|
| 71 |
+
data, ms = self._post("/api/spelling", text)
|
| 72 |
+
corrected = data.get("corrected_text", data.get("corrected", text))
|
| 73 |
+
return corrected, ms
|
| 74 |
+
|
| 75 |
+
def grammar_raw(self, text):
|
| 76 |
+
"""Call /api/grammar — raw grammar model through API."""
|
| 77 |
+
data, ms = self._post("/api/grammar", text)
|
| 78 |
+
corrected = data.get("corrected_text", data.get("corrected", text))
|
| 79 |
+
return corrected, ms
|
| 80 |
+
|
| 81 |
+
def punctuation_raw(self, text):
|
| 82 |
+
"""Call /api/punctuation — raw punctuation model through API."""
|
| 83 |
+
data, ms = self._post("/api/punctuation", text)
|
| 84 |
+
corrected = data.get("corrected_text", data.get("corrected", text))
|
| 85 |
+
return corrected, ms
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def load_datasets(dataset_filter=None):
|
| 89 |
+
"""Load all gold datasets."""
|
| 90 |
+
datasets = {}
|
| 91 |
+
for f in sorted(DATASETS_DIR.glob("*.json")):
|
| 92 |
+
name = f.stem
|
| 93 |
+
if dataset_filter and name != dataset_filter:
|
| 94 |
+
continue
|
| 95 |
+
with open(f, 'r', encoding='utf-8') as fh:
|
| 96 |
+
data = json.load(fh)
|
| 97 |
+
datasets[name] = data
|
| 98 |
+
return datasets
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def strip_diacritics(text):
|
| 102 |
+
return re.sub(r'[\u064B-\u065F\u0670]', '', text)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def normalize(text):
|
| 106 |
+
t = strip_diacritics(text)
|
| 107 |
+
t = re.sub(r'\s+', ' ', t).strip()
|
| 108 |
+
return t
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def run_level1(api: APIClient, datasets: dict) -> List[RawModelResult]:
|
| 112 |
+
"""Run each test case through all 3 raw models."""
|
| 113 |
+
results = []
|
| 114 |
+
total = sum(len(v) for v in datasets.values())
|
| 115 |
+
idx = 0
|
| 116 |
+
|
| 117 |
+
for ds_name, cases in datasets.items():
|
| 118 |
+
print(f"\n{'='*60}")
|
| 119 |
+
print(f"DATASET: {ds_name.upper()} ({len(cases)} samples)")
|
| 120 |
+
print(f"{'='*60}")
|
| 121 |
+
|
| 122 |
+
for case in cases:
|
| 123 |
+
idx += 1
|
| 124 |
+
cid = case.get('id', f'{ds_name}_{idx}')
|
| 125 |
+
cat = case.get('category', '')
|
| 126 |
+
inp = case.get('input', '')
|
| 127 |
+
expected = case.get('expected', case.get('input', ''))
|
| 128 |
+
severity = case.get('severity', '')
|
| 129 |
+
|
| 130 |
+
r = RawModelResult(
|
| 131 |
+
id=cid, dataset=ds_name, category=cat,
|
| 132 |
+
input_text=inp, expected=expected, severity=severity
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
print(f" [{idx}/{total}] {cid} ({cat})...", end=" ", flush=True)
|
| 136 |
+
|
| 137 |
+
# ── Spelling ──
|
| 138 |
+
try:
|
| 139 |
+
r.spelling_raw, r.spelling_ms = api.spelling_raw(inp)
|
| 140 |
+
r.spelling_changed = (normalize(r.spelling_raw) != normalize(inp))
|
| 141 |
+
except Exception as e:
|
| 142 |
+
r.spelling_raw = f"ERROR: {e}"
|
| 143 |
+
|
| 144 |
+
# ── Grammar ──
|
| 145 |
+
try:
|
| 146 |
+
r.grammar_raw, r.grammar_ms = api.grammar_raw(inp)
|
| 147 |
+
r.grammar_changed = (normalize(r.grammar_raw) != normalize(inp))
|
| 148 |
+
except Exception as e:
|
| 149 |
+
r.grammar_raw = f"ERROR: {e}"
|
| 150 |
+
|
| 151 |
+
# ── Punctuation ──
|
| 152 |
+
try:
|
| 153 |
+
r.punctuation_raw, r.punctuation_ms = api.punctuation_raw(inp)
|
| 154 |
+
r.punctuation_changed = (normalize(r.punctuation_raw) != normalize(inp))
|
| 155 |
+
except Exception as e:
|
| 156 |
+
r.punctuation_raw = f"ERROR: {e}"
|
| 157 |
+
|
| 158 |
+
total_ms = r.spelling_ms + r.grammar_ms + r.punctuation_ms
|
| 159 |
+
changes = []
|
| 160 |
+
if r.spelling_changed: changes.append("S")
|
| 161 |
+
if r.grammar_changed: changes.append("G")
|
| 162 |
+
if r.punctuation_changed: changes.append("P")
|
| 163 |
+
change_str = "+".join(changes) if changes else "none"
|
| 164 |
+
print(f"[{change_str}] ({total_ms}ms)")
|
| 165 |
+
|
| 166 |
+
results.append(r)
|
| 167 |
+
|
| 168 |
+
return results
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def analyze_results(results: List[RawModelResult]) -> dict:
|
| 172 |
+
"""Produce per-model analysis of raw outputs."""
|
| 173 |
+
analysis = {
|
| 174 |
+
"total": len(results),
|
| 175 |
+
"by_model": {
|
| 176 |
+
"spelling": {"changed": 0, "unchanged": 0, "errors": 0},
|
| 177 |
+
"grammar": {"changed": 0, "unchanged": 0, "errors": 0},
|
| 178 |
+
"punctuation": {"changed": 0, "unchanged": 0, "errors": 0},
|
| 179 |
+
},
|
| 180 |
+
"by_dataset": {},
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
for r in results:
|
| 184 |
+
ds = r.dataset
|
| 185 |
+
if ds not in analysis["by_dataset"]:
|
| 186 |
+
analysis["by_dataset"][ds] = {
|
| 187 |
+
"total": 0,
|
| 188 |
+
"spelling_changed": 0,
|
| 189 |
+
"grammar_changed": 0,
|
| 190 |
+
"punctuation_changed": 0,
|
| 191 |
+
}
|
| 192 |
+
analysis["by_dataset"][ds]["total"] += 1
|
| 193 |
+
|
| 194 |
+
# Spelling
|
| 195 |
+
if "ERROR" in r.spelling_raw:
|
| 196 |
+
analysis["by_model"]["spelling"]["errors"] += 1
|
| 197 |
+
elif r.spelling_changed:
|
| 198 |
+
analysis["by_model"]["spelling"]["changed"] += 1
|
| 199 |
+
analysis["by_dataset"][ds]["spelling_changed"] += 1
|
| 200 |
+
else:
|
| 201 |
+
analysis["by_model"]["spelling"]["unchanged"] += 1
|
| 202 |
+
|
| 203 |
+
# Grammar
|
| 204 |
+
if "ERROR" in r.grammar_raw:
|
| 205 |
+
analysis["by_model"]["grammar"]["errors"] += 1
|
| 206 |
+
elif r.grammar_changed:
|
| 207 |
+
analysis["by_model"]["grammar"]["changed"] += 1
|
| 208 |
+
analysis["by_dataset"][ds]["grammar_changed"] += 1
|
| 209 |
+
else:
|
| 210 |
+
analysis["by_model"]["grammar"]["unchanged"] += 1
|
| 211 |
+
|
| 212 |
+
# Punctuation
|
| 213 |
+
if "ERROR" in r.punctuation_raw:
|
| 214 |
+
analysis["by_model"]["punctuation"]["errors"] += 1
|
| 215 |
+
elif r.punctuation_changed:
|
| 216 |
+
analysis["by_model"]["punctuation"]["changed"] += 1
|
| 217 |
+
analysis["by_dataset"][ds]["punctuation_changed"] += 1
|
| 218 |
+
else:
|
| 219 |
+
analysis["by_model"]["punctuation"]["unchanged"] += 1
|
| 220 |
+
|
| 221 |
+
return analysis
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def print_analysis(analysis: dict):
|
| 225 |
+
print(f"\n{'='*60}")
|
| 226 |
+
print("LEVEL 1: RAW MODEL ANALYSIS")
|
| 227 |
+
print(f"{'='*60}")
|
| 228 |
+
|
| 229 |
+
print(f"\n## Per-Model Summary ({analysis['total']} tests)")
|
| 230 |
+
print(f"| Model | Changed | Unchanged | Errors |")
|
| 231 |
+
print(f"|-------------|---------|-----------|--------|")
|
| 232 |
+
for model, data in analysis["by_model"].items():
|
| 233 |
+
print(f"| {model:<11} | {data['changed']:>7} | {data['unchanged']:>9} | {data['errors']:>6} |")
|
| 234 |
+
|
| 235 |
+
print(f"\n## Change Rate by Dataset")
|
| 236 |
+
print(f"| Dataset | Total | S-Changed | G-Changed | P-Changed |")
|
| 237 |
+
print(f"|--------------|-------|-----------|-----------|-----------|")
|
| 238 |
+
for ds, data in sorted(analysis["by_dataset"].items()):
|
| 239 |
+
print(f"| {ds:<12} | {data['total']:>5} | {data['spelling_changed']:>9} | {data['grammar_changed']:>9} | {data['punctuation_changed']:>9} |")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def main():
|
| 243 |
+
parser = argparse.ArgumentParser(description="Level 1: Raw Model Tests")
|
| 244 |
+
parser.add_argument("--url", default="https://bayan10-bayan-api.hf.space")
|
| 245 |
+
parser.add_argument("--dataset", default=None, help="Filter to single dataset")
|
| 246 |
+
args = parser.parse_args()
|
| 247 |
+
|
| 248 |
+
api = APIClient(args.url)
|
| 249 |
+
datasets = load_datasets(args.dataset)
|
| 250 |
+
|
| 251 |
+
print(f"\n{'='*60}")
|
| 252 |
+
print("BAYAN v2.0 — Level 1: Raw Model Tests")
|
| 253 |
+
print(f"{'='*60}")
|
| 254 |
+
print(f" Target: {args.url}")
|
| 255 |
+
print(f" Datasets: {list(datasets.keys())}")
|
| 256 |
+
print(f" Total: {sum(len(v) for v in datasets.values())} tests")
|
| 257 |
+
print(f"{'='*60}")
|
| 258 |
+
|
| 259 |
+
results = run_level1(api, datasets)
|
| 260 |
+
analysis = analyze_results(results)
|
| 261 |
+
print_analysis(analysis)
|
| 262 |
+
|
| 263 |
+
# Save results
|
| 264 |
+
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
| 265 |
+
out_path = REPORT_DIR / "level1_raw_results.json"
|
| 266 |
+
report = {
|
| 267 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 268 |
+
"target": args.url,
|
| 269 |
+
"analysis": analysis,
|
| 270 |
+
"results": [asdict(r) for r in results],
|
| 271 |
+
}
|
| 272 |
+
with open(out_path, 'w', encoding='utf-8') as f:
|
| 273 |
+
json.dump(report, f, ensure_ascii=False, indent=2)
|
| 274 |
+
print(f"\n[L1] Results → {out_path}")
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
if __name__ == "__main__":
|
| 278 |
+
main()
|
tests/v2/test_level2_solo.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN v2.0 — Level 2: Solo API Tests
|
| 3 |
+
=====================================
|
| 4 |
+
Tests each model through its INDIVIDUAL API endpoint (/api/spelling, /api/grammar,
|
| 5 |
+
/api/punctuation). This measures what each stage produces in isolation — with
|
| 6 |
+
any endpoint-level preprocessing but WITHOUT pipeline integration (StageLocker,
|
| 7 |
+
OffsetMapper, cross-stage interaction).
|
| 8 |
+
|
| 9 |
+
Compares with Level 1 raw results to measure filter impact:
|
| 10 |
+
- If L2 passes more tests than L1 → filters are helping
|
| 11 |
+
- If L2 passes fewer tests than L1 → filters are over-filtering
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
python tests/v2/test_level2_solo.py --url URL [--dataset DATASET]
|
| 15 |
+
"""
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import re
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from dataclasses import dataclass, asdict
|
| 22 |
+
from typing import List
|
| 23 |
+
import requests
|
| 24 |
+
|
| 25 |
+
DATASETS_DIR = Path(__file__).parent.parent / "phase10" / "gold_datasets"
|
| 26 |
+
REPORT_DIR = Path(__file__).parent / "reports"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def normalize(text):
|
| 30 |
+
t = re.sub(r'[\u064B-\u065F\u0670]', '', text)
|
| 31 |
+
t = re.sub(r'\s+', ' ', t).strip()
|
| 32 |
+
return t
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class SoloResult:
|
| 37 |
+
id: str
|
| 38 |
+
dataset: str
|
| 39 |
+
category: str
|
| 40 |
+
input_text: str
|
| 41 |
+
expected: str
|
| 42 |
+
severity: str
|
| 43 |
+
# Solo API outputs (each model called independently on the SAME input)
|
| 44 |
+
spelling_solo: str = ""
|
| 45 |
+
spelling_ms: int = 0
|
| 46 |
+
grammar_solo: str = ""
|
| 47 |
+
grammar_ms: int = 0
|
| 48 |
+
punctuation_solo: str = ""
|
| 49 |
+
punctuation_ms: int = 0
|
| 50 |
+
# Verdict per model
|
| 51 |
+
spelling_verdict: str = "" # TP, TN, FP, FN
|
| 52 |
+
grammar_verdict: str = ""
|
| 53 |
+
punctuation_verdict: str = ""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class APIClient:
|
| 57 |
+
def __init__(self, base_url):
|
| 58 |
+
self.base = base_url.rstrip('/')
|
| 59 |
+
self.session = requests.Session()
|
| 60 |
+
self.session.headers['Content-Type'] = 'application/json'
|
| 61 |
+
|
| 62 |
+
def call(self, endpoint, text, timeout=120):
|
| 63 |
+
t0 = time.time()
|
| 64 |
+
try:
|
| 65 |
+
r = self.session.post(
|
| 66 |
+
f"{self.base}{endpoint}",
|
| 67 |
+
json={"text": text},
|
| 68 |
+
timeout=timeout
|
| 69 |
+
)
|
| 70 |
+
ms = int((time.time() - t0) * 1000)
|
| 71 |
+
data = r.json()
|
| 72 |
+
corrected = data.get("corrected_text", data.get("corrected", text))
|
| 73 |
+
return corrected, ms
|
| 74 |
+
except Exception as e:
|
| 75 |
+
ms = int((time.time() - t0) * 1000)
|
| 76 |
+
return f"ERROR: {e}", ms
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def classify_result(input_text, output_text, expected_text, dataset):
|
| 80 |
+
"""Classify a single model output as TP/TN/FP/FN.
|
| 81 |
+
|
| 82 |
+
For datasets that test CORRECTION (spelling, grammar, punctuation):
|
| 83 |
+
- TP: model corrected AND the correction is in the expected direction
|
| 84 |
+
- FN: model did NOT correct (output == input) but should have
|
| 85 |
+
- FP: model corrected but incorrectly (changed text that was correct or wrong direction)
|
| 86 |
+
- TN: model correctly left unchanged (output == input AND input was correct)
|
| 87 |
+
|
| 88 |
+
For datasets that test PRESERVATION (entities, religious, structured, hallucination):
|
| 89 |
+
- TN: model left text unchanged → PASS
|
| 90 |
+
- FP: model modified text → FAIL
|
| 91 |
+
"""
|
| 92 |
+
inp_n = normalize(input_text)
|
| 93 |
+
out_n = normalize(output_text)
|
| 94 |
+
exp_n = normalize(expected_text)
|
| 95 |
+
|
| 96 |
+
is_preservation = dataset in ('entities', 'religious', 'structured', 'hallucination')
|
| 97 |
+
text_changed = (out_n != inp_n)
|
| 98 |
+
|
| 99 |
+
if is_preservation:
|
| 100 |
+
# For preservation tests, the expected output == input (don't change)
|
| 101 |
+
if not text_changed:
|
| 102 |
+
return "TN" # Correctly preserved
|
| 103 |
+
else:
|
| 104 |
+
return "FP" # Incorrectly modified
|
| 105 |
+
else:
|
| 106 |
+
# For correction tests
|
| 107 |
+
needs_correction = (inp_n != exp_n)
|
| 108 |
+
|
| 109 |
+
if needs_correction:
|
| 110 |
+
if text_changed:
|
| 111 |
+
# Check if output matches expected (or is closer to expected)
|
| 112 |
+
if out_n == exp_n:
|
| 113 |
+
return "TP" # Perfect correction
|
| 114 |
+
elif _edit_distance(out_n, exp_n) < _edit_distance(inp_n, exp_n):
|
| 115 |
+
return "TP" # Partial but improving correction
|
| 116 |
+
else:
|
| 117 |
+
return "FP" # Changed but not in right direction
|
| 118 |
+
else:
|
| 119 |
+
return "FN" # Should have corrected but didn't
|
| 120 |
+
else:
|
| 121 |
+
if text_changed:
|
| 122 |
+
return "FP" # Changed text that was already correct
|
| 123 |
+
else:
|
| 124 |
+
return "TN" # Correctly left unchanged
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _edit_distance(a, b):
|
| 128 |
+
"""Simple Levenshtein edit distance."""
|
| 129 |
+
if len(a) < len(b):
|
| 130 |
+
return _edit_distance(b, a)
|
| 131 |
+
if len(b) == 0:
|
| 132 |
+
return len(a)
|
| 133 |
+
|
| 134 |
+
prev = list(range(len(b) + 1))
|
| 135 |
+
for i, ca in enumerate(a):
|
| 136 |
+
curr = [i + 1]
|
| 137 |
+
for j, cb in enumerate(b):
|
| 138 |
+
cost = 0 if ca == cb else 1
|
| 139 |
+
curr.append(min(curr[j] + 1, prev[j + 1] + 1, prev[j] + cost))
|
| 140 |
+
prev = curr
|
| 141 |
+
return prev[len(b)]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def load_datasets(dataset_filter=None):
|
| 145 |
+
datasets = {}
|
| 146 |
+
for f in sorted(DATASETS_DIR.glob("*.json")):
|
| 147 |
+
name = f.stem
|
| 148 |
+
if dataset_filter and name != dataset_filter:
|
| 149 |
+
continue
|
| 150 |
+
with open(f, 'r', encoding='utf-8') as fh:
|
| 151 |
+
datasets[name] = json.load(fh)
|
| 152 |
+
return datasets
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def run_level2(api: APIClient, datasets: dict) -> List[SoloResult]:
|
| 156 |
+
results = []
|
| 157 |
+
total = sum(len(v) for v in datasets.values())
|
| 158 |
+
idx = 0
|
| 159 |
+
|
| 160 |
+
for ds_name, cases in datasets.items():
|
| 161 |
+
print(f"\n{'='*60}")
|
| 162 |
+
print(f"DATASET: {ds_name.upper()} ({len(cases)} samples)")
|
| 163 |
+
print(f"{'='*60}")
|
| 164 |
+
|
| 165 |
+
for case in cases:
|
| 166 |
+
idx += 1
|
| 167 |
+
cid = case.get('id', f'{ds_name}_{idx}')
|
| 168 |
+
cat = case.get('category', '')
|
| 169 |
+
inp = case.get('input', '')
|
| 170 |
+
expected = case.get('expected', case.get('input', ''))
|
| 171 |
+
severity = case.get('severity', '')
|
| 172 |
+
|
| 173 |
+
r = SoloResult(
|
| 174 |
+
id=cid, dataset=ds_name, category=cat,
|
| 175 |
+
input_text=inp, expected=expected, severity=severity
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
print(f" [{idx}/{total}] {cid} ({cat})...", end=" ", flush=True)
|
| 179 |
+
|
| 180 |
+
# Test each model independently on the SAME original input
|
| 181 |
+
r.spelling_solo, r.spelling_ms = api.call("/api/spelling", inp)
|
| 182 |
+
r.grammar_solo, r.grammar_ms = api.call("/api/grammar", inp)
|
| 183 |
+
r.punctuation_solo, r.punctuation_ms = api.call("/api/punctuation", inp)
|
| 184 |
+
|
| 185 |
+
# Classify each model's result
|
| 186 |
+
r.spelling_verdict = classify_result(inp, r.spelling_solo, expected, ds_name)
|
| 187 |
+
r.grammar_verdict = classify_result(inp, r.grammar_solo, expected, ds_name)
|
| 188 |
+
r.punctuation_verdict = classify_result(inp, r.punctuation_solo, expected, ds_name)
|
| 189 |
+
|
| 190 |
+
total_ms = r.spelling_ms + r.grammar_ms + r.punctuation_ms
|
| 191 |
+
print(f"S={r.spelling_verdict} G={r.grammar_verdict} P={r.punctuation_verdict} ({total_ms}ms)")
|
| 192 |
+
|
| 193 |
+
results.append(r)
|
| 194 |
+
|
| 195 |
+
return results
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def analyze_and_print(results: List[SoloResult]) -> dict:
|
| 199 |
+
analysis = {"total": len(results), "by_model": {}, "by_dataset": {}}
|
| 200 |
+
|
| 201 |
+
for model in ("spelling", "grammar", "punctuation"):
|
| 202 |
+
verdicts = {"TP": 0, "TN": 0, "FP": 0, "FN": 0}
|
| 203 |
+
for r in results:
|
| 204 |
+
v = getattr(r, f"{model}_verdict", "")
|
| 205 |
+
if v in verdicts:
|
| 206 |
+
verdicts[v] += 1
|
| 207 |
+
total = sum(verdicts.values())
|
| 208 |
+
pass_count = verdicts["TP"] + verdicts["TN"]
|
| 209 |
+
analysis["by_model"][model] = {
|
| 210 |
+
**verdicts,
|
| 211 |
+
"pass_rate": round(pass_count / total, 4) if total else 0,
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
# Per-dataset breakdown
|
| 215 |
+
for ds in set(r.dataset for r in results):
|
| 216 |
+
ds_results = [r for r in results if r.dataset == ds]
|
| 217 |
+
ds_analysis = {}
|
| 218 |
+
for model in ("spelling", "grammar", "punctuation"):
|
| 219 |
+
verdicts = {"TP": 0, "TN": 0, "FP": 0, "FN": 0}
|
| 220 |
+
for r in ds_results:
|
| 221 |
+
v = getattr(r, f"{model}_verdict", "")
|
| 222 |
+
if v in verdicts:
|
| 223 |
+
verdicts[v] += 1
|
| 224 |
+
total = sum(verdicts.values())
|
| 225 |
+
pass_count = verdicts["TP"] + verdicts["TN"]
|
| 226 |
+
ds_analysis[model] = {
|
| 227 |
+
**verdicts,
|
| 228 |
+
"pass_rate": round(pass_count / total, 4) if total else 0,
|
| 229 |
+
}
|
| 230 |
+
analysis["by_dataset"][ds] = {"total": len(ds_results), **ds_analysis}
|
| 231 |
+
|
| 232 |
+
# Print
|
| 233 |
+
print(f"\n{'='*60}")
|
| 234 |
+
print("LEVEL 2: SOLO API ANALYSIS")
|
| 235 |
+
print(f"{'='*60}")
|
| 236 |
+
|
| 237 |
+
print(f"\n## Per-Model Summary ({analysis['total']} tests)")
|
| 238 |
+
print(f"| Model | TP | TN | FP | FN | Pass% |")
|
| 239 |
+
print(f"|-------------|-----|-----|-----|-----|--------|")
|
| 240 |
+
for model, data in analysis["by_model"].items():
|
| 241 |
+
print(f"| {model:<11} | {data['TP']:>3} | {data['TN']:>3} | {data['FP']:>3} | {data['FN']:>3} | {data['pass_rate']*100:5.1f}% |")
|
| 242 |
+
|
| 243 |
+
print(f"\n## Per-Dataset × Model Pass Rate")
|
| 244 |
+
print(f"| Dataset | Spelling | Grammar | Punctuation |")
|
| 245 |
+
print(f"|--------------|----------|---------|-------------|")
|
| 246 |
+
for ds in sorted(analysis["by_dataset"].keys()):
|
| 247 |
+
d = analysis["by_dataset"][ds]
|
| 248 |
+
s = d["spelling"]["pass_rate"] * 100
|
| 249 |
+
g = d["grammar"]["pass_rate"] * 100
|
| 250 |
+
p = d["punctuation"]["pass_rate"] * 100
|
| 251 |
+
print(f"| {ds:<12} | {s:6.1f}% | {g:5.1f}% | {p:9.1f}% |")
|
| 252 |
+
|
| 253 |
+
return analysis
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def main():
|
| 257 |
+
parser = argparse.ArgumentParser(description="Level 2: Solo API Tests")
|
| 258 |
+
parser.add_argument("--url", default="https://bayan10-bayan-api.hf.space")
|
| 259 |
+
parser.add_argument("--dataset", default=None)
|
| 260 |
+
args = parser.parse_args()
|
| 261 |
+
|
| 262 |
+
api = APIClient(args.url)
|
| 263 |
+
datasets = load_datasets(args.dataset)
|
| 264 |
+
|
| 265 |
+
print(f"\n{'='*60}")
|
| 266 |
+
print("BAYAN v2.0 — Level 2: Solo API Tests")
|
| 267 |
+
print(f"{'='*60}")
|
| 268 |
+
print(f" Target: {args.url}")
|
| 269 |
+
print(f" Datasets: {list(datasets.keys())}")
|
| 270 |
+
print(f" Total: {sum(len(v) for v in datasets.values())} tests")
|
| 271 |
+
|
| 272 |
+
results = run_level2(api, datasets)
|
| 273 |
+
analysis = analyze_and_print(results)
|
| 274 |
+
|
| 275 |
+
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
| 276 |
+
out_path = REPORT_DIR / "level2_solo_results.json"
|
| 277 |
+
report = {
|
| 278 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 279 |
+
"target": args.url,
|
| 280 |
+
"analysis": analysis,
|
| 281 |
+
"results": [asdict(r) for r in results],
|
| 282 |
+
}
|
| 283 |
+
with open(out_path, 'w', encoding='utf-8') as f:
|
| 284 |
+
json.dump(report, f, ensure_ascii=False, indent=2)
|
| 285 |
+
print(f"\n[L2] Results → {out_path}")
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
if __name__ == "__main__":
|
| 289 |
+
main()
|
tests/v2/test_level3_integrated.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BAYAN v2.0 — Level 3: Integrated Pipeline Tests
|
| 3 |
+
=================================================
|
| 4 |
+
Tests the FULL integrated pipeline through /api/analyze.
|
| 5 |
+
This is the end-to-end test: Spelling → Grammar → Punctuation
|
| 6 |
+
with all filters, StageLocker, OffsetMapper, PatchSet.
|
| 7 |
+
|
| 8 |
+
Reuses the exact same verdict logic as the existing benchmark_runner.py
|
| 9 |
+
to ensure comparability.
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
python tests/v2/test_level3_integrated.py --url URL [--dataset DATASET]
|
| 13 |
+
"""
|
| 14 |
+
import argparse
|
| 15 |
+
import json
|
| 16 |
+
import re
|
| 17 |
+
import time
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from dataclasses import dataclass, asdict
|
| 20 |
+
from typing import List
|
| 21 |
+
import requests
|
| 22 |
+
|
| 23 |
+
DATASETS_DIR = Path(__file__).parent.parent / "phase10" / "gold_datasets"
|
| 24 |
+
REPORT_DIR = Path(__file__).parent / "reports"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def normalize(text):
|
| 28 |
+
t = re.sub(r'[\u064B-\u065F\u0670]', '', text)
|
| 29 |
+
t = re.sub(r'\s+', ' ', t).strip()
|
| 30 |
+
return t
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class IntegratedResult:
|
| 35 |
+
id: str
|
| 36 |
+
dataset: str
|
| 37 |
+
category: str
|
| 38 |
+
input_text: str
|
| 39 |
+
expected: str
|
| 40 |
+
severity: str
|
| 41 |
+
# Pipeline output
|
| 42 |
+
pipeline_corrected: str = ""
|
| 43 |
+
pipeline_suggestions: int = 0
|
| 44 |
+
pipeline_ms: int = 0
|
| 45 |
+
spelling_ms: int = 0
|
| 46 |
+
grammar_ms: int = 0
|
| 47 |
+
punctuation_ms: int = 0
|
| 48 |
+
# Verdict
|
| 49 |
+
verdict: str = "" # TP, TN, FP, FN
|
| 50 |
+
detail: str = ""
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class APIClient:
|
| 54 |
+
def __init__(self, base_url):
|
| 55 |
+
self.base = base_url.rstrip('/')
|
| 56 |
+
self.session = requests.Session()
|
| 57 |
+
self.session.headers['Content-Type'] = 'application/json'
|
| 58 |
+
|
| 59 |
+
def analyze(self, text, timeout=120):
|
| 60 |
+
t0 = time.time()
|
| 61 |
+
try:
|
| 62 |
+
r = self.session.post(
|
| 63 |
+
f"{self.base}/api/analyze",
|
| 64 |
+
json={"text": text},
|
| 65 |
+
timeout=timeout
|
| 66 |
+
)
|
| 67 |
+
ms = int((time.time() - t0) * 1000)
|
| 68 |
+
return r.json(), ms
|
| 69 |
+
except Exception as e:
|
| 70 |
+
ms = int((time.time() - t0) * 1000)
|
| 71 |
+
return {"error": str(e)}, ms
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def classify_pipeline(input_text, corrected_text, expected_text, dataset, entity=None):
|
| 75 |
+
"""Classify pipeline output.
|
| 76 |
+
|
| 77 |
+
For correction datasets (spelling, grammar, punctuation, collision):
|
| 78 |
+
Input has errors → should be corrected to match expected.
|
| 79 |
+
|
| 80 |
+
For preservation datasets (entities, religious, structured, hallucination):
|
| 81 |
+
Input is correct → should NOT be modified.
|
| 82 |
+
For entity tests: specifically check that the entity string is preserved.
|
| 83 |
+
"""
|
| 84 |
+
inp_n = normalize(input_text)
|
| 85 |
+
out_n = normalize(corrected_text)
|
| 86 |
+
exp_n = normalize(expected_text)
|
| 87 |
+
|
| 88 |
+
is_preservation = dataset in ('entities', 'religious', 'structured', 'hallucination')
|
| 89 |
+
text_changed = (out_n != inp_n)
|
| 90 |
+
|
| 91 |
+
if dataset == 'entities' and entity:
|
| 92 |
+
# Entity tests: check if entity is preserved in output
|
| 93 |
+
entity_n = normalize(entity)
|
| 94 |
+
if entity_n in out_n:
|
| 95 |
+
return "TN", "Entity preserved"
|
| 96 |
+
elif not text_changed:
|
| 97 |
+
return "TN", "Text unchanged"
|
| 98 |
+
else:
|
| 99 |
+
return "FP", f"ENTITY CORRUPTED: '{entity}' missing from output"
|
| 100 |
+
|
| 101 |
+
if is_preservation:
|
| 102 |
+
if not text_changed:
|
| 103 |
+
return "TN", "Text correctly preserved"
|
| 104 |
+
else:
|
| 105 |
+
# Check what changed
|
| 106 |
+
inp_words = inp_n.split()
|
| 107 |
+
out_words = out_n.split()
|
| 108 |
+
changes = []
|
| 109 |
+
for iw, ow in zip(inp_words, out_words):
|
| 110 |
+
if iw != ow:
|
| 111 |
+
changes.append(f"{iw}→{ow}")
|
| 112 |
+
detail = f"Text modified: {changes[:5]}"
|
| 113 |
+
return "FP", detail
|
| 114 |
+
else:
|
| 115 |
+
# Correction dataset
|
| 116 |
+
needs_correction = (inp_n != exp_n)
|
| 117 |
+
|
| 118 |
+
if needs_correction:
|
| 119 |
+
if out_n == exp_n:
|
| 120 |
+
return "TP", "Exact match"
|
| 121 |
+
elif text_changed and _closer(out_n, inp_n, exp_n):
|
| 122 |
+
return "TP", "Partial improvement"
|
| 123 |
+
elif not text_changed:
|
| 124 |
+
return "FN", "No correction applied"
|
| 125 |
+
else:
|
| 126 |
+
return "FP", f"Wrong correction"
|
| 127 |
+
else:
|
| 128 |
+
if not text_changed:
|
| 129 |
+
return "TN", "Correctly unchanged"
|
| 130 |
+
else:
|
| 131 |
+
return "FP", f"Modified correct text"
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _closer(output, input_text, expected):
|
| 135 |
+
"""Is output closer to expected than input was?"""
|
| 136 |
+
d_out = _edit_distance(output, expected)
|
| 137 |
+
d_inp = _edit_distance(input_text, expected)
|
| 138 |
+
return d_out < d_inp
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _edit_distance(a, b):
|
| 142 |
+
if len(a) < len(b):
|
| 143 |
+
return _edit_distance(b, a)
|
| 144 |
+
if len(b) == 0:
|
| 145 |
+
return len(a)
|
| 146 |
+
prev = list(range(len(b) + 1))
|
| 147 |
+
for i, ca in enumerate(a):
|
| 148 |
+
curr = [i + 1]
|
| 149 |
+
for j, cb in enumerate(b):
|
| 150 |
+
cost = 0 if ca == cb else 1
|
| 151 |
+
curr.append(min(curr[j] + 1, prev[j + 1] + 1, prev[j] + cost))
|
| 152 |
+
prev = curr
|
| 153 |
+
return prev[len(b)]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def load_datasets(dataset_filter=None):
|
| 157 |
+
datasets = {}
|
| 158 |
+
for f in sorted(DATASETS_DIR.glob("*.json")):
|
| 159 |
+
name = f.stem
|
| 160 |
+
if dataset_filter and name != dataset_filter:
|
| 161 |
+
continue
|
| 162 |
+
with open(f, 'r', encoding='utf-8') as fh:
|
| 163 |
+
datasets[name] = json.load(fh)
|
| 164 |
+
return datasets
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def run_level3(api: APIClient, datasets: dict) -> List[IntegratedResult]:
|
| 168 |
+
results = []
|
| 169 |
+
total = sum(len(v) for v in datasets.values())
|
| 170 |
+
idx = 0
|
| 171 |
+
|
| 172 |
+
for ds_name, cases in datasets.items():
|
| 173 |
+
print(f"\n{'='*60}")
|
| 174 |
+
print(f"DATASET: {ds_name.upper()} ({len(cases)} samples)")
|
| 175 |
+
print(f"{'='*60}")
|
| 176 |
+
|
| 177 |
+
tp = tn = fp = fn = 0
|
| 178 |
+
for case in cases:
|
| 179 |
+
idx += 1
|
| 180 |
+
cid = case.get('id', f'{ds_name}_{idx}')
|
| 181 |
+
cat = case.get('category', '')
|
| 182 |
+
inp = case.get('input', '')
|
| 183 |
+
expected = case.get('expected', case.get('input', ''))
|
| 184 |
+
severity = case.get('severity', '')
|
| 185 |
+
entity = case.get('entity', None)
|
| 186 |
+
|
| 187 |
+
r = IntegratedResult(
|
| 188 |
+
id=cid, dataset=ds_name, category=cat,
|
| 189 |
+
input_text=inp, expected=expected, severity=severity
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
print(f" [{idx}/{total}] {cid} ({cat})...", end=" ", flush=True)
|
| 193 |
+
|
| 194 |
+
data, ms = api.analyze(inp)
|
| 195 |
+
r.pipeline_ms = ms
|
| 196 |
+
r.pipeline_corrected = data.get('corrected', inp)
|
| 197 |
+
r.pipeline_suggestions = len(data.get('suggestions', []))
|
| 198 |
+
timing = data.get('timing_ms', {})
|
| 199 |
+
r.spelling_ms = timing.get('spelling_ms', 0)
|
| 200 |
+
r.grammar_ms = timing.get('grammar_ms', 0)
|
| 201 |
+
r.punctuation_ms = timing.get('punctuation_ms', 0)
|
| 202 |
+
|
| 203 |
+
r.verdict, r.detail = classify_pipeline(
|
| 204 |
+
inp, r.pipeline_corrected, expected, ds_name, entity
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
icon = {"TP": "✅", "TN": "✅", "FP": "❌", "FN": "⚠️"}.get(r.verdict, "?")
|
| 208 |
+
print(f"{icon} {r.verdict} ({r.pipeline_ms}ms)")
|
| 209 |
+
|
| 210 |
+
if r.verdict == "TP": tp += 1
|
| 211 |
+
elif r.verdict == "TN": tn += 1
|
| 212 |
+
elif r.verdict == "FP": fp += 1
|
| 213 |
+
elif r.verdict == "FN": fn += 1
|
| 214 |
+
|
| 215 |
+
results.append(r)
|
| 216 |
+
|
| 217 |
+
total_ds = tp + tn + fp + fn
|
| 218 |
+
pass_pct = (tp + tn) / total_ds * 100 if total_ds else 0
|
| 219 |
+
print(f"\n Pass={pass_pct:.1f}% TP={tp} TN={tn} FP={fp} FN={fn}")
|
| 220 |
+
|
| 221 |
+
return results
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def analyze_and_print(results: List[IntegratedResult]) -> dict:
|
| 225 |
+
verdicts = {"TP": 0, "TN": 0, "FP": 0, "FN": 0}
|
| 226 |
+
by_dataset = {}
|
| 227 |
+
|
| 228 |
+
for r in results:
|
| 229 |
+
verdicts[r.verdict] = verdicts.get(r.verdict, 0) + 1
|
| 230 |
+
if r.dataset not in by_dataset:
|
| 231 |
+
by_dataset[r.dataset] = {"TP": 0, "TN": 0, "FP": 0, "FN": 0, "total": 0}
|
| 232 |
+
by_dataset[r.dataset][r.verdict] += 1
|
| 233 |
+
by_dataset[r.dataset]["total"] += 1
|
| 234 |
+
|
| 235 |
+
total = sum(verdicts.values())
|
| 236 |
+
pass_count = verdicts["TP"] + verdicts["TN"]
|
| 237 |
+
|
| 238 |
+
analysis = {
|
| 239 |
+
"total": total,
|
| 240 |
+
"aggregate": {
|
| 241 |
+
**verdicts,
|
| 242 |
+
"pass_rate": round(pass_count / total, 4) if total else 0,
|
| 243 |
+
},
|
| 244 |
+
"by_dataset": {},
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
print(f"\n{'='*60}")
|
| 248 |
+
print("LEVEL 3: INTEGRATED PIPELINE ANALYSIS")
|
| 249 |
+
print(f"{'='*60}")
|
| 250 |
+
print(f"\n Total: {total} | Pass: {pass_count}/{total} ({analysis['aggregate']['pass_rate']*100:.1f}%)")
|
| 251 |
+
print(f" TP={verdicts['TP']} TN={verdicts['TN']} FP={verdicts['FP']} FN={verdicts['FN']}")
|
| 252 |
+
|
| 253 |
+
print(f"\n| Dataset | Total | TP | TN | FP | FN | Pass% |")
|
| 254 |
+
print(f"|--------------|-------|-----|-----|-----|-----|--------|")
|
| 255 |
+
for ds in sorted(by_dataset.keys()):
|
| 256 |
+
d = by_dataset[ds]
|
| 257 |
+
p = (d["TP"] + d["TN"]) / d["total"] * 100 if d["total"] else 0
|
| 258 |
+
print(f"| {ds:<12} | {d['total']:>5} | {d['TP']:>3} | {d['TN']:>3} | {d['FP']:>3} | {d['FN']:>3} | {p:5.1f}% |")
|
| 259 |
+
analysis["by_dataset"][ds] = {**d, "pass_rate": round(p / 100, 4)}
|
| 260 |
+
|
| 261 |
+
return analysis
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def main():
|
| 265 |
+
parser = argparse.ArgumentParser(description="Level 3: Integrated Pipeline Tests")
|
| 266 |
+
parser.add_argument("--url", default="https://bayan10-bayan-api.hf.space")
|
| 267 |
+
parser.add_argument("--dataset", default=None)
|
| 268 |
+
args = parser.parse_args()
|
| 269 |
+
|
| 270 |
+
api = APIClient(args.url)
|
| 271 |
+
datasets = load_datasets(args.dataset)
|
| 272 |
+
|
| 273 |
+
print(f"\n{'='*60}")
|
| 274 |
+
print("BAYAN v2.0 — Level 3: Integrated Pipeline Tests")
|
| 275 |
+
print(f"{'='*60}")
|
| 276 |
+
print(f" Target: {args.url}")
|
| 277 |
+
print(f" Datasets: {list(datasets.keys())}")
|
| 278 |
+
print(f" Total: {sum(len(v) for v in datasets.values())} tests")
|
| 279 |
+
|
| 280 |
+
results = run_level3(api, datasets)
|
| 281 |
+
analysis = analyze_and_print(results)
|
| 282 |
+
|
| 283 |
+
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
| 284 |
+
out_path = REPORT_DIR / "level3_integrated_results.json"
|
| 285 |
+
report = {
|
| 286 |
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 287 |
+
"target": args.url,
|
| 288 |
+
"analysis": analysis,
|
| 289 |
+
"results": [asdict(r) for r in results],
|
| 290 |
+
}
|
| 291 |
+
with open(out_path, 'w', encoding='utf-8') as f:
|
| 292 |
+
json.dump(report, f, ensure_ascii=False, indent=2)
|
| 293 |
+
print(f"\n[L3] Results → {out_path}")
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
if __name__ == "__main__":
|
| 297 |
+
main()
|