File size: 10,874 Bytes
134375c
 
 
 
 
 
 
f9fbcba
134375c
e672d7b
134375c
 
 
 
 
 
 
b401778
 
 
134375c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78101a7
 
 
 
 
 
 
 
 
 
 
f9fbcba
78101a7
f9fbcba
a0af152
 
 
 
 
 
 
 
 
f9fbcba
78101a7
 
b401778
134375c
 
f9fbcba
78101a7
f9fbcba
78101a7
f9fbcba
134375c
b401778
f9fbcba
 
 
 
 
134375c
 
f9fbcba
134375c
 
e672d7b
134375c
 
 
 
 
 
 
 
 
f0fdf2a
e672d7b
 
 
 
 
 
 
 
134375c
 
e672d7b
134375c
 
 
e672d7b
134375c
e672d7b
 
 
 
134375c
 
 
 
 
 
 
 
 
 
 
 
e672d7b
134375c
 
 
 
 
 
 
 
 
 
 
 
e672d7b
134375c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e94c90
134375c
 
 
 
4e94c90
 
 
b3fa1e9
4e94c90
 
b3fa1e9
4e94c90
b3fa1e9
 
 
4e94c90
 
 
 
 
 
134375c
 
a35b73b
 
4e94c90
 
 
 
 
 
 
 
134375c
 
4e94c90
 
 
 
 
134375c
 
 
 
4e94c90
 
134375c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
"""Türkçe PII Guard — Hugging Face Space demosu.

Model uçtan uca çalışır: metni alır, maskelenmiş metni döndürür.
Şema dışı etiket üretebildiği için çıktı bir whitelist katmanından geçirilir
ve kullanıcıya ayrıca gösterilir — ham model davranışı gizlenmez.
"""

import os
import re
import time

import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL_ADI = "melikegks/turkish-pii-guard-0.8b"

# Konteynerde stdout tamponlanır; loglar anında görünsün diye kapatıyoruz.
os.environ.setdefault("PYTHONUNBUFFERED", "1")

GECERLI_ETIKETLER = {
    "[AD]", "[TCKN]", "[DOGUM_TARIHI]", "[DOGUM_YERI]", "[ANNE_ADI]",
    "[ANNE_KIZLIK]", "[BABA_ADI]", "[PASAPORT_NO]", "[EHLIYET_NO]", "[SGK_NO]",
    "[IMZA]", "[IBAN]", "[HESAP_NO]", "[KART]", "[KART_SKT]", "[CVV]",
    "[MAAS]", "[VERGI_NO]", "[MUSTERI_NO]", "[KREDI_NOTU]", "[POLICE_NO]",
    "[SOZLESME_NO]", "[KRIPTO_CUZDAN]", "[TEL]", "[EMAIL]", "[ADRES]",
    "[KONUM]", "[SAGLIK]", "[DIN]", "[ETNIK_KOKEN]", "[SENDIKA]",
    "[BIYOMETRIK]", "[CEZA_KAYDI]", "[KAN_GRUBU]", "[ENGEL_DURUMU]",
    "[SIFRE]", "[PIN]", "[KULLANICI_ADI]", "[IP_ADRES]", "[MAC_ADRES]",
    "[IMEI]", "[CIHAZ_ID]", "[PLAKA]", "[SASI_NO]", "[MOTOR_NO]",
    "[RUHSAT_NO]", "[SICIL_NO]", "[ISYERI]", "[AILE]", "[REFERANS]",
}

ETIKET_RE = re.compile(r"\[[A-Z_]+\]")
IM_END = "<|im_end|>"

TALIMATLAR = {
    "Tam maskeleme":
        "Metindeki tüm kişisel ve hassas bilgileri uygun etiketlerle maskele. "
        "Diğer kısımları değiştirme.",
    "Yalnızca IBAN":
        "Yalnızca IBAN bilgisini maskele. Diğer bilgileri değiştirme.",
    "Yalnızca telefon":
        "Sadece telefon numarasını maskele. Geri kalan her şey aynen kalsın.",
    "Ad soyad açık kalsın":
        "Kişinin adını açık bırak. Diğer kişisel bilgileri maskele.",
    "IBAN açık kalsın":
        "IBAN bilgisini değiştirme. Diğer kişisel ve hassas bilgileri maskele.",
    "Sadece finansal veriler":
        "Bu metinde sadece finansal bilgileri maskele.",
    "Sadece özel nitelikli (KVKK md. 6)":
        "Bu metinde sadece KVKK madde 6 kapsamındaki özel nitelikli kişisel "
        "verileri maskele.",
    "Serbest (aşağıya yaz)": "",
}

# ZeroGPU tespiti ORTAM DEĞİŞKENİNDEN yapılmalı, paketin varlığından değil:
# `spaces` requirements.txt'te olduğu için cpu-basic'te de kuruluyor ve
# "import edilebiliyorsa GPU vardır" varsayımı yanlış sonuç veriyor.
# HF, ZeroGPU donanımında SPACES_ZERO_GPU=true tanımlar.
#
# Ayrıca ZeroGPU'da torch.cuda.is_available() açılışta False döner — GPU
# yalnızca @spaces.GPU ile dekore edilmiş fonksiyon çalışırken bağlanır.
# Bu yüzden cuda kontrolü tek başına da yeterli değil.
ZEROGPU = os.environ.get("SPACES_ZERO_GPU", "").lower() in ("1", "true")

if ZEROGPU:
    import spaces
else:
    spaces = None
    # os.cpu_count() konteynerde HOST çekirdek sayısını verir; Space'e ayrılan
    # 2 vCPU'ya 32 thread açmak çekirdekleri birbirine bekletir ve üretimi
    # yavaşlatır. sched_getaffinity cgroup sınırını görür.
    try:
        cekirdek = len(os.sched_getaffinity(0))
    except AttributeError:
        cekirdek = os.cpu_count() or 2
    torch.set_num_threads(max(1, min(cekirdek, 4)))
    print("CPU thread:", torch.get_num_threads(), flush=True)

GPU_VAR = ZEROGPU or torch.cuda.is_available()

print("Model yükleniyor… ZeroGPU:", ZEROGPU, flush=True)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ADI)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ADI,
    dtype=torch.bfloat16 if GPU_VAR else torch.float32,
).eval()
if GPU_VAR:
    model = model.to("cuda")
IM_END_ID = tokenizer.convert_tokens_to_ids(IM_END)
print("Model hazır. Cihaz:", next(model.parameters()).device, flush=True)


def gpu_gerekiyorsa(fn):
    """ZeroGPU'da @spaces.GPU uygular, CPU'da fonksiyonu olduğu gibi bırakır."""
    return spaces.GPU(duration=20)(fn) if ZEROGPU else fn


@gpu_gerekiyorsa
@torch.inference_mode()
def maskele(metin, talimat):
    """Maskelenmiş metni ve geçen süreyi döndürür."""
    mesajlar = [
        {"role": "system", "content": talimat},
        {"role": "user", "content": metin},
    ]
    prompt = tokenizer.apply_chat_template(
        mesajlar, tokenize=False, add_generation_prompt=True,
        enable_thinking=False,
    )
    girdi = tokenizer(prompt, return_tensors="pt",
                      add_special_tokens=False).to(model.device)

    # Çıktı girdiyle yaklaşık aynı uzunlukta: maskeleme metni yeniden yazmaz,
    # yalnızca bazı parçaları etiketle değiştirir. Sabit 320 yerine girdiye
    # bağlı sınır, model <|im_end|> üretmezse boşa dönmeyi engeller.
    girdi_uzunlugu = girdi["input_ids"].shape[1]
    sinir = min(256, max(48, girdi_uzunlugu + 24))

    baslangic = time.time()
    cikti = model.generate(
        **girdi,
        max_new_tokens=sinir,
        do_sample=False,               # maskeleme belirlenimci olmalı
        eos_token_id=IM_END_ID,
        pad_token_id=tokenizer.pad_token_id,
        use_cache=True,
    )
    sure = time.time() - baslangic

    yeni = cikti[0, girdi_uzunlugu:]
    return tokenizer.decode(yeni, skip_special_tokens=True).strip(), sure


def calistir(metin, secim, serbest_talimat):
    metin = (metin or "").strip()
    if not metin:
        return "", "Metin gir.", ""

    talimat = serbest_talimat.strip() if secim == "Serbest (aşağıya yaz)" \
        else TALIMATLAR[secim]
    if not talimat:
        talimat = TALIMATLAR["Tam maskeleme"]

    cikti, sure = maskele(metin, talimat)

    etiketler = ETIKET_RE.findall(cikti)
    uydurma = sorted({e for e in etiketler if e not in GECERLI_ETIKETLER})

    if not etiketler:
        ozet = "Kişisel veri bulunmadı — metin değiştirilmedi."
    else:
        sayim = {}
        for e in etiketler:
            sayim[e] = sayim.get(e, 0) + 1
        ozet = "Maskelenen: " + ", ".join(
            f"{e}×{n}" if n > 1 else e for e, n in sorted(sayim.items()))
    ozet += f"   ·   {sure:.1f} sn"

    if uydurma:
        uyari = (
            f"⚠️ Şema dışı etiket üretildi: {', '.join(uydurma)}\n\n"
            "Model 50 etiketin dışında bir etiket uydurdu — genellikle "
            "metindeki komşu kelimeden türetir. 5.000 örnekte ~9 kez görülür "
            "(%0,18). Üretimde bu satırlar reddedilmeli veya insan "
            "incelemesine gönderilmelidir."
        )
    else:
        uyari = "✅ Tüm etiketler şema içinde."

    return cikti, ozet, uyari


ORNEKLER = [
    # Tam maskeleme
    ["müşteri Ahmet Yıldız 0542 321 45 67 numarasından aradı, "
     "iade hesabı TR12 0001 2000 0034 5678 9012 34", "Tam maskeleme", ""],
    ["tc kimlik numaram otuz dört yirmi üç altmış beş yetmiş sekiz doksan bir, "
     "acil bakar mısınız", "Tam maskeleme", ""],
    ["aracımın plakası 34 ABC 123, şasi numarası NM0KXXTTFKJ123456 "
     "kasko yenilenecek", "Tam maskeleme", ""],

    # Aynı görünen iki 16 hane: biri kart, diğeri seri numarası
    ["kart numaram 5401 2345 6789 1234 işlem yapamıyorum",
     "Sadece finansal veriler", ""],
    ["ürün seri numarası 5401234567891234 olarak kayıtlı", "Tam maskeleme", ""],

    # Aynı cümlede yok sayılan kod ve maskelenen güvenlik kodu
    ["işlem kodu 419 ama kartın arkasındaki güvenlik kodu 732",
     "Tam maskeleme", ""],
    ["kargo takip kodu 12345678901 henüz hareket etmemiş",
     "Ad soyad açık kalsın", ""],
    ["maaşım 45.000 TL kredi başvurusu için yeterli mi",
     "Sadece finansal veriler", ""],

    # Aynı cümle, dört politika — talimatın çıktıyı nasıl çevirdiğini gösterir
    ["Ali Kaya 0532 111 22 33 numarasından aradı, "
     "ibanı TR33 0006 1005 1978 6457 8413 26", "Yalnızca IBAN", ""],
    ["Ali Kaya 0532 111 22 33 numarasından aradı, "
     "ibanı TR33 0006 1005 1978 6457 8413 26", "IBAN açık kalsın", ""],
    ["Ali Kaya 0532 111 22 33 numarasından aradı, "
     "ibanı TR33 0006 1005 1978 6457 8413 26", "Yalnızca telefon", ""],
    ["Ali Kaya 0532 111 22 33 numarasından aradı, "
     "ibanı TR33 0006 1005 1978 6457 8413 26", "Ad soyad açık kalsın", ""],

    # Seçici politikalar
    ["iban:TR330006100519786457841326,telefon:05321112233",
     "Yalnızca telefon", ""],
    ["Müşteri Selin Aksoy tc 10000000146 telefon 0507 123 45 67",
     "Ad soyad açık kalsın", ""],
    ["başvuru sahibi Zeynep Arslan, mail zeynep.arslan@firma.com.tr, "
     "adres Cumhuriyet Mah. Lale Sok. No:14 Kadıköy İstanbul",
     "Ad soyad açık kalsın", ""],

    # Kategori ve serbest talimat
    ["personel kaydımda kan grubum A Rh negatif ve inanç bilgim Alevi yazıyor",
     "Sadece özel nitelikli (KVKK md. 6)", ""],
    ["ben emre taş tel 0531 222 44 55 iban da TR330006100519786457841326",
     "Serbest (aşağıya yaz)", "kişisel bilgi ne varsa kapat geri kalan yazıya dokunma"],
    ["vpn tanımlayın ip adresim 88.240.12.34, cihaz kimliğim HM8C-S7JK-YDFX",
     "Serbest (aşağıya yaz)", "teknik kimlikler dahil tüm kişisel verileri gizle"],
]

ACIKLAMA = """
# Türkçe PII Guard

Türkçe metindeki kişisel verileri tespit edip maskeleyen 0.8B model.
50 etiket tanır ve **kapsam talimatını okur** — aynı metin farklı politika
altında farklı maskelenir.

Model [melikegks/turkish-pii-guard-0.8b](https://huggingface.co/melikegks/turkish-pii-guard-0.8b)

**Not:** Buraya gerçek kişisel veri girme. Değerlendirme tamamen sentetik veriyle
yapılmıştır; gerçek metindeki performans ölçülmemiştir.
"""

with gr.Blocks(title="Türkçe PII Guard") as demo:
    gr.Markdown(ACIKLAMA)

    with gr.Row():
        with gr.Column():
            metin = gr.Textbox(
                label="Metin", lines=5,
                placeholder="müşteri Ahmet Yıldız 0542 321 45 67 numarasından aradı…",
            )
            secim = gr.Radio(
                choices=list(TALIMATLAR), value="Tam maskeleme",
                label="Maskeleme politikası",
            )
            serbest = gr.Textbox(
                label="Serbest talimat", lines=2,
                placeholder="Örn: Maaşı gizle, ad soyad ve telefona dokunma.",
            )
            calistir_btn = gr.Button("Maskele", variant="primary")

        with gr.Column():
            cikti = gr.Textbox(label="Maskelenmiş metin", lines=5,
                               show_copy_button=True)
            ozet = gr.Textbox(label="Özet", lines=2)
            uyari = gr.Textbox(label="Şema kontrolü", lines=4)

    gr.Examples(examples=ORNEKLER, inputs=[metin, secim, serbest])

    calistir_btn.click(calistir, [metin, secim, serbest], [cikti, ozet, uyari])
    metin.submit(calistir, [metin, secim, serbest], [cikti, ozet, uyari])

demo.queue().launch()