sentinel-api / app.py
Mustafa Öztürk
Use /analyze-batch for bulk UI requests
78509a9
import io
import os
import subprocess
import time
from datetime import datetime
import pandas as pd
import requests
import streamlit as st
try:
import psutil
except ImportError:
psutil = None
st.set_page_config(
page_title="Sentinel — İçerik Moderasyon",
layout="wide",
initial_sidebar_state="expanded",
)
st.markdown(
"""
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap');
html, body, [class*="css"] {
font-family: 'IBM Plex Sans', sans-serif;
background-color: #0a0e17;
color: #c9d1e0;
}
[data-testid="stSidebar"] {
background: #0d1220;
border-right: 1px solid #1e2d45;
min-width: 300px !important;
max-width: 300px !important;
width: 300px !important;
margin-left: 0 !important;
transform: translateX(0) !important;
flex-shrink: 0 !important;
}
[data-testid="stSidebar"][aria-expanded="false"] {
min-width: 300px !important;
max-width: 300px !important;
width: 300px !important;
margin-left: 0 !important;
transform: translateX(0) !important;
}
[data-testid="stSidebar"][aria-expanded="true"] {
min-width: 300px !important;
max-width: 300px !important;
width: 300px !important;
}
[data-testid="stSidebarContent"] {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
[data-testid="stSidebar"] * { color: #8a9bc0 !important; }
[data-testid="stSidebar"] .stRadio label { color: #c9d1e0 !important; }
[data-testid="collapsedControl"],
[data-testid="stSidebarCollapseButton"],
button[title="Close sidebar"],
button[title="Open sidebar"] { display: none !important; }
#MainMenu, footer, header { visibility: hidden; }
.block-container { padding-top: 1.5rem; padding-bottom: 2rem; }
.sentinel-header {
display: flex; align-items: center; gap: 16px;
padding: 20px 0 28px 0;
border-bottom: 1px solid #1e2d45;
margin-bottom: 28px;
}
.sentinel-logo {
width: 44px; height: 44px;
background: linear-gradient(135deg, #1a6cf7, #0d3d8e);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 22px;
}
.sentinel-title { font-family:'IBM Plex Mono',monospace; font-size:22px; font-weight:600; color:#e8eef8; }
.sentinel-sub { font-size:12px; color:#6f86ab; font-family:'IBM Plex Mono',monospace; letter-spacing:1px; text-transform:uppercase; }
.status-pill {
margin-left:auto; background:#0a1f0e; border:1px solid #1a5c28;
color:#3ddc5f; font-family:'IBM Plex Mono',monospace;
font-size:11px; padding:4px 12px; border-radius:20px;
}
.status-dot { display:inline-block; width:7px; height:7px; background:#3ddc5f; border-radius:50%; margin-right:6px; animation:pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }
.verdict-card { border-radius:12px; padding:24px 28px; margin-bottom:20px; border:1px solid; position:relative; overflow:hidden; }
.verdict-card::before { content:''; position:absolute; top:0; left:0; width:4px; height:100%; }
.verdict-TEMIZ { background:#050f07; border-color:#1a4d25; } .verdict-TEMIZ::before { background:#2ea84a; }
.verdict-KUFUR { background:#0f0c02; border-color:#4d3d08; } .verdict-KUFUR::before { background:#d4a017; }
.verdict-SALDIRGAN{ background:#0f0c02; border-color:#4d3d08; } .verdict-SALDIRGAN::before{ background:#d4a017; }
.verdict-TOXIC { background:#0f0c02; border-color:#4d3d08; } .verdict-TOXIC::before { background:#d4a017; }
.verdict-NEFRET { background:#120a02; border-color:#5c2e0a; } .verdict-NEFRET::before { background:#e07020; }
.verdict-INCELEME { background:#060a13; border-color:#1a2d5c; } .verdict-INCELEME::before { background:#3a7bd4; }
.verdict-SPAM { background:#080810; border-color:#2a1a4d; } .verdict-SPAM::before { background:#8030d4; }
.verdict-label { font-family:'IBM Plex Mono',monospace; font-size:26px; font-weight:600; margin-bottom:6px; }
.verdict-reason { font-size:14px; color:#6a7f9a; font-family:'IBM Plex Mono',monospace; }
.metric-row { display:flex; gap:12px; margin-bottom:20px; }
.metric-card { flex:1; background:#0d1220; border:1px solid #1e2d45; border-radius:10px; padding:16px 20px; }
.metric-label { font-family:'IBM Plex Mono',monospace; font-size:11px; color:#7690b8; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px; }
.metric-value { font-family:'IBM Plex Mono',monospace; font-size:24px; font-weight:600; color:#e8eef8; }
.metric-value.low{color:#2ea84a} .metric-value.med{color:#d4a017} .metric-value.high{color:#e03030}
.score-row { margin-bottom:14px; }
.score-label { display:flex; justify-content:space-between; font-family:'IBM Plex Mono',monospace; font-size:12px; color:#8ea7cb; margin-bottom:5px; }
.score-track { height:5px; background:#1a2535; border-radius:3px; overflow:hidden; }
.score-fill { height:100%; border-radius:3px; }
.stTextArea textarea { background:#0d1220 !important; border:1px solid #1e2d45 !important; border-radius:10px !important; color:#c9d1e0 !important; font-family:'IBM Plex Sans',sans-serif !important; font-size:15px !important; padding:14px !important; }
.stTextArea textarea:focus { border-color:#1a6cf7 !important; }
.stButton button { background:#1a6cf7 !important; color:white !important; border:none !important; border-radius:8px !important; font-family:'IBM Plex Sans',sans-serif !important; font-weight:500 !important; font-size:14px !important; padding:10px 24px !important; }
.stButton button:hover { background:#1557cc !important; }
.stTabs [data-baseweb="tab-list"] { background:transparent !important; border-bottom:1px solid #1e2d45 !important; }
.stTabs [data-baseweb="tab"] { background:transparent !important; color:#4a6080 !important; font-family:'IBM Plex Mono',monospace !important; font-size:13px !important; padding:10px 20px !important; border-bottom:2px solid transparent !important; }
.stTabs [aria-selected="true"] { color:#1a6cf7 !important; border-bottom-color:#1a6cf7 !important; background:transparent !important; }
[data-testid="stFileUploader"] { background:#0d1220 !important; border:1px dashed #1e2d45 !important; border-radius:10px !important; }
.stRadio label { background:#111827 !important; border:1px solid #1e2d45 !important; border-radius:8px !important; padding:10px 14px !important; }
.stRadio label:has(input:checked) { border-color:#1a6cf7 !important; background:#0d1a33 !important; }
hr { border-color:#1e2d45 !important; }
.stTextInput input { background:#0d1220 !important; border:1px solid #1e2d45 !important; color:#c9d1e0 !important; border-radius:8px !important; font-family:'IBM Plex Mono',monospace !important; font-size:12px !important; }
[data-testid="stDataFrame"] { border:1px solid #1e2d45 !important; border-radius:10px !important; overflow:hidden !important; }
.stProgress > div > div { background:#1a6cf7 !important; }
.report-table { width:100%; border-collapse:collapse; font-family:'IBM Plex Mono',monospace; font-size:12px; }
.report-table th {
text-align:left; padding:10px 14px;
color:#4a6080; font-weight:600; font-size:10px;
letter-spacing:1.2px; text-transform:uppercase;
background:#0d1220; border-bottom:1px solid #1e2d45;
position:sticky; top:0; z-index:10;
}
.report-table td { padding:10px 14px; border-bottom:1px solid #0f1826; vertical-align:middle; }
.report-table tr:hover td { background:#0d1525; }
.risk-badge {
display:inline-block; padding:2px 10px; border-radius:12px;
font-size:10px; font-weight:600; letter-spacing:0.8px;
font-family:'IBM Plex Mono',monospace;
}
.badge-CRITICAL { background:#1f0c0c; color:#e03030; border:1px solid #5c1a1a; }
.badge-HIGH { background:#1a0e03; color:#e07020; border:1px solid #5c2e0a; }
.badge-MEDIUM { background:#141002; color:#d4a017; border:1px solid #4d3d08; }
.badge-LOW { background:#07091a; color:#3a7bd4; border:1px solid #1a2d5c; }
.badge-NONE { background:#050f07; color:#2ea84a; border:1px solid #1a4d25; }
.inline-bar {
display:inline-block; height:4px; border-radius:2px;
vertical-align:middle; margin-right:4px;
}
.hits-tag {
display:inline-block; background:#1f0e0e; border:1px solid #5c1a1a;
color:#e05050; font-size:10px; padding:1px 6px; border-radius:4px; margin:1px;
}
.karar-cell { font-weight:600; font-size:11px; }
.metin-cell { color:#8a9bc0; max-width:280px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.skor-cell { color:#6a8cb0; font-size:11px; }
.summary-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(140px, 1fr)); gap:12px; margin-bottom:24px; }
.summary-card { background:#0d1220; border:1px solid #1e2d45; border-radius:10px; padding:16px; text-align:center; }
.summary-count { font-family:'IBM Plex Mono',monospace; font-size:36px; font-weight:700; margin-bottom:4px; }
.summary-label { font-family:'IBM Plex Mono',monospace; font-size:10px; color:#4a6080; text-transform:uppercase; letter-spacing:1px; }
.queue-card {
background:#060a13; border:1px solid #1a2d5c; border-radius:10px;
padding:16px; margin-bottom:10px;
display:flex; gap:16px; align-items:flex-start;
}
.queue-index { font-family:'IBM Plex Mono',monospace; font-size:11px; color:#2a3d55; min-width:28px; }
.queue-text { color:#c9d1e0; font-size:13px; line-height:1.5; flex:1; }
.queue-meta { font-family:'IBM Plex Mono',monospace; font-size:10px; color:#4a6080; margin-top:4px; }
</style>
""",
unsafe_allow_html=True,
)
API_URL = os.getenv("SENTINEL_API_URL", "https://moztrk-sentinel-api.hf.space/analyze")
VERDICT_COLORS = {
"CRITICAL": "#e03030",
"HIGH": "#e07020",
"MEDIUM": "#d4a017",
"LOW": "#3a7bd4",
"NONE": "#2ea84a",
}
VERDICT_ICONS = {"CRITICAL": "🚨", "HIGH": "🤬", "MEDIUM": "◆", "LOW": "▲", "NONE": "✓"}
if "last_latency_ms" not in st.session_state:
st.session_state["last_latency_ms"] = None
if "last_metrics" not in st.session_state:
st.session_state["last_metrics"] = None
def get_gpu_info():
try:
result = subprocess.check_output(
[
"nvidia-smi",
"--query-gpu=name,utilization.gpu,temperature.gpu,memory.used,memory.total",
"--format=csv,noheader,nounits",
],
encoding="utf-8",
stderr=subprocess.STDOUT,
)
line = result.strip().splitlines()[0]
name, util, temp, mem_used, mem_total = [p.strip() for p in line.split(",", maxsplit=4)]
return {
"name": name,
"load": int(float(util)),
"temp": int(float(temp)),
"vram_used": int(float(mem_used)),
"vram_total": int(float(mem_total)),
}
except Exception:
return None
def capture_process_metrics():
gpu_data = get_gpu_info()
cpu_val = 0.0
ram_pct = 0.0
if psutil is not None:
cpu_val = psutil.cpu_percent(interval=0.1)
ram_pct = psutil.virtual_memory().percent
return {
"cpu": round(cpu_val, 1),
"ram_pct": round(ram_pct, 1),
"vram_used": str(gpu_data["vram_used"]) if gpu_data else "0",
"gpu_load": str(gpu_data["load"]) if gpu_data else "0",
"timestamp": time.strftime("%H:%M:%S"),
}
def resolve_api_endpoints(api_url_raw: str):
base = (api_url_raw or "").strip().rstrip("/")
if base.endswith("/analyze"):
root = base[: -len("/analyze")]
elif base.endswith("/analyze-batch"):
root = base[: -len("/analyze-batch")]
else:
root = base
analyze_url = f"{root}/analyze"
batch_url = f"{root}/analyze-batch"
return analyze_url, batch_url
def verdict_css_class(decision):
d = decision.upper()
if "TEMIZ" in d or "CLEAR" in d:
return "TEMIZ"
if "HAKARET" in d or "INSULT" in d:
return "SALDIRGAN"
if "NEFRET" in d or "IDENTITY" in d:
return "NEFRET"
if "KÜFÜR" in d or "KUFUR" in d or "PROFANITY" in d:
return "KUFUR"
if "SALDIRGAN" in d or "TOXIC" in d:
return "SALDIRGAN"
if "İNCELEME" in d or "INCELEME" in d or "REVIEW" in d:
return "INCELEME"
if "SPAM" in d or "GİBBERİSH" in d:
return "SPAM"
return "TEMIZ"
def risk_color(val):
if val > 0.7:
return "#e03030"
if val > 0.4:
return "#d4a017"
if val > 0.15:
return "#f0a020"
return "#2ea84a"
def score_bar(label, value, color="#1a6cf7"):
pct = min(max(value * 100, 0), 100)
return f"""<div class=\"score-row\">
<div class=\"score-label\"><span>{label}</span><span style=\"color:{color};font-weight:600\">%{pct:.1f}</span></div>
<div class=\"score-track\"><div class=\"score-fill\" style=\"width:{pct}%;background:{color}\"></div></div>
</div>"""
def badge_html(risk):
cls = {
"CRITICAL": "badge-CRITICAL",
"HIGH": "badge-HIGH",
"MEDIUM": "badge-MEDIUM",
"LOW": "badge-LOW",
"NONE": "badge-NONE",
}.get(risk.upper(), "badge-NONE")
return f'<span class="risk-badge {cls}">{risk}</span>'
def inline_bar_html(value, color):
w = min(max(value * 60, 0), 60)
return f'<span class="inline-bar" style="width:{w}px;background:{color}"></span><span style="color:{color};font-size:11px">%{value * 100:.0f}</span>'
def generate_docx_report(res_df, total_time, platform_dil):
try:
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
except ImportError:
return None
doc = Document()
for section in doc.sections:
section.top_margin = Cm(1.8)
section.bottom_margin = Cm(1.8)
section.left_margin = Cm(2.0)
section.right_margin = Cm(2.0)
def set_cell_bg(cell, hex_color):
tc = cell._tc
tc_pr = tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), hex_color)
tc_pr.append(shd)
def add_run(para, text, bold=False, size=10, color="000000", italic=False):
run = para.add_run(text)
run.bold = bold
run.italic = italic
run.font.size = Pt(size)
run.font.color.rgb = RGBColor(int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
return run
title_para = doc.add_paragraph()
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_run(title_para, "SENTINEL AI - Moderasyon Analiz Raporu", bold=True, size=18, color="1F4E79")
sub_para = doc.add_paragraph()
sub_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
ts = datetime.now().strftime("%d.%m.%Y %H:%M")
add_run(
sub_para,
f"Platform: {platform_dil.upper()} | Olusturulma: {ts} | {len(res_df)} kayit | {total_time:.1f}s",
size=9,
color="888888",
)
doc.add_paragraph()
counts = res_df["Karar"].value_counts()
sum_para = doc.add_paragraph()
add_run(sum_para, "OZET", bold=True, size=11, color="1F4E79")
sum_tbl = doc.add_table(rows=1, cols=len(counts) + 1)
sum_tbl.style = "Table Grid"
hdr = sum_tbl.rows[0].cells
set_cell_bg(hdr[0], "1F4E79")
p = hdr[0].paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_run(p, "Metrik", bold=True, size=9, color="FFFFFF")
karar_colors = {
"TEMIZ": "2EA84A",
"KÜFÜR": "D4A017",
"KUFUR": "D4A017",
"PROFANITY": "D4A017",
"SALDIRGAN": "D4A017",
"TOXIC": "D4A017",
"NEFRET": "E07020",
"INCELEME": "3A7BD4",
"SPAM": "8030D4",
"GIBBERISH": "8030D4",
}
for i, (karar, cnt) in enumerate(counts.items()):
cell = hdr[i + 1]
set_cell_bg(cell, "0D1220")
p2 = cell.paragraphs[0]
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
c = next((v for k, v in karar_colors.items() if k in karar.upper()), "888888")
add_run(p2, f"{cnt}", bold=True, size=14, color=c)
p3 = cell.add_paragraph()
p3.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_run(p3, karar[:16], size=7, color="888888")
doc.add_paragraph()
detail_para = doc.add_paragraph()
add_run(detail_para, "DETAYLI ANALIZ SONUCLARI", bold=True, size=11, color="1F4E79")
cols = ["#", "Metin", "Normalize", "Karar", "Risk", "Saldirganlik", "Nefret", "Tehdit", "Hits"]
tbl = doc.add_table(rows=1, cols=len(cols))
tbl.style = "Table Grid"
for i, col_name in enumerate(cols):
cell = tbl.rows[0].cells[i]
set_cell_bg(cell, "1F4E79")
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_run(p, col_name, bold=True, size=8, color="FFFFFF")
for idx, row in res_df.iterrows():
tr = tbl.add_row()
cells = tr.cells
risk_str = str(row.get("Risk", "")).upper()
row_colors = {
"CRITICAL": "1F0C0C",
"HIGH": "1A0E03",
"MEDIUM": "141002",
"LOW": "07091A",
"NONE": "050F07",
}
row_fill = row_colors.get(risk_str, "0D1220")
set_cell_bg(cells[0], row_fill)
p = cells[0].paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_run(p, str(idx + 1), size=8, color="4A6080")
set_cell_bg(cells[1], row_fill)
p = cells[1].paragraphs[0]
add_run(p, str(row.get("Metin", ""))[:120], size=8, color="C9D1E0")
set_cell_bg(cells[2], row_fill)
p = cells[2].paragraphs[0]
add_run(p, str(row.get("Normalize", ""))[:60], size=7, color="6A8CB0", italic=True)
set_cell_bg(cells[3], row_fill)
p = cells[3].paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
karar = str(row.get("Karar", ""))
c = next((v for k, v in karar_colors.items() if k in karar.upper()), "888888")
add_run(p, karar[:20], bold=True, size=8, color=c)
set_cell_bg(cells[4], row_fill)
p = cells[4].paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
risk_colors = {
"CRITICAL": "E03030",
"HIGH": "E07020",
"MEDIUM": "D4A017",
"LOW": "3A7BD4",
"NONE": "2EA84A",
}
rc = risk_colors.get(risk_str, "888888")
add_run(p, risk_str, bold=True, size=8, color=rc)
for col_i, field in [(5, "Saldırganlık"), (6, "Nefret"), (7, "Tehdit")]:
set_cell_bg(cells[col_i], row_fill)
p = cells[col_i].paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
score = float(row.get(field, 0.0))
add_run(p, f"%{score * 100:.1f}", size=8, color=risk_color(score).replace("#", ""))
set_cell_bg(cells[8], row_fill)
p = cells[8].paragraphs[0]
hits = str(row.get("Hits", "")).strip("[]'\"")
add_run(p, hits if hits else "-", size=7, color="E05050" if hits else "2A3D55")
widths_cm = [0.7, 4.5, 3.0, 2.8, 1.5, 1.4, 1.4, 1.4, 2.0]
for i, w in enumerate(widths_cm):
for row in tbl.rows:
row.cells[i].width = Cm(w)
doc.add_paragraph()
inceleme = res_df[res_df["Karar"].str.contains("İNCELEME|INCELEME|REVIEW", na=False)]
if len(inceleme):
q_para = doc.add_paragraph()
add_run(q_para, f"INCELEME KUYRUGU - {len(inceleme)} Icerik", bold=True, size=11, color="3A7BD4")
for _, row in inceleme.iterrows():
q_tbl = doc.add_table(rows=1, cols=1)
q_tbl.style = "Table Grid"
cell = q_tbl.rows[0].cells[0]
set_cell_bg(cell, "060A13")
p = cell.paragraphs[0]
add_run(p, str(row.get("Metin", ""))[:200], size=9, color="C9D1E0")
p2 = cell.add_paragraph()
add_run(
p2,
f"Risk: {row.get('Risk', '')} | Saldirganlik: %{float(row.get('Saldırganlık', 0)) * 100:.0f} | {row.get('Gerekçe', '')}",
size=8,
color="4A6080",
italic=True,
)
doc.add_paragraph()
footer_p = doc.add_paragraph()
footer_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_run(footer_p, "Sentinel AI - Dahili Kullanim - " + datetime.now().strftime("%Y"), size=8, color="2A3D55")
buf = io.BytesIO()
doc.save(buf)
buf.seek(0)
return buf
st.markdown(
"""
<div class="sentinel-header">
<div class="sentinel-logo">⬡</div>
<div>
<div class="sentinel-title">Sentinel</div>
<div class="sentinel-sub">İçerik Moderasyon Sistemi</div>
</div>
<div class="status-pill"><span class="status-dot"></span>ONLINE</div>
</div>
""",
unsafe_allow_html=True,
)
with st.sidebar:
st.markdown(
"""<div style="padding:8px 0 20px 0; border-bottom:1px solid #1e2d45; margin-bottom:20px;">
<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#4a6080; letter-spacing:1.5px; text-transform:uppercase; margin-bottom:16px;">Sistem Konfigürasyonu</div>
</div>""",
unsafe_allow_html=True,
)
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#4a6080; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px;">Platform Dili</div>""",
unsafe_allow_html=True,
)
platform_dil = st.radio(
"Platform dili",
["tr", "en"],
format_func=lambda x: "Türkçe · TR Pipeline" if x == "tr" else "English · EN Pipeline",
label_visibility="collapsed",
)
st.markdown("<br>", unsafe_allow_html=True)
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#4a6080; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px;">API Endpoint</div>""",
unsafe_allow_html=True,
)
api_url = st.text_input("API", value=API_URL, label_visibility="collapsed")
st.markdown("<br><br>", unsafe_allow_html=True)
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace; font-size:11px; color:#2a3d55; line-height:1.8;">
TR PIPELINE<br><span style="color:#4a6289">──────────────</span><br>
<span style="color:#6f8fbf">▸</span> is_spam() evrensel filtre<br>
<span style="color:#6f8fbf">▸</span> Küfür listesi lookup<br>
<span style="color:#6f8fbf">▸</span> BERTurk offensive 42K<br>
<span style="color:#6f8fbf">▸</span> Detoxify multilingual<br><br>
EN PIPELINE<br><span style="color:#4a6289">──────────────</span><br>
<span style="color:#6f8fbf">▸</span> is_spam() evrensel filtre<br>
<span style="color:#6f8fbf">▸</span> Gibberish Detector<br>
<span style="color:#6f8fbf">▸</span> Detoxify original 6-label
</div>""",
unsafe_allow_html=True,
)
st.markdown("---")
st.markdown("### 🖥️ Sistem Monitörü")
if psutil is None:
st.warning("psutil yüklü değil. Kurulum: pip install psutil")
else:
cpu_load = psutil.cpu_percent(interval=0.2)
ram = psutil.virtual_memory()
ram_used_gb = ram.used / (1024**3)
col1, col2 = st.columns(2)
col1.metric("CPU Yükü", f"%{cpu_load:.0f}")
col2.metric("RAM", f"{ram_used_gb:.1f} GB", f"%{ram.percent:.0f}", delta_color="inverse")
gpu = get_gpu_info()
if gpu:
st.markdown(f"**GPU:** {gpu['name']}")
col3, col4 = st.columns(2)
col3.metric("GPU Yükü", f"%{gpu['load']}")
col4.metric("GPU Isı", f"{gpu['temp']}°C")
vram_pct = 0.0
if gpu["vram_total"] > 0:
vram_pct = min(max(gpu["vram_used"] / gpu["vram_total"], 0.0), 1.0)
st.write(f"VRAM: {gpu['vram_used']}MB / {gpu['vram_total']}MB")
st.progress(vram_pct)
else:
st.warning("GPU bilgisi alınamadı (nvidia-smi erişimi yok).")
st.markdown("---")
live_latency = st.session_state.get("last_latency_ms")
if live_latency is None:
st.info("🚀 **Model Latency:** N/A\n\n🛡️ **Sentinel v2.9 Active**")
else:
st.info(f"🚀 **Model Latency:** ~{live_latency:.0f}ms/req\n\n🛡️ **Sentinel v2.9 Active**")
st.markdown("---")
if st.session_state.get("last_metrics"):
m = st.session_state["last_metrics"]
st.markdown("### ⚡ Son İşlem Performansı")
st.caption(f"Saat: {m['timestamp']} (İstek anındaki veriler)")
col5, col6 = st.columns(2)
col5.metric("İşlem CPU", f"%{m['cpu']}")
col6.metric("İşlem RAM", f"%{m['ram_pct']}")
col7, col8 = st.columns(2)
col7.metric("GPU Yükü", f"%{m['gpu_load']}")
col8.metric("VRAM", f"{m['vram_used']} MB")
st.success("Analiz işlemi için performans verisi kaydedildi.")
else:
st.info("Performans verisi için analiz başlatın.")
tab1, tab2 = st.tabs([" Tek Metin Analizi ", " Toplu Analiz "])
with tab1:
st.markdown("<br>", unsafe_allow_html=True)
user_input = st.text_area(
"Analiz metni",
height=120,
placeholder="Analiz edilecek metni buraya yazın...",
label_visibility="collapsed",
)
col_btn, col_info = st.columns([2, 5])
with col_btn:
analyze_btn = st.button("Analiz Et", use_container_width=True)
with col_info:
st.markdown(
"""<div style="padding:10px 0; font-family:'IBM Plex Mono',monospace; font-size:11px; color:#8ea7cb; line-height:1.8;">Spam → Dil → Küfür → Model → Karar</div>""",
unsafe_allow_html=True,
)
if analyze_btn:
if not user_input.strip():
st.warning("Analiz için metin gerekli.")
else:
with st.spinner(""):
try:
t0 = time.time()
analyze_url, _ = resolve_api_endpoints(api_url)
resp = requests.post(analyze_url, json={"text": user_input, "platform_dil": platform_dil}, timeout=30)
st.session_state["last_metrics"] = capture_process_metrics()
elapsed = (time.time() - t0) * 1000
except requests.RequestException as e:
st.error(f"API bağlantı hatası: {e}")
st.stop()
if resp.status_code != 200:
st.error(f"API {resp.status_code} döndü.")
st.stop()
r = resp.json()
decision = r.get("decision", "—")
reason = r.get("reason", "—")
risk = r.get("risk_level", "None")
risk_u = str(risk).upper()
lang = r.get("language", platform_dil).upper()
cleaned = r.get("cleaned_text", "")
details = r.get("details", {})
latency = r.get("latency_ms", round(elapsed, 1))
st.session_state["last_latency_ms"] = float(latency)
backend_perf = r.get("performance")
if isinstance(backend_perf, dict):
st.session_state["last_metrics"] = {
"cpu": backend_perf.get("cpu", 0),
"ram_pct": backend_perf.get("ram_pct", 0),
"vram_used": str(backend_perf.get("vram_used", 0)),
"gpu_load": str(backend_perf.get("gpu_load", 0)),
"timestamp": backend_perf.get("timestamp", time.strftime("%H:%M:%S")),
}
vcls = verdict_css_class(decision)
vcolor = VERDICT_COLORS.get(risk_u, "#2ea84a")
vicon = VERDICT_ICONS.get(risk_u, "✓")
st.markdown(
f"""<div class="verdict-card verdict-{vcls}">
<div class="verdict-label" style="color:{vcolor}">{vicon}&nbsp; {decision}
<span style="font-size:14px;color:#2a3d55;margin-left:12px;">[{lang}]</span>
</div>
<div class="verdict-reason">{reason}</div>
</div>""",
unsafe_allow_html=True,
)
lat_class = "low" if latency < 200 else ("med" if latency < 500 else "high")
risk_class = {
"CRITICAL": "high",
"HIGH": "high",
"MEDIUM": "med",
"LOW": "med",
"NONE": "low",
}.get(risk_u, "low")
st.markdown(
f"""<div class="metric-row">
<div class="metric-card"><div class="metric-label">Risk Seviyesi</div><div class="metric-value {risk_class}">{risk}</div></div>
<div class="metric-card"><div class="metric-label">Gecikme</div><div class="metric-value {lat_class}">{latency:.0f} ms</div></div>
<div class="metric-card"><div class="metric-label">Pipeline</div><div class="metric-value" style="font-size:18px;">{lang}</div></div>
<div class="metric-card" style="flex:2"><div class="metric-label">Normalize Edilen Metin</div>
<div style="font-family:'IBM Plex Mono',monospace;font-size:13px;color:#6a8cb0;margin-top:6px;word-break:break-all;">{cleaned}</div>
</div>
</div>""",
unsafe_allow_html=True,
)
hits = details.get("hits", []) or []
insult_hits = details.get("insult_hits", []) or []
if hits or insult_hits:
tags = "".join(f'<span class="hits-tag">⚡ {h}</span>' for h in hits)
tags += "".join(
f'<span class="hits-tag" style="color:#d4a017;border-color:#5c3d08;background:#1a1002">⚠ {h}</span>'
for h in insult_hits
)
st.markdown(
f"""<div style="margin-bottom:16px;">
<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">Kara Liste Eşleşmeleri</div>
{tags}
</div>""",
unsafe_allow_html=True,
)
col_scores, col_models = st.columns([1, 1.2])
with col_scores:
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;">Sinyal Analizi</div>""",
unsafe_allow_html=True,
)
bars = ""
if lang == "TR":
off = details.get("off_score", 0.0)
ia = details.get("detox", {}).get("identity_attack", 0.0)
thr = details.get("threat", 0.0)
bars += score_bar("Saldırganlık", off, risk_color(off))
bars += score_bar("Nefret (identity_attack)", ia, risk_color(ia))
bars += score_bar("Tehdit", thr, risk_color(thr))
else:
dtx = details.get("detox", {})
for key, lbl in [
("toxicity", "Toxicity"),
("threat", "Threat"),
("insult", "Insult"),
("identity_attack", "Identity Attack"),
("severe_toxicity", "Severe Toxicity"),
("obscene", "Obscene"),
]:
v = dtx.get(key, 0.0)
bars += score_bar(lbl, v, risk_color(v))
st.markdown(bars, unsafe_allow_html=True)
with col_models:
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;">Model Kaynak Analizi (Source)</div>""",
unsafe_allow_html=True,
)
rows_html = ""
if lang == "TR":
m_list = [
("BERTurk Offensive", "N/A", details.get("off_score", 0.0)),
("Detoxify (TR)", "Analyzed", details.get("detox", {}).get("toxicity", 0.0)),
]
else:
m_list = [
("Detoxify (Original)", "Analyzed", details.get("detox", {}).get("toxicity", 0.0)),
(
"Gibberish Detector",
details.get("gibberish_label", "N/A"),
details.get("gibberish_score", 0.0) or 0.0,
),
]
for m_name, m_dec, m_score in m_list:
try:
m_score = float(m_score)
except (TypeError, ValueError):
m_score = 0.0
c = risk_color(m_score)
rows_html += f"""<div style="background:#0d1220;border:1px solid #1e2d45;border-radius:8px;padding:10px;margin-bottom:8px;">
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">
<span style="font-size:12px;font-weight:600;color:#e8eef8;">{m_name}</span>
<span style="font-size:10px;color:{c};background:{c}22;padding:2px 8px;border-radius:4px;border:1px solid {c}44;white-space:nowrap;">
{m_dec} (%{m_score * 100:.1f})
</span>
</div>
</div>"""
st.markdown(rows_html, unsafe_allow_html=True)
with tab2:
st.markdown("<br>", unsafe_allow_html=True)
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:16px;">Veri Seti Yükle</div>""",
unsafe_allow_html=True,
)
uploaded = st.file_uploader("Dosya", type=["csv", "xlsx"], label_visibility="collapsed")
if uploaded:
df = pd.read_csv(uploaded) if uploaded.name.endswith(".csv") else pd.read_excel(uploaded)
if len(df) == 0:
st.warning("Dosya boş.")
st.stop()
st.markdown(
f"""<div style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#4a6080;margin-bottom:16px;">{len(df)} satır yüklendi</div>""",
unsafe_allow_html=True,
)
col_name = st.selectbox("Analiz sütunu:", df.columns)
if st.button("Toplu Analizi Başlat", use_container_width=False):
progress = st.progress(0)
status_text = st.empty()
results = []
t0 = time.time()
texts_payload = [str(text) for text in df[col_name]]
_, batch_url = resolve_api_endpoints(api_url)
progress.progress(0.2)
status_text.markdown(
f"""<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#4a6080;">Batch isteği gönderiliyor... ({len(texts_payload)} satır)</span>""",
unsafe_allow_html=True,
)
try:
resp = requests.post(
batch_url,
json={"texts": texts_payload, "platform_dil": platform_dil, "batch_size": 16},
timeout=300,
)
payload = resp.json() if resp.status_code == 200 else {}
except requests.RequestException as exc:
st.error(f"Batch API bağlantı hatası: {exc}")
st.stop()
if resp.status_code != 200:
st.error(f"Batch API {resp.status_code} döndü.")
st.stop()
items = payload.get("results", []) if isinstance(payload, dict) else []
if len(items) != len(texts_payload):
st.warning(f"Batch sonuç sayısı beklenenden farklı: {len(items)} / {len(texts_payload)}")
for text, r in zip(texts_payload, items):
details = r.get("details", {})
hits_all = list(details.get("hits", []) or []) + list(details.get("insult_hits", []) or [])
results.append(
{
"Metin": text,
"Normalize": r.get("cleaned_text", ""),
"Dil": r.get("language", "—").upper(),
"Karar": r.get("decision", "—"),
"Risk": r.get("risk_level", "—"),
"Gerekçe": r.get("reason", "—"),
"Saldırganlık": round(float(details.get("off_score", 0.0)), 4),
"Nefret": round(float(details.get("detox", {}).get("identity_attack", 0.0)), 4),
"Tehdit": round(float(details.get("threat", details.get("detox", {}).get("threat", 0.0))), 4),
"Hits": ", ".join(hits_all) if hits_all else "",
}
)
progress.progress(1.0)
status_text.markdown(
f"""<span style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#4a6080;">{len(results)} / {len(df)} işlendi</span>""",
unsafe_allow_html=True,
)
elapsed = time.time() - t0
res_df = pd.DataFrame(results)
if len(df) > 0:
st.session_state["last_latency_ms"] = (elapsed * 1000.0) / len(df)
status_text.empty()
progress.empty()
st.markdown(
f"""<div style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:#2ea84a;margin:12px 0;">
{len(df)} satır {elapsed:.1f}s içinde analiz edildi</div>""",
unsafe_allow_html=True,
)
counts = res_df["Karar"].value_counts()
karar_colors_ui = {
"TEMIZ": "#2ea84a",
"CLEAR": "#2ea84a",
"KÜFÜR": "#d4a017",
"KUFUR": "#d4a017",
"PROFANITY": "#d4a017",
"SALDIRGAN": "#d4a017",
"TOXIC": "#d4a017",
"NEFRET": "#e07020",
"IDENTITY": "#e07020",
"İNCELEME": "#3a7bd4",
"INCELEME": "#3a7bd4",
"REVIEW": "#3a7bd4",
"SPAM": "#8030d4",
"GİBBERİSH": "#8030d4",
}
cols_summary = st.columns(min(len(counts), 6))
for i, (karar, cnt) in enumerate(counts.items()):
if i < 6:
vc = next((v for k, v in karar_colors_ui.items() if k in karar.upper()), "#888888")
with cols_summary[i]:
st.markdown(
f"""<div class="metric-card" style="text-align:center;">
<div class="summary-count" style="color:{vc}">{cnt}</div>
<div class="summary-label">{karar[:18]}</div>
</div>""",
unsafe_allow_html=True,
)
st.markdown("<br>", unsafe_allow_html=True)
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;">Detaylı Analiz Tablosu</div>""",
unsafe_allow_html=True,
)
table_rows = ""
for idx, row in res_df.iterrows():
risk_str = str(row.get("Risk", "")).upper()
row_bg = {
"CRITICAL": "#1f0c0c",
"HIGH": "#1a0e03",
"MEDIUM": "#141002",
"LOW": "#07091a",
"NONE": "#050f07",
}.get(risk_str, "#0d1220")
karar_str = str(row.get("Karar", ""))
kc = next((v for k, v in karar_colors_ui.items() if k in karar_str.upper()), "#888888")
sal = float(row.get("Saldırganlık", 0.0))
nef = float(row.get("Nefret", 0.0))
thr = float(row.get("Tehdit", 0.0))
hits_str = str(row.get("Hits", "")).strip()
hits_html = ""
if hits_str:
for h in hits_str.split(","):
h = h.strip()
if h:
hits_html += f'<span class="hits-tag">{h}</span>'
else:
hits_html = '<span style="color:#2a3d55;font-size:10px;">—</span>'
metin_full = str(row.get("Metin", ""))
metin_short = metin_full[:60] + "..." if len(metin_full) > 60 else metin_full
normalize = str(row.get("Normalize", ""))[:50]
table_rows += f"""
<tr style="background:{row_bg}">
<td style="color:#2a3d55;text-align:center;font-size:11px;">{idx + 1}</td>
<td class="metin-cell" title="{metin_full}">{metin_short}</td>
<td style="color:#4a6080;font-size:10px;font-style:italic;">{normalize}</td>
<td class="karar-cell" style="color:{kc}">{karar_str[:22]}</td>
<td>{badge_html(risk_str)}</td>
<td class="skor-cell">{inline_bar_html(sal, risk_color(sal))}</td>
<td class="skor-cell">{inline_bar_html(nef, risk_color(nef))}</td>
<td class="skor-cell">{inline_bar_html(thr, risk_color(thr))}</td>
<td>{hits_html}</td>
<td style="color:#4a6080;font-size:10px;max-width:180px;">{str(row.get("Gerekçe", ""))[:60]}</td>
</tr>"""
st.markdown(
f"""
<div style="overflow-x:auto;overflow-y:auto;max-height:520px;border:1px solid #1e2d45;border-radius:10px;">
<table class="report-table">
<thead>
<tr>
<th>#</th><th>Metin</th><th>Normalize</th><th>Karar</th>
<th>Risk</th><th>Saldırganlık</th><th>Nefret</th><th>Tehdit</th>
<th>Hits</th><th>Gerekçe</th>
</tr>
</thead>
<tbody>{table_rows}</tbody>
</table>
</div>""",
unsafe_allow_html=True,
)
st.markdown("<br>", unsafe_allow_html=True)
col_chart, col_stats = st.columns([1, 1])
with col_chart:
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;">Dağılım</div>""",
unsafe_allow_html=True,
)
st.bar_chart(counts)
with col_stats:
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;">İstatistikler</div>""",
unsafe_allow_html=True,
)
total = len(res_df)
zararli = total - len(res_df[res_df["Karar"].str.contains("TEMİZ|CLEAR", na=False)])
st.markdown(
f"""
<div style="font-family:'IBM Plex Mono',monospace;font-size:13px;line-height:2.2;color:#8a9bc0;">
<span style="color:#4a6080">Toplam kayıt </span> {total}<br>
<span style="color:#4a6080">Zararlı içerik</span> <span style="color:#e03030">{zararli}</span> (%{zararli / total * 100:.1f})<br>
<span style="color:#4a6080">Ortalama süre </span> {elapsed / total * 1000:.0f}ms / satır<br>
<span style="color:#4a6080">Hits bulundu </span> {len(res_df[res_df['Hits'].str.len() > 0])} kayıt<br>
<span style="color:#4a6080">İnceleme kuyruğu</span> {len(res_df[res_df['Karar'].str.contains('İNCELEME|INCELEME', na=False)])} içerik
</div>""",
unsafe_allow_html=True,
)
st.markdown("<br>", unsafe_allow_html=True)
inceleme = res_df[res_df["Karar"].str.contains("İNCELEME|INCELEME|REVIEW", na=False)]
if len(inceleme):
st.markdown(
f"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#3a7bd4;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;">İnceleme Kuyruğu — {len(inceleme)} İçerik</div>""",
unsafe_allow_html=True,
)
for i, (_, row) in enumerate(inceleme.iterrows()):
sal = float(row.get("Saldırganlık", 0.0))
st.markdown(
f"""<div class="queue-card">
<div class="queue-index">{i + 1:02d}</div>
<div>
<div class="queue-text">{str(row.get('Metin', ''))}</div>
<div class="queue-meta">
Risk: {row.get('Risk', '')} &nbsp;|&nbsp;
Saldırganlık: %{sal * 100:.0f} &nbsp;|&nbsp;
{row.get('Gerekçe', '')}
</div>
</div>
</div>""",
unsafe_allow_html=True,
)
st.markdown("<br>", unsafe_allow_html=True)
st.markdown(
"""<div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:#4a6080;text-transform:uppercase;letter-spacing:1px;margin-bottom:12px;">Raporu İndir</div>""",
unsafe_allow_html=True,
)
col_dl1, col_dl2, _ = st.columns([1, 1, 4])
with col_dl1:
csv_bytes = res_df.to_csv(index=False).encode("utf-8")
st.download_button(
"⬇ CSV",
data=csv_bytes,
file_name=f"sentinel_raporu_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
mime="text/csv",
use_container_width=True,
)
with col_dl2:
docx_buf = generate_docx_report(res_df, elapsed, platform_dil)
if docx_buf:
st.download_button(
"⬇ DOCX",
data=docx_buf,
file_name=f"sentinel_raporu_{datetime.now().strftime('%Y%m%d_%H%M')}.docx",
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
use_container_width=True,
)
else:
st.warning("python-docx yüklü değil: pip install python-docx")