File size: 4,615 Bytes
1cca871
1fce961
 
 
1cca871
11d1836
d420fe2
1fce961
1cca871
1fce961
 
 
 
 
 
 
 
1cca871
 
d420fe2
3a03e86
1fce961
 
 
 
 
 
 
 
 
 
 
3a03e86
 
 
1fce961
3a03e86
1fce961
 
9a16674
3a03e86
9a16674
 
3a03e86
9a16674
 
3a03e86
 
9a16674
3a03e86
 
9a16674
3a03e86
 
 
9a16674
 
 
3a03e86
55012e5
9a16674
55012e5
 
 
 
 
 
 
 
 
3a03e86
 
55012e5
 
 
 
 
 
1cca871
55012e5
d420fe2
1cca871
 
 
 
 
 
55012e5
 
1cca871
d420fe2
 
11d1836
55012e5
11d1836
 
d420fe2
 
 
 
3a03e86
 
 
d420fe2
 
 
1cca871
 
d420fe2
 
 
1cca871
d420fe2
1cca871
d420fe2
 
1cca871
afd5b3c
d420fe2
 
 
3a03e86
 
d420fe2
3a03e86
d420fe2
 
3a03e86
d420fe2
 
3a03e86
 
d420fe2
 
 
 
1cca871
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import spaces
import os
import gradio as gr
from transformers import pipeline
from fastapi import HTTPException
from pydantic import BaseModel, ConfigDict
from typing import List

# ── Model ──────────────────────────────────────────────────────────────────────
ner = pipeline(
    "ner",
    model="Ramosh99/mixcode_pii_detector",
    aggregation_strategy="average",
    token=os.getenv("HF_TOKEN"),
)


# ── Core inference (ZeroGPU β€” GPU used only during inference call) ──────────────
@spaces.GPU
def detect_pii(text: str):
    """Run NER and return structured PII results (used by REST API)."""
    if not text.strip():
        return {"text": text, "entities": []}

    raw = ner(text)
    entities = []
    for item in raw:
        label = item.get("entity_group", "")
        if label.startswith(("B-", "I-")):
            label = label[2:]
        entities.append({
            "entity": label,
            "start":  item["start"],
            "end":    item["end"],
            "score":  round(float(item["score"]), 4),
        })
    return {"text": text, "entities": entities}


def detect_pii_ui(text: str):
    """Return list-of-tuples for Gradio HighlightedText (used by UI)."""
    res = detect_pii(text)
    entities = sorted(res["entities"], key=lambda x: x["start"])

    if not entities:
        return [(text, None)]

    result, last = [], 0
    for ent in entities:
        if ent["start"] > last:
            result.append((text[last:ent["start"]], None))
        result.append((text[ent["start"]:ent["end"]], ent["entity"]))
        last = ent["end"]
    if last < len(text):
        result.append((text[last:], None))
    return result


# ── Gradio Interface ───────────────────────────────────────────────────────────
demo = gr.Interface(
    fn=detect_pii_ui,
    inputs=gr.Textbox(
        label="Input Text",
        placeholder="Enter Sinhala-English codemixed text...",
        lines=4,
    ),
    outputs=gr.HighlightedText(label="Detected PII Entities"),
    title="Sinhala-English Codemix PII Detector",
    description=(
        "Detects PII entities (PERSON, ORG, PHONE, EMAIL, NIC, etc.) in "
        "Sinhala-English code-mixed text.\n\n"
        "**REST API:** `POST /api/detect` Β· `GET /api/health` Β· Docs: `/docs`"
    ),
    examples=[
        ["Mage name Kamal, man wada karanne MAS Holdings eke, phone 071-1234567"],
        ["ࢸ࢜ේ email address kamal.perera@gmail.com, NIC number 199912345678"],
        ["I live in Colombo and my phone number is 0771234567"],
    ],
    flagging_mode="never",
)

# ── Launch Gradio β€” properly registers @spaces.GPU with ZeroGPU ───────────────
gr_app, _local_url, _share_url = demo.launch(
    server_name="0.0.0.0",
    server_port=7860,
    prevent_thread_lock=True,
    show_api=False,
)

# ── REST API routes (added to Gradio's internal FastAPI app) ───────────────────

class DetectRequest(BaseModel):
    model_config = ConfigDict(
        json_schema_extra={"example": {"text": "Mage name Kamal, phone 071-1234567"}}
    )
    text: str


class Entity(BaseModel):
    entity: str
    start:  int
    end:    int
    score:  float


class DetectResponse(BaseModel):
    text:          str
    entities:      List[Entity]
    redacted_text: str


@gr_app.get("/api/health")
def health_check():
    return {"status": "ok", "model": "Ramosh99/mixcode_pii_detector"}


@gr_app.post("/api/detect", response_model=DetectResponse)
async def api_detect_pii(request: DetectRequest):
    if not request.text.strip():
        raise HTTPException(status_code=422, detail="Input text cannot be empty.")

    result   = detect_pii(request.text)
    text     = result["text"]
    entities = sorted(result["entities"], key=lambda e: e["start"], reverse=True)

    redacted = text
    for ent in entities:
        redacted = redacted[: ent["start"]] + f"[{ent['entity']}]" + redacted[ent["end"]:]

    return {
        "text":          result["text"],
        "entities":      result["entities"],
        "redacted_text": redacted,
    }


# ── Keep process alive ─────────────────────────────────────────────────────────
demo.block_thread()