| """ |
| ๅฏน่ฟ็ซฏ HF Spaces ไธ้จ็ฝฒ็ NER API ๅ็ซฏๅฐ็ซฏๆต่ฏ๏ผ่ฆ็ๆๆ่ทฏ็ฑๅๆฏไธ่พน็ๆ
ๅตใ |
| ไธบๆฏไธช็จไพ่ฎฐๅฝ๏ผHTTP ็ถๆใ่ฏๅซๅฐ็ๅฎไฝใ่ฐ็จ่ๆถใ่ชๅจๆฃๆต็่ฏญ่จ๏ผๅฆๆ๏ผใ |
| ๆ็ป่พๅบ Markdown ๆฅๅ๏ผreports/remote_api_test_report.md |
| """ |
| import io |
| import json |
| import time |
| import urllib.request |
| import urllib.error |
| from dataclasses import dataclass, field |
| from pathlib import Path |
|
|
| BASE_URL = "https://robinwu-nerserver.hf.space" |
| EXTRACT = f"{BASE_URL}/api/v1/extract" |
| HEALTH = f"{BASE_URL}/api/v1/health" |
| REPORT = Path("reports/remote_api_test_report.md") |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| CASES: list[dict] = [ |
| |
| { |
| "id": "EN-01", "group": "EN โ GLiNER ไธป่ทฏๅพ", |
| "description": "่ฑๆ็ญๅฅ๏ผๆพๅผ language=en๏ผ่ชๅฎไนๆ ็ญพ", |
| "payload": { |
| "text": "Elon Musk founded SpaceX in Hawthorne, California in 2002.", |
| "labels": ["full name of a person", "company or organization name", |
| "geographical location", "date or year"], |
| "language": "en", |
| }, |
| "expected": {"Elon Musk", "SpaceX", "Hawthorne", "California", "2002"}, |
| }, |
| { |
| "id": "EN-02", "group": "EN โ GLiNER ไธป่ทฏๅพ", |
| "description": "่ฑๆ้ฟๆฎต๏ผlabels ็็ฉบ่งฆๅ้ป่ฎคๅ่ฏญๆ ็ญพ้", |
| "payload": { |
| "text": ("President Biden signed the Inflation Reduction Act in " |
| "Washington D.C. on August 16, 2022. The legislation was " |
| "championed by Senator Chuck Schumer and was seen as a major " |
| "win for the Democratic Party."), |
| "language": "en", |
| }, |
| "expected": {"Biden", "Chuck Schumer", "Washington D.C.", "Democratic Party"}, |
| }, |
| |
| { |
| "id": "ZH-01", "group": "ZH โ BERT ไธป่ทฏๅพ", |
| "description": "ไธญๆ็ฐไปฃๅไธๆๆฌ๏ผๆพๅผ language=zh", |
| "payload": { |
| "text": "้ฟ้ๅทดๅทด้ๅขๅๅงไบบ้ฉฌไบไบ2019ๅนดๅธไปป่ฃไบๅฑไธปๅธญ๏ผ็ฑๅผ ๅๆฅไปปใ" |
| "ๆป้จไฝไบๆญๅท็้ฟ้ๅทดๅทดๆไธๆฅๆๆทๅฎใๅคฉ็ซใๆฏไปๅฎ็ญไธๅกๆฟๅใ", |
| "language": "zh", |
| }, |
| "expected": {"้ฉฌไบ", "ๅผ ๅ", "้ฟ้ๅทดๅทด", "ๆญๅท"}, |
| }, |
| { |
| "id": "ZH-02", "group": "ZH โ BERT ไธป่ทฏๅพ", |
| "description": "ไธญๆๅป็ๅบๆฏ๏ผ่ชๅฎไนๅ่ฏญๆ ็ญพ", |
| "payload": { |
| "text": "ๅไบฌๅๅๅป้ขๅฟๅ
็งไธปไปป็ๅปบๅฝๆๆๅข้๏ผไบ2023ๅนดๆๅๅฎๆ้ฆไพ" |
| "ๆบๅจไบบ่พ
ๅฉๅ ็ถๅจ่ๆญๆกฅๆๆฏ๏ผๆฃ่
ๆฅ่ชๅฑฑไธ็ๆตๅๅธใ", |
| "labels": ["ไบบๅๆๅงๅ", "ๅป้ขๆๅป็ๆบๆๅ็งฐ", "ๅฐๅๆๅๅธ", "ๆฅๆๆๅนดไปฝ"], |
| "language": "zh", |
| }, |
| "expected": {"็ๅปบๅฝ", "ๅไบฌๅๅๅป้ข", "ๆตๅ"}, |
| }, |
| { |
| "id": "ZH-03", "group": "ZH โ BERT ่พน็่ฏๅซ", |
| "description": "ๅคๅ
ธๆๅญฆ่พน็ๆต่ฏ โ ใๅฐคๆฐๆฅ่ฏทใๅบๅชๅใๅฐคๆฐใ", |
| "payload": { |
| "text": "ๅฐคๆฐๆฅ่ฏท๏ผ็็ๅค็ฌ้๏ผไฝ ๆฅไบใ่ดพๆฏๅฝไบบๆ้
๏ผๅฎ็ๅ้ป็ๅจๅคง่งๅญๆฃๆญฅใ", |
| "language": "zh", |
| }, |
| "expected": {"ๅฐคๆฐ", "็็ๅค", "่ดพๆฏ", "ๅฎ็", "้ป็", "ๅคง่งๅญ"}, |
| "must_not_contain": {"ๅฐคๆฐๆฅ่ฏท", "็็ๅค็ฌ้"}, |
| }, |
| |
| { |
| "id": "AR-01", "group": "AR โ GLiNER ไธป่ทฏๅพ", |
| "description": "้ฟๆไผฏ่ฏญๆฐ้ป", |
| "payload": { |
| "text": ("ุฃุนูู ุงูุฑุฆูุณ ู
ุญู
ุฏ ุจู ุณูู
ุงู ุนู ุฅุทูุงู ู
ุดุฑูุน ูููู
ูู ุงูู
ู
ููุฉ " |
| "ุงูุนุฑุจูุฉ ุงูุณุนูุฏูุฉ ุนุงู
2017ุ ูุชุจูุบ ุชูููุชู 500 ู
ููุงุฑ ุฏููุงุฑ."), |
| "labels": ["full name of a person", "geographical location", |
| "project or initiative name", "date or year"], |
| "language": "ar", |
| }, |
| "expected": {"ู
ุญู
ุฏ ุจู ุณูู
ุงู", "ุงูู
ู
ููุฉ ุงูุนุฑุจูุฉ ุงูุณุนูุฏูุฉ"}, |
| }, |
| |
| { |
| "id": "MIX-01", "group": "Mixed โ ๅๆจกๅๅๅนถ", |
| "description": "ไธญ่ฑๆททๅ ยท ่ๅบๅบๆฏ๏ผlanguage=mixed ๅผบๅถๅ่ท", |
| "payload": { |
| "text": "ๅผ ไผๅ ๅ
ฅไบ Google ๅไบฌ็ ๅไธญๅฟ๏ผ่ด่ดฃ Android ็ณป็ปไผๅใ" |
| "ไป็ๅไบ Sarah Chen ๆฅ่ช Meta๏ผไธคไบบๅ
ฑๅๅไธไบ 2024 ๅนด็ AI Summitใ", |
| "language": "mixed", |
| }, |
| "expected": {"ๅผ ไผ", "Google", "Sarah Chen", "Meta", "Android", "ๅไบฌ", "2024"}, |
| }, |
| { |
| "id": "MIX-02", "group": "Mixed โ ๅๆจกๅๅๅนถ", |
| "description": "ๅญฆๆฏๅบๆฏ๏ผlabels ็็ฉบ", |
| "payload": { |
| "text": "ๆธ
ๅๅคงๅญฆ่ฎก็ฎๆบ็ณปๆๆๆๆๅจ NeurIPS 2023 ๅ่กจไบๅ
ณไบ " |
| "Transformer ๆถๆ็่ฎบๆ๏ผๅไฝ่
ๆฅ่ช MIT ๅ Stanford Universityใ", |
| "language": "mixed", |
| }, |
| "expected": {"ๆๆ", "ๆธ
ๅๅคงๅญฆ", "MIT", "Stanford University", "Transformer"}, |
| }, |
| |
| { |
| "id": "AUTO-01", "group": "auto โ ่ชๅจ่ฏญ่จๆฃๆต", |
| "description": "็บฏไธญๆๆๆฌ๏ผๅบ่ขซๆฃๆตไธบ zh", |
| "payload": { |
| "text": "้ฉฌไบๅ็ซไบ้ฟ้ๅทดๅทด๏ผๆป้จๅจๆญๅทใ", |
| }, |
| "expected": {"้ฉฌไบ", "้ฟ้ๅทดๅทด", "ๆญๅท"}, |
| }, |
| { |
| "id": "AUTO-02", "group": "auto โ ่ชๅจ่ฏญ่จๆฃๆต", |
| "description": "็บฏ่ฑๆๆๆฌ๏ผๅบ่ขซๆฃๆตไธบ en", |
| "payload": { |
| "text": "Tim Cook is the CEO of Apple in Cupertino.", |
| }, |
| "expected": {"Tim Cook", "Apple", "Cupertino"}, |
| }, |
| { |
| "id": "AUTO-03", "group": "auto โ ่ชๅจ่ฏญ่จๆฃๆต", |
| "description": "ไธญ่ฑๆททๅ๏ผๅบ่ขซๆฃๆตไธบ mixed ๅนถๅ่ทๅๅนถ", |
| "payload": { |
| "text": "ๆๅๅจ Microsoft ๆ
ไปปๅทฅ็จๅธ๏ผๅธธ้ฉป Seattle ๅๅ
ฌๅฎคใ", |
| }, |
| "expected": {"ๆๅ", "Microsoft", "Seattle"}, |
| }, |
| |
| { |
| "id": "MIN-01", "group": "min_entities ่ฆ็ๅฏๅๅผ", |
| "description": "min_entities=10 ๅผบๅถๅ
ๅบ๏ผ็ญๆๆฌๅฏๅๅผๅชๆๆ 1 ไธช๏ผ", |
| "payload": { |
| "text": "้ฉฌไบ", |
| "language": "zh", |
| "min_entities": 10, |
| }, |
| "expected": {"้ฉฌไบ"}, |
| }, |
| { |
| "id": "MIN-02", "group": "min_entities ่ฆ็ๅฏๅๅผ", |
| "description": "min_entities=0 ๅ
ณ้ญๅ
ๅบ", |
| "payload": { |
| "text": "้ฉฌไบ", |
| "language": "zh", |
| "min_entities": 0, |
| }, |
| "expected": {"้ฉฌไบ"}, |
| }, |
| |
| { |
| "id": "THR-01", "group": "Threshold ๅๅ", |
| "description": "้ซ้ๅผ 0.8 - ๆๆ่ฟๅๆดๅฐไฝๆด้ซ็ฝฎไฟกๅบฆ็ๅฎไฝ", |
| "payload": { |
| "text": "Tesla and SpaceX are companies founded by Elon Musk.", |
| "language": "en", |
| "threshold": 0.8, |
| }, |
| "expected": {"Tesla", "SpaceX", "Elon Musk"}, |
| }, |
| |
| { |
| "id": "EDGE-01", "group": "Edge cases", |
| "description": "็ฉบๆๆฌ", |
| "payload": {"text": ""}, |
| "expected": set(), |
| }, |
| ] |
|
|
|
|
| |
|
|
| @dataclass |
| class CallResult: |
| case_id: str |
| status: int |
| elapsed_ms: float |
| entities: list[dict] = field(default_factory=list) |
| labels_used: list[str] = field(default_factory=list) |
| error: str | None = None |
|
|
|
|
| def post_extract(payload: dict, timeout: int = 60) -> CallResult: |
| body = json.dumps(payload).encode("utf-8") |
| req = urllib.request.Request( |
| EXTRACT, |
| data=body, |
| headers={"Content-Type": "application/json"}, |
| method="POST", |
| ) |
| t0 = time.perf_counter() |
| try: |
| with urllib.request.urlopen(req, timeout=timeout) as resp: |
| elapsed = (time.perf_counter() - t0) * 1000 |
| data = json.loads(resp.read().decode()) |
| return CallResult( |
| case_id="", |
| status=resp.status, |
| elapsed_ms=elapsed, |
| entities=data.get("entities", []), |
| labels_used=data.get("labels_used", []), |
| ) |
| except urllib.error.HTTPError as e: |
| elapsed = (time.perf_counter() - t0) * 1000 |
| return CallResult(case_id="", status=e.code, elapsed_ms=elapsed, |
| error=e.read().decode("utf-8", errors="replace")) |
| except Exception as e: |
| elapsed = (time.perf_counter() - t0) * 1000 |
| return CallResult(case_id="", status=0, elapsed_ms=elapsed, error=str(e)) |
|
|
|
|
| |
|
|
| def check_health() -> tuple[bool, float, str]: |
| t0 = time.perf_counter() |
| try: |
| with urllib.request.urlopen(HEALTH, timeout=30) as resp: |
| elapsed = (time.perf_counter() - t0) * 1000 |
| return resp.status == 200, elapsed, resp.read().decode() |
| except Exception as e: |
| return False, (time.perf_counter() - t0) * 1000, str(e) |
|
|
|
|
| |
|
|
| def write_report(results: list[tuple[dict, CallResult]], health: tuple[bool, float, str]): |
| buf = io.StringIO() |
| w = buf.write |
|
|
| w("# ่ฟ็ซฏ API ๆต่ฏๆฅๅ\n\n") |
| w(f"- ๆๅกๅฐๅ๏ผ`{BASE_URL}`\n") |
| w(f"- ๆต่ฏๆถ้ด๏ผ{time.strftime('%Y-%m-%d %H:%M:%S')}\n") |
| ok, hms, hbody = health |
| w(f"- ๅฅๅบทๆฃๆฅ๏ผ{'โ OK' if ok else 'โ FAIL'} ({hms:.0f}ms) โ {hbody}\n") |
| w(f"- ็จไพๆปๆฐ๏ผ{len(results)}\n\n") |
|
|
| |
| w("## ไธใๆฑๆป\n\n") |
| w("| ็จไพ | ๆ่ฟฐ | HTTP | ๅฎไฝๆฐ | ๅฌๅ | ่ๆถ |\n") |
| w("|---|---|---|---|---|---|\n") |
| total_ms = 0.0 |
| pass_n = 0 |
| for case, res in results: |
| expected = case.get("expected", set()) |
| found = {e["text"] for e in res.entities} |
| hit = len(expected & found) |
| recall = f"{hit}/{len(expected)}" if expected else "โ" |
| ok_mark = "โ" if res.status == 200 else "โ" |
| w(f"| **{case['id']}** | {case['description']} | {ok_mark} {res.status} | " |
| f"{len(res.entities)} | {recall} | {res.elapsed_ms:.0f}ms |\n") |
| if res.status == 200: |
| pass_n += 1 |
| total_ms += res.elapsed_ms |
| w(f"\n- ้่ฟ็๏ผ**{pass_n}/{len(results)}**\n") |
| w(f"- ็ดฏ่ฎก่ๆถ๏ผ**{total_ms:.0f}ms**๏ผๅนณๅ {total_ms/len(results):.0f}ms/่ฏทๆฑ๏ผ\n\n") |
|
|
| |
| groups: dict[str, list] = {} |
| for case, res in results: |
| groups.setdefault(case["group"], []).append((case, res)) |
|
|
| w("## ไบใๅ็ป่ฏฆ็ป็ปๆ\n\n") |
| for group_name, items in groups.items(): |
| w(f"### {group_name}\n\n") |
| for case, res in items: |
| w(f"#### {case['id']} ยท {case['description']}\n\n") |
| w("**่ฏทๆฑ**\n```json\n") |
| w(json.dumps(case["payload"], ensure_ascii=False, indent=2)) |
| w("\n```\n\n") |
|
|
| w(f"**ๅๅบ**๏ผHTTP {res.status} ยท {res.elapsed_ms:.0f}ms ยท " |
| f"{len(res.entities)} ไธชๅฎไฝ\n\n") |
|
|
| if res.error: |
| w(f"```\nERROR: {res.error}\n```\n\n") |
| continue |
|
|
| if res.entities: |
| w("| ๆๆฌ | ๆ ็ญพ | ็ฝฎไฟกๅบฆ | ่ตทๆญข |\n|---|---|---|---|\n") |
| for e in res.entities: |
| w(f"| `{e['text']}` | {e['label']} | {e['score']:.2f} | " |
| f"{e['start']}โ{e['end']} |\n") |
| else: |
| w("_ๆช่ฏๅซๅฐๅฎไฝ_\n") |
|
|
| expected = case.get("expected", set()) |
| if expected: |
| found = {e["text"] for e in res.entities} |
| hits = expected & found |
| misses = expected - found |
| w(f"\n**ๆๆๅฝไธญ** {len(hits)}/{len(expected)}๏ผ") |
| w(", ".join(f"`{x}`" for x in expected) + " \n") |
| if misses: |
| w(f"**ๆชๅฝไธญ**๏ผ{', '.join(f'`{x}`' for x in misses)} \n") |
|
|
| mnc = case.get("must_not_contain", set()) |
| if mnc: |
| bad = {e["text"] for e in res.entities} & mnc |
| if bad: |
| w(f"\n> โ ๏ธ **่พน็้่ฏฏ**๏ผ{bad}\n") |
| else: |
| w(f"\n> โ ่พน็ๆญฃ็กฎ๏ผๆชๅบ็ฐ {mnc}๏ผ\n") |
| w("\n") |
|
|
| |
| w("## ไธใๆ่ทฏ็ฑๅ็ปๆง่ฝ\n\n") |
| by_group: dict[str, list[float]] = {} |
| for case, res in results: |
| if res.status == 200: |
| by_group.setdefault(case["group"], []).append(res.elapsed_ms) |
| w("| ๅ็ป | ็จไพๆฐ | ๆๅฟซ | ๆๆ
ข | ๅนณๅ |\n|---|---|---|---|---|\n") |
| for g, times in by_group.items(): |
| w(f"| {g} | {len(times)} | {min(times):.0f}ms | " |
| f"{max(times):.0f}ms | {sum(times)/len(times):.0f}ms |\n") |
|
|
| REPORT.parent.mkdir(parents=True, exist_ok=True) |
| REPORT.write_text(buf.getvalue(), encoding="utf-8") |
| print(f"\nReport: {REPORT.resolve()}") |
|
|
|
|
| |
|
|
| def main(): |
| print(f"Target: {BASE_URL}") |
| health = check_health() |
| print(f"Health: {'OK' if health[0] else 'FAIL'} ({health[1]:.0f}ms)") |
| if not health[0]: |
| print(f" -> {health[2]}") |
| return |
|
|
| results: list[tuple[dict, CallResult]] = [] |
| for case in CASES: |
| print(f" {case['id']:8s} ", end="", flush=True) |
| res = post_extract(case["payload"]) |
| res.case_id = case["id"] |
| results.append((case, res)) |
| status = "OK" if res.status == 200 else f"FAIL({res.status})" |
| print(f"{status:8s} {res.elapsed_ms:6.0f}ms {len(res.entities)} entities") |
|
|
| write_report(results, health) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|