wl-ko-ner / app.py
ohohOD
gogo
e833ff7
Raw History Blame
4.31 kB
# WygLore Leaf β€” Korean NER endpoint (HF Space)
#
# Gradio ν…ŒμŠ€νŠΈ UI (/) + κΉ”λ”ν•œ FastAPI JSON API (/extract Β· /extract_batch Β· /health).
# ν”ŒλŸ¬κ·ΈμΈμ€ /extract λ₯Ό host.nativeFetch 둜 *ν•œ λ°©* 호좜 (λͺ¨λ°”일 μ €λ©”λͺ¨λ¦¬ 폴백).
# 무거운 λͺ¨λΈ μΆ”λ‘ λ§Œ μ—¬κΈ°μ„œ; canon-액컀 / POS ν•„ν„° / assemble 은 ν”ŒλŸ¬κ·ΈμΈ JS κ°€ κ·ΈλŒ€λ‘œ.
#
# β€» fastapi/uvicorn/pydantic 은 gradio μ˜μ‘΄μ„±μœΌλ‘œ 이미 μ„€μΉ˜λ¨ -> requirements λΆˆμš”.
import gradio as gr
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from huggingface_hub import list_repo_files
from optimum.onnxruntime import ORTModelForTokenClassification
from transformers import AutoTokenizer, pipeline
MODEL_REPO = "m6dd8m/wl-ko-ner-v2" # ONNX κ°€μ€‘μΉ˜ (operator 곡개)
TOK_REPO = "monologg/koelectra-base-v3-discriminator" # ν† ν¬λ‚˜μ΄μ € (νŒŒμΈνŠœλ‹ν•΄λ„ 동일)
# 1) repo μ•ˆμ˜ .onnx μžλ™ 탐지 β€” fp32 μš°μ„  (μ„œλ²„ CPU 라 fp16 shader λΆˆμš”).
onnx_files = [f for f in list_repo_files(MODEL_REPO) if f.endswith(".onnx")]
if not onnx_files:
raise RuntimeError(f"{MODEL_REPO} 에 .onnx κ°€ μ—†μŒ β€” repo λ‚΄μš© 확인")
onnx_path = next((f for f in onnx_files if "fp32" in f.lower()), onnx_files[0])
subfolder, onnx_name = (onnx_path.rsplit("/", 1) if "/" in onnx_path else ("", onnx_path))
print(f"[boot] onnx -> '{onnx_path}'")
# 2) λͺ¨λΈ + ν† ν¬λ‚˜μ΄μ € + pipeline.
model = ORTModelForTokenClassification.from_pretrained(
MODEL_REPO, file_name=onnx_name, subfolder=subfolder)
tok = AutoTokenizer.from_pretrained(TOK_REPO)
print(f"[boot] id2label = {model.config.id2label}")
ner = pipeline("token-classification", model=model, tokenizer=tok,
aggregation_strategy="simple")
# 3) pipeline 좜λ ₯ -> on-device ner.js 와 동일 λͺ¨μ–‘ (type/text/score/start/end).
def _spans(entities):
return [
{"text": e["word"],
"type": e["entity_group"], # PS / LC / OG / DT / TI / QT
"score": float(e["score"]),
"start": int(e["start"]),
"end": int(e["end"])}
for e in entities
]
def extract(text):
if not text or not text.strip():
return []
return _spans(ner(text))
# ───────────────────────── FastAPI (κΉ”λ”ν•œ JSON API) ─────────────────────────
app = FastAPI(title="WygLore Leaf Korean NER")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"])
class OneReq(BaseModel):
text: str
class BatchReq(BaseModel):
texts: list[str]
@app.get("/health")
def health():
return {"ok": True, "model": MODEL_REPO, "onnx": onnx_path}
@app.post("/extract") # ν•œ λ°©: {"text": "..."} -> {"entities": [...]}
def extract_api(req: OneReq):
return {"entities": extract(req.text)}
@app.post("/extract_batch") # 배치(μ½œλ“œμŠ€νƒ€νŠΈ 효율): {"texts": [...]} -> {"results": [[...], ...]}
def extract_batch_api(req: BatchReq):
if len(req.texts) > 512: # κ°€λ²Όμš΄ abuse/λ©”λͺ¨λ¦¬ κ°€λ“œ
raise HTTPException(400, "batch too large (max 512)")
idx = [i for i, t in enumerate(req.texts) if t and t.strip()]
outs = ner([req.texts[i] for i in idx]) if idx else []
results = [[] for _ in req.texts]
for k, i in enumerate(idx):
results[i] = _spans(outs[k])
return {"results": results}
# ───────────────────────── Gradio ν…ŒμŠ€νŠΈ UI λ₯Ό / 에 마운트 ─────────────────────────
demo = gr.Interface(
fn=extract,
inputs=gr.Textbox(lines=4, label="ν•œκ΅­μ–΄ ν…μŠ€νŠΈ",
placeholder="μƒˆλ΄„μ΄ μ—¬μ˜λ„μ—μ„œ μ•ˆλ„ν˜„μ„ λ§Œλ‚¬λ‹€."),
outputs=gr.JSON(label="μ—”ν‹°ν‹° 슀팬"),
title="WygLore Leaf β€” Korean NER (wl-ko-ner-v2)",
description="UI=ν…ŒμŠ€νŠΈμš© Β· API=POST /extract Β· 배치=/extract_batch Β· λ¬Έμ„œ=/docs",
)
app = gr.mount_gradio_app(app, demo, path="/") # API λΌμš°νŠΈκ°€ λ¨Όμ € 등둝돼 μš°μ„  λ§€μΉ­
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860) # HF Space κΈ°λŒ€ 포트