ohohOD commited on
Commit
e833ff7
·
1 Parent(s): 0a16bf6
Files changed (2) hide show
  1. README.md +20 -8
  2. app.py +65 -28
README.md CHANGED
@@ -1,14 +1,26 @@
1
  ---
2
- title: Wl Ko Ner
3
- emoji: 🏆
4
- colorFrom: blue
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.15.2
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
- short_description: WygLore NER Service
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: WygLore Leaf Korean NER
 
 
 
3
  sdk: gradio
 
 
4
  app_file: app.py
5
  pinned: false
 
6
  ---
7
 
8
+ # WygLore Leaf — Korean NER endpoint
9
+
10
+ `wl-ko-ner-v2` (RP 도메인 적응) ONNX 를 CPU 로 서빙. 한국어 텍스트 -> 엔티티 스팬 (PS / LC / OG / DT / TI / QT).
11
+
12
+ - **용도**: WygLore Leaf 플러그인의 *모바일 저메모리 폴백* — on-device 로 모델(215-430MB)을 못 올리는 기기가 이 엔드포인트에 *NER 추론만* 위임. canon-앵커 / POS 필터 / assemble 은 플러그인 JS 가 그대로 수행.
13
+ - **모델**: [m6dd8m/wl-ko-ner-v2](https://huggingface.co/m6dd8m/wl-ko-ner-v2) (CC-BY-SA-4.0). 토크나이저는 base `monologg/koelectra-base-v3-discriminator`.
14
+ - **출력 모양**: on-device `ner.js` 와 동일 — `[{text, type, score, start, end}, ...]`.
15
+ - **API** (FastAPI, 한 방 JSON):
16
+ - `POST /extract` — `{"text":"..."}` -> `{"entities":[{text,type,score,start,end}, ...]}`
17
+ - `POST /extract_batch` — `{"texts":[...]}` -> `{"results":[[...], ...]}` (콜드스타트 배치, max 512)
18
+ - `GET /health` -> `{ok, model, onnx}` · `GET /docs` -> 인터랙티브 Swagger UI
19
+ - UI(테스트용)는 `/` 그대로. 플러그인은 `/extract` 를 `host.nativeFetch` 로 호출. ※ fastapi/uvicorn 은 gradio 의존성이라 requirements 불요.
20
+
21
+ ## 한계 / TODO (프로덕션 전)
22
+
23
+ - 무료 CPU Space: 48h *무트래픽* 시 sleep -> 첫 요청 ~30-60s wake (일일 트래픽 있으면 계속 깨어있음).
24
+ - abuse 보호 없음 (public URL) — rate-limit / 토큰은 FastAPI 이주 시 추가.
25
+ - 프라이버시: 텍스트가 HF 인프라 경유 (클라우드-LLM 유저 무방 / 로컬 Ollama 의 완전-로컬은 깨짐 / NER=태깅이라 생성 ToS 리스크 낮음).
26
+ - 첫 부팅 로그(`[boot] ...`)에서 (a) 어떤 onnx 가 선택됐는지, (b) `id2label` 이 `B-PS..` 인지(=정상) `LABEL_0..` 인지(=라벨맵 필요) 확인.
app.py CHANGED
@@ -1,11 +1,15 @@
1
- # WygLore Leaf — Korean NER inference endpoint (HF Space · Gradio)
2
  #
3
- # wl-ko-ner-v2 (RP 도메인 적응 ONNX) 를 CPU 로 서빙해 text -> 엔티티 스팬을 내준다.
4
- # 용도: 플러그인의 *모바일 저메모리 폴백*. on-device 로 모델(215-430MB)을 못 올리는
5
- # 기기가 이 엔드포인트로 *NER 추론만* 위임. canon-앵커 / POS 필터 / assemble 은
6
- # 플러그인 JS 가 그대로 수행 (서버는 무거운 모델 추론만 = 얇은 엔드포인트).
 
7
 
8
  import gradio as gr
 
 
 
9
  from huggingface_hub import list_repo_files
10
  from optimum.onnxruntime import ORTModelForTokenClassification
11
  from transformers import AutoTokenizer, pipeline
@@ -13,46 +17,79 @@ from transformers import AutoTokenizer, pipeline
13
  MODEL_REPO = "m6dd8m/wl-ko-ner-v2" # ONNX 가중치 (operator 공개)
14
  TOK_REPO = "monologg/koelectra-base-v3-discriminator" # 토크나이저 (파인튜닝해도 동일)
15
 
16
- # 1) repo 안의 .onnx 파일명 자동 탐지 — fp32 우선.
17
- # (서버는 CPU 라 fp16 shader 불요, fp32 가 가장 호환 안전. repo 구조 몰라도 OK.)
18
  onnx_files = [f for f in list_repo_files(MODEL_REPO) if f.endswith(".onnx")]
19
  if not onnx_files:
20
- raise RuntimeError(f"{MODEL_REPO} 에 .onnx 가 없음 — repo 내용을 확인하세요")
21
  onnx_path = next((f for f in onnx_files if "fp32" in f.lower()), onnx_files[0])
22
  subfolder, onnx_name = (onnx_path.rsplit("/", 1) if "/" in onnx_path else ("", onnx_path))
23
- print(f"[boot] onnx 후보 {onnx_files} -> 선택 '{onnx_path}'")
24
 
25
- # 2) 모델(=repo, NER 라벨 config 포함) + 토크나이저(=base) + pipeline.
26
  model = ORTModelForTokenClassification.from_pretrained(
27
  MODEL_REPO, file_name=onnx_name, subfolder=subfolder)
28
  tok = AutoTokenizer.from_pretrained(TOK_REPO)
29
- print(f"[boot] id2label = {model.config.id2label}") # B-PS.. 면 정상 / LABEL_0.. 면 라벨맵 필요
30
  ner = pipeline("token-classification", model=model, tokenizer=tok,
31
- aggregation_strategy="simple") # 서브워드 -> 엔티티 스팬으로 묶음
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- # 3) 출력 모양을 on-device ner.js 와 *동일하게* (type/text/score/start/end).
34
- # 플러그인이 "로컬이든 서버든 같은 모양" 으로 받아 라우팅 단순.
35
  def extract(text):
36
  if not text or not text.strip():
37
  return []
38
- return [
39
- {
40
- "text": e["word"],
41
- "type": e["entity_group"], # PS / LC / OG / DT / TI / QT
42
- "score": float(e["score"]), # numpy float -> JSON 안전 캐스팅
43
- "start": int(e["start"]),
44
- "end": int(e["end"]),
45
- }
46
- for e in ner(text)
47
- ]
48
 
49
- # 4) Gradio = 화면(손 테스트용) + API(/run/predict 류) 둘 다 자동 제공.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  demo = gr.Interface(
51
  fn=extract,
52
  inputs=gr.Textbox(lines=4, label="한국어 텍스트",
53
- placeholder="새봄이 USB 를 들고 감찰국으로 향했다."),
54
  outputs=gr.JSON(label="엔티티 스팬"),
55
  title="WygLore Leaf — Korean NER (wl-ko-ner-v2)",
56
- description="RP 도메인 적응 NER. text -> 엔티티 스팬. 모바일 저메모리 폴백용 엔드포인트.",
57
  )
58
- demo.queue(max_size=32).launch() # queue = 동시 요청 폭주 완충
 
 
 
 
 
1
+ # WygLore Leaf — Korean NER endpoint (HF Space)
2
  #
3
+ # Gradio 테스트 UI (/) + 깔끔한 FastAPI JSON API (/extract · /extract_batch · /health).
4
+ # 플러그인은 /extract 를 host.nativeFetch 로 *한 방* 호출 (모바일 저메모리 폴백).
5
+ # 무거운 모델 추론만 여기서; canon-앵커 / POS 필터 / assemble 은 플러그인 JS 가 그대로.
6
+ #
7
+ # ※ fastapi/uvicorn/pydantic 은 gradio 의존성으로 이미 설치됨 -> requirements 불요.
8
 
9
  import gradio as gr
10
+ from fastapi import FastAPI, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from pydantic import BaseModel
13
  from huggingface_hub import list_repo_files
14
  from optimum.onnxruntime import ORTModelForTokenClassification
15
  from transformers import AutoTokenizer, pipeline
 
17
  MODEL_REPO = "m6dd8m/wl-ko-ner-v2" # ONNX 가중치 (operator 공개)
18
  TOK_REPO = "monologg/koelectra-base-v3-discriminator" # 토크나이저 (파인튜닝해도 동일)
19
 
20
+ # 1) repo 안의 .onnx 자동 탐지 — fp32 우선 (서버 CPU 라 fp16 shader 불요).
 
21
  onnx_files = [f for f in list_repo_files(MODEL_REPO) if f.endswith(".onnx")]
22
  if not onnx_files:
23
+ raise RuntimeError(f"{MODEL_REPO} 에 .onnx 가 없음 — repo 내용 확인")
24
  onnx_path = next((f for f in onnx_files if "fp32" in f.lower()), onnx_files[0])
25
  subfolder, onnx_name = (onnx_path.rsplit("/", 1) if "/" in onnx_path else ("", onnx_path))
26
+ print(f"[boot] onnx -> '{onnx_path}'")
27
 
28
+ # 2) 모델 + 토크나이저 + pipeline.
29
  model = ORTModelForTokenClassification.from_pretrained(
30
  MODEL_REPO, file_name=onnx_name, subfolder=subfolder)
31
  tok = AutoTokenizer.from_pretrained(TOK_REPO)
32
+ print(f"[boot] id2label = {model.config.id2label}")
33
  ner = pipeline("token-classification", model=model, tokenizer=tok,
34
+ aggregation_strategy="simple")
35
+
36
+ # 3) pipeline 출력 -> on-device ner.js 와 동일 모양 (type/text/score/start/end).
37
+ def _spans(entities):
38
+ return [
39
+ {"text": e["word"],
40
+ "type": e["entity_group"], # PS / LC / OG / DT / TI / QT
41
+ "score": float(e["score"]),
42
+ "start": int(e["start"]),
43
+ "end": int(e["end"])}
44
+ for e in entities
45
+ ]
46
 
 
 
47
  def extract(text):
48
  if not text or not text.strip():
49
  return []
50
+ return _spans(ner(text))
51
+
52
+ # ───────────────────────── FastAPI (깔끔한 JSON API) ─────────────────────────
53
+ app = FastAPI(title="WygLore Leaf Korean NER")
54
+ app.add_middleware(CORSMiddleware, allow_origins=["*"],
55
+ allow_methods=["*"], allow_headers=["*"])
 
 
 
 
56
 
57
+ class OneReq(BaseModel):
58
+ text: str
59
+
60
+ class BatchReq(BaseModel):
61
+ texts: list[str]
62
+
63
+ @app.get("/health")
64
+ def health():
65
+ return {"ok": True, "model": MODEL_REPO, "onnx": onnx_path}
66
+
67
+ @app.post("/extract") # 한 방: {"text": "..."} -> {"entities": [...]}
68
+ def extract_api(req: OneReq):
69
+ return {"entities": extract(req.text)}
70
+
71
+ @app.post("/extract_batch") # 배치(콜드스타트 효율): {"texts": [...]} -> {"results": [[...], ...]}
72
+ def extract_batch_api(req: BatchReq):
73
+ if len(req.texts) > 512: # 가벼운 abuse/메모리 가드
74
+ raise HTTPException(400, "batch too large (max 512)")
75
+ idx = [i for i, t in enumerate(req.texts) if t and t.strip()]
76
+ outs = ner([req.texts[i] for i in idx]) if idx else []
77
+ results = [[] for _ in req.texts]
78
+ for k, i in enumerate(idx):
79
+ results[i] = _spans(outs[k])
80
+ return {"results": results}
81
+
82
+ # ───────────────────────── Gradio 테스트 UI 를 / 에 마운트 ─────────────────────────
83
  demo = gr.Interface(
84
  fn=extract,
85
  inputs=gr.Textbox(lines=4, label="한국어 텍스트",
86
+ placeholder="새봄이 여의도에서 안도현을 만났다."),
87
  outputs=gr.JSON(label="엔티티 스팬"),
88
  title="WygLore Leaf — Korean NER (wl-ko-ner-v2)",
89
+ description="UI=테스트용 · API=POST /extract · 배치=/extract_batch · 문서=/docs",
90
  )
91
+ app = gr.mount_gradio_app(app, demo, path="/") # API 라우트가 먼저 등록돼 우선 매칭
92
+
93
+ if __name__ == "__main__":
94
+ import uvicorn
95
+ uvicorn.run(app, host="0.0.0.0", port=7860) # HF Space 기대 포트