Spaces:
Running
Running
| """ | |
| Server HTTP minimal untuk NER test — berjalan di http://127.0.0.1:8001 | |
| Endpoint: | |
| GET /api/status — health-check, kembalikan status model | |
| POST /api/ner — deteksi entitas dari {"text": "..."} | |
| Jalankan: | |
| python src/ner_server.py | |
| python src/ner_server.py --port 8001 --model cahya/xlm-roberta-base-indonesian-NER | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import sys | |
| import threading | |
| from http.server import BaseHTTPRequestHandler, HTTPServer | |
| from pathlib import Path | |
| # Pastikan src/ ada di sys.path agar ner_detector bisa diimpor langsung | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from core.language import detect_language | |
| from ner_detector import IndonesianNER | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s %(levelname)-7s %(message)s", | |
| datefmt="%H:%M:%S", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # State global | |
| _ner: IndonesianNER | None = None | |
| _model_loading = threading.Event() # di-set saat load selesai (berhasil atau gagal) | |
| _load_success = False | |
| def _load_model_background(model_name: str | None) -> None: | |
| """Muat model di thread terpisah agar server bisa langsung merespons /api/status.""" | |
| global _ner, _load_success | |
| _ner = IndonesianNER(model_name=model_name) | |
| _load_success = _ner.load() | |
| if not _load_success: | |
| logger.error("Gagal memuat model: %s", _ner.load_error) | |
| _model_loading.set() | |
| # Request handler | |
| class NERHandler(BaseHTTPRequestHandler): | |
| # CORS: izinkan semua origin agar bisa dipanggil dari file:// | |
| def _send_cors_headers(self) -> None: | |
| self.send_header("Access-Control-Allow-Origin", "*") | |
| self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") | |
| self.send_header("Access-Control-Allow-Headers", "Content-Type") | |
| def do_OPTIONS(self) -> None: # preflight | |
| self.send_response(204) | |
| self._send_cors_headers() | |
| self.end_headers() | |
| # Helpers | |
| def _json_response(self, status: int, body: object) -> None: | |
| payload = json.dumps(body, ensure_ascii=False).encode() | |
| self.send_response(status) | |
| self.send_header("Content-Type", "application/json; charset=utf-8") | |
| self.send_header("Content-Length", str(len(payload))) | |
| self._send_cors_headers() | |
| self.end_headers() | |
| self.wfile.write(payload) | |
| def _read_json_body(self) -> dict | None: | |
| length = int(self.headers.get("Content-Length", 0)) | |
| if length == 0: | |
| return {} | |
| try: | |
| return json.loads(self.rfile.read(length)) | |
| except (json.JSONDecodeError, ValueError): | |
| return None | |
| def log_message(self, fmt: str, *args) -> None: | |
| logger.info("%-6s %s", args[0] if args else "", args[1] if len(args) > 1 else "") | |
| # GET | |
| def do_GET(self) -> None: | |
| if self.path == "/api/status": | |
| loaded = _load_success and _ner is not None and _ner.is_loaded | |
| self._json_response(200, { | |
| "model_loaded": loaded, | |
| "model": _ner.loaded_model if (loaded and _ner) else None, | |
| "loading": not _model_loading.is_set(), | |
| }) | |
| else: | |
| self._json_response(404, {"error": "Not found"}) | |
| # POST | |
| def do_POST(self) -> None: | |
| if self.path != "/api/ner": | |
| self._json_response(404, {"error": "Not found"}) | |
| return | |
| body = self._read_json_body() | |
| if body is None: | |
| self._json_response(400, {"error": "Body bukan JSON yang valid."}) | |
| return | |
| text = str(body.get("text", "")).strip() | |
| if not text: | |
| self._json_response(400, {"error": "Field 'text' kosong atau tidak ada."}) | |
| return | |
| if not (_ner and _ner.is_loaded): | |
| self._json_response(503, { | |
| "error": "Model belum selesai dimuat. Coba lagi dalam beberapa detik.", | |
| "loading": not _model_loading.is_set(), | |
| }) | |
| return | |
| language = str(body.get("language") or detect_language(text).language) | |
| entities = _ner.predict(text, language=language) | |
| self._json_response(200, { | |
| "text": text, | |
| "entities": [ | |
| { | |
| "word": e.word, | |
| "label": e.label, | |
| "score": round(e.score, 4), | |
| "start": e.start, | |
| "end": e.end, | |
| "source": e.source, | |
| } | |
| for e in entities | |
| ], | |
| "model": _ner.loaded_model, | |
| }) | |
| # Entrypoint | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Server NER Bahasa Indonesia") | |
| parser.add_argument("--port", type=int, default=8001) | |
| parser.add_argument("--host", default="127.0.0.1") | |
| parser.add_argument("--model", default=None, | |
| help="ID model HuggingFace atau path lokal. " | |
| "Default: cascade otomatis (xlm-roberta-large → base → bert).") | |
| args = parser.parse_args() | |
| # Muat model di background agar server langsung bisa dijangkau | |
| logger.info("Memuat model NER di background…") | |
| t = threading.Thread(target=_load_model_background, args=(args.model,), daemon=True) | |
| t.start() | |
| server = HTTPServer((args.host, args.port), NERHandler) | |
| logger.info("Server berjalan di http://%s:%d", args.host, args.port) | |
| logger.info(" GET http://%s:%d/api/status", args.host, args.port) | |
| logger.info(" POST http://%s:%d/api/ner", args.host, args.port) | |
| logger.info("Buka web/ner-test.html di browser untuk memulai pengujian.") | |
| logger.info("Tekan Ctrl+C untuk menghentikan server.") | |
| try: | |
| server.serve_forever() | |
| except KeyboardInterrupt: | |
| logger.info("Server dihentikan.") | |
| server.server_close() | |
| if __name__ == "__main__": | |
| main() | |