GoshawkVortexAI commited on
Commit
99e99ba
·
verified ·
1 Parent(s): e15715d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +565 -0
app.py ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import urllib.request
4
+ import xml.etree.ElementTree as ET
5
+ from email.utils import parsedate_to_datetime
6
+ from urllib.parse import quote
7
+
8
+ import gradio as gr
9
+ import pandas as pd
10
+ import torch
11
+ from peft import PeftModel
12
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
13
+
14
+ # ================================================================
15
+ # CONFIG
16
+ # ================================================================
17
+
18
+ BASE_MODEL = "distilbert-base-uncased"
19
+ ADAPTER_PATH = "."
20
+ MAX_LENGTH = 128
21
+ BATCH_SIZE = 32
22
+
23
+ LABELS = {0: "negative", 1: "neutral", 2: "positive"}
24
+
25
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
26
+
27
+ # ================================================================
28
+ # MODEL LOADING (DistilBERT + LoRA)
29
+ # ================================================================
30
+
31
+ print("Device:", DEVICE)
32
+
33
+ tokenizer = AutoTokenizer.from_pretrained(ADAPTER_PATH, use_fast=True)
34
+
35
+ base_model = AutoModelForSequenceClassification.from_pretrained(
36
+ BASE_MODEL,
37
+ num_labels=3,
38
+ id2label=LABELS,
39
+ label2id={v: k for k, v in LABELS.items()},
40
+ ignore_mismatched_sizes=True,
41
+ )
42
+
43
+ model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)
44
+ try:
45
+ model = model.merge_and_unload()
46
+ except Exception as e:
47
+ print("Merge skipped:", e)
48
+
49
+ model = model.to(DEVICE)
50
+ model.eval()
51
+ print("Model ready.")
52
+
53
+
54
+ # ================================================================
55
+ # SENTIMENT (batch)
56
+ # ================================================================
57
+
58
+ def predict_batch(texts):
59
+ """Her metin için (p_neg, p_neu, p_pos) döndürür."""
60
+ out = []
61
+ for i in range(0, len(texts), BATCH_SIZE):
62
+ chunk = texts[i:i + BATCH_SIZE]
63
+ inputs = tokenizer(
64
+ chunk,
65
+ return_tensors="pt",
66
+ truncation=True,
67
+ max_length=MAX_LENGTH,
68
+ padding=True,
69
+ )
70
+ inputs = {k: v.to(DEVICE) for k, v in inputs.items()}
71
+ with torch.inference_mode():
72
+ probs = torch.softmax(model(**inputs).logits, dim=-1).cpu().tolist()
73
+ out.extend(probs)
74
+ return out
75
+
76
+
77
+ # ================================================================
78
+ # RISK KURALLARI (anahtar kelime tabanlı sinyaller)
79
+ # ================================================================
80
+
81
+ RISK_SIGNALS = {
82
+ "Hasar / Claim": [
83
+ "claim", "claims", "payout", "payouts", "loss", "losses",
84
+ "combined ratio", "reserve", "reserves",
85
+ ],
86
+ "Doğal afet": [
87
+ "hurricane", "earthquake", "flood", "flooding", "wildfire", "storm",
88
+ "catastrophe", "cyclone", "typhoon", "tornado", "hailstorm", "drought",
89
+ ],
90
+ "Büyük kaza": [
91
+ "crash", "explosion", "collapse", "derailment", "blast", "accident",
92
+ "fire", "sinking", "spill",
93
+ ],
94
+ "Dava / Fraud": [
95
+ "lawsuit", "sued", "litigation", "fraud", "fine", "fined", "penalty",
96
+ "investigation", "probe", "settlement", "class action", "regulator",
97
+ ],
98
+ "Finansal stres": [
99
+ "downgrade", "downgraded", "profit warning", "solvency", "writedown",
100
+ "write-down", "impairment", "insolvency", "bankruptcy", "capital shortfall",
101
+ "negative outlook", "slump", "plunge",
102
+ ],
103
+ "Siber / Operasyonel": [
104
+ "cyberattack", "cyber attack", "data breach", "breach", "ransomware",
105
+ "outage",
106
+ ],
107
+ }
108
+
109
+ TOPICS = {
110
+ "Motor insurance": ["motor", "auto insurance", "car insurance", "vehicle", "automobile"],
111
+ "Health insurance": ["health insurance", "health", "medical", "hospital"],
112
+ "Catastrophe risk": ["catastrophe", "hurricane", "earthquake", "flood", "wildfire",
113
+ "storm", "cyclone", "typhoon", "tornado", "hailstorm"],
114
+ "Life insurance": ["life insurance", "life insurer", "annuity", "annuities", "pension", "mortality"],
115
+ "Reinsurance": ["reinsur", "retrocession", "cat bond", "catastrophe bond"],
116
+ "Property": ["property insurance", "homeowner", "home insurance", "real estate", "fire"],
117
+ "Cyber": ["cyber", "ransomware", "data breach"],
118
+ }
119
+
120
+ COMPANIES = [
121
+ "Allianz", "AXA", "Zurich", "Generali", "Aviva", "Munich Re", "Swiss Re",
122
+ "Hannover Re", "Chubb", "AIG", "MetLife", "Prudential", "Allstate",
123
+ "Progressive", "State Farm", "Mapfre", "Ageas", "Lloyd's", "Travelers",
124
+ "Liberty Mutual", "Tokio Marine", "Ping An",
125
+ ]
126
+
127
+
128
+ def _compile(words):
129
+ return re.compile(r"\b(?:%s)" % "|".join(re.escape(w) for w in words), re.I)
130
+
131
+
132
+ SIGNAL_RE = {cat: _compile(words) for cat, words in RISK_SIGNALS.items()}
133
+ TOPIC_RE = {t: _compile(words) for t, words in TOPICS.items()}
134
+ COMPANY_RE = {c: re.compile(r"\b%s\b" % re.escape(c), re.I) for c in COMPANIES}
135
+
136
+
137
+ def find_signals(text):
138
+ hits = []
139
+ for cat, rx in SIGNAL_RE.items():
140
+ m = rx.findall(text)
141
+ if m:
142
+ hits.append((cat, sorted({x.lower() for x in m})[:2]))
143
+ return hits
144
+
145
+
146
+ def find_topics(text):
147
+ return [t for t, rx in TOPIC_RE.items() if rx.search(text)]
148
+
149
+
150
+ def find_companies(text):
151
+ return [c for c, rx in COMPANY_RE.items() if rx.search(text)]
152
+
153
+
154
+ def risk_level(score, high=60, medium=35):
155
+ if score >= high:
156
+ return "🔴 HIGH"
157
+ if score >= medium:
158
+ return "🟠 MEDIUM"
159
+ return "🟢 LOW"
160
+
161
+
162
+ # ================================================================
163
+ # TEK HABER ANALİZİ
164
+ # ================================================================
165
+
166
+ def score_item(text, probs):
167
+ p_neg, p_neu, p_pos = probs
168
+ sentiment = p_pos - p_neg # -1 .. +1
169
+ signals = find_signals(text)
170
+ n_sig = len(signals)
171
+ # Risk indeksi: %65 negatif olasılık + %35 risk sinyali yoğunluğu
172
+ risk = 100 * (0.65 * p_neg + 0.35 * min(n_sig, 3) / 3)
173
+ label = LABELS[max(range(3), key=lambda i: probs[i])]
174
+ reason = "; ".join(f"{c} ({', '.join(k)})" for c, k in signals) or "-"
175
+ return {
176
+ "label": label,
177
+ "sentiment": round(sentiment, 3),
178
+ "signals": n_sig,
179
+ "risk_score": round(risk, 1),
180
+ "risk": risk_level(risk),
181
+ "reason": reason,
182
+ }
183
+
184
+
185
+ def analyze_news(text):
186
+ text = (text or "").strip()
187
+ if not text:
188
+ return "Lütfen bir sigorta haber başlığı girin.", "", ""
189
+
190
+ probs = predict_batch([text])[0]
191
+ s = score_item(text, probs)
192
+
193
+ companies = find_companies(text) or ["-"]
194
+ topics = find_topics(text) or ["-"]
195
+
196
+ result = (
197
+ f"Sentiment: {s['label'].upper()}\n"
198
+ f"Skor: {s['sentiment']:+.2f}\n"
199
+ f"Güven: {max(probs) * 100:.1f}%"
200
+ )
201
+ prob_text = (
202
+ f"Negative: {probs[0] * 100:.1f}%\n"
203
+ f"Neutral: {probs[1] * 100:.1f}%\n"
204
+ f"Positive: {probs[2] * 100:.1f}%"
205
+ )
206
+ risk_text = (
207
+ f"Şirket: {', '.join(companies)}\n"
208
+ f"Risk: {s['risk']} ({s['risk_score']}/100)\n"
209
+ f"Risk sinyali: {s['signals']}\n"
210
+ f"Neden: {s['reason']}\n"
211
+ f"Konu: {', '.join(topics)}"
212
+ )
213
+ return result, prob_text, risk_text
214
+
215
+
216
+ # ================================================================
217
+ # TOPLU ANALİZ PIPELINE
218
+ # ================================================================
219
+
220
+ def analyze_items(items):
221
+ """items: [{'headline':str, 'date':datetime|None, 'company':str|None}]"""
222
+ if not items:
223
+ return None, None, None
224
+
225
+ texts = [it["headline"] for it in items]
226
+ probs = predict_batch(texts)
227
+
228
+ rows = []
229
+ for it, p in zip(items, probs):
230
+ s = score_item(it["headline"], p)
231
+ companies = [it["company"]] if it.get("company") else find_companies(it["headline"])
232
+ rows.append({
233
+ "date": it.get("date"),
234
+ "headline": it["headline"],
235
+ "companies": companies or ["(Sektör)"],
236
+ "topics": find_topics(it["headline"]),
237
+ **s,
238
+ })
239
+ df = pd.DataFrame(rows)
240
+
241
+ has_dates = df["date"].notna().sum() >= 4
242
+ if has_dates:
243
+ df["date"] = pd.to_datetime(df["date"], utc=True, errors="coerce")
244
+ mid = df["date"].dropna().sort_values().iloc[len(df["date"].dropna()) // 2]
245
+ df["recent"] = df["date"] >= mid
246
+ else:
247
+ df["recent"] = False
248
+
249
+ # ---- Şirket tablosu ----
250
+ ex = df.explode("companies")
251
+ comp_rows = []
252
+ for comp, g in ex.groupby("companies"):
253
+ risk = g["risk_score"].mean()
254
+ trend = "—"
255
+ if has_dates:
256
+ r, e = g[g["recent"]], g[~g["recent"]]
257
+ if len(r) and len(e):
258
+ d = r["sentiment"].mean() - e["sentiment"].mean()
259
+ trend = ("Improving ↑" if d > 0.15
260
+ else "Deteriorating ↓" if d < -0.15 else "Stable →")
261
+ comp_rows.append({
262
+ "Company": comp,
263
+ "News": len(g),
264
+ "Avg sentiment": round(g["sentiment"].mean(), 2),
265
+ "Risk signals": int(g["signals"].sum()),
266
+ "High-risk news": int((g["risk_score"] >= 60).sum()),
267
+ "Risk score": round(risk, 1),
268
+ "Risk": risk_level(risk, high=55, medium=30),
269
+ "Trend": trend,
270
+ })
271
+ comp_df = pd.DataFrame(comp_rows).sort_values("Risk score", ascending=False)
272
+
273
+ # ---- Konu / trend tablosu ----
274
+ topic_rows = []
275
+ n_recent = max(int(df["recent"].sum()), 1)
276
+ n_early = max(int((~df["recent"]).sum()), 1)
277
+ for topic in TOPICS:
278
+ mask = df["topics"].apply(lambda t: topic in t)
279
+ if not mask.any():
280
+ continue
281
+ arrow = "—"
282
+ if has_dates:
283
+ share_r = (mask & df["recent"]).sum() / n_recent
284
+ share_e = (mask & ~df["recent"]).sum() / n_early
285
+ if share_e == 0:
286
+ arrow = "↑↑"
287
+ else:
288
+ ratio = share_r / share_e
289
+ arrow = ("↑↑" if ratio >= 1.5 else "↑" if ratio >= 1.15
290
+ else "↓" if ratio <= 0.8 else "→")
291
+ topic_rows.append({
292
+ "Topic": topic,
293
+ "News": int(mask.sum()),
294
+ "Avg sentiment": round(df.loc[mask, "sentiment"].mean(), 2),
295
+ "Trend": arrow,
296
+ })
297
+ topic_df = pd.DataFrame(topic_rows).sort_values("News", ascending=False) \
298
+ if topic_rows else pd.DataFrame(columns=["Topic", "News", "Avg sentiment", "Trend"])
299
+
300
+ # ---- Haber tablosu ----
301
+ news_df = df.assign(
302
+ companies=df["companies"].apply(", ".join),
303
+ topics=df["topics"].apply(", ".join),
304
+ date=df["date"].apply(lambda d: d.strftime("%Y-%m-%d") if pd.notna(d) else ""),
305
+ )[["date", "companies", "headline", "label", "sentiment", "risk", "risk_score", "reason", "topics"]]
306
+ news_df = news_df.sort_values("risk_score", ascending=False)
307
+
308
+ return news_df, comp_df, topic_df
309
+
310
+
311
+ # ================================================================
312
+ # TOPLU GİRİŞ (metin / CSV)
313
+ # ================================================================
314
+
315
+ def _pick(cols, names):
316
+ for n in names:
317
+ if n in cols:
318
+ return n
319
+ return None
320
+
321
+
322
+ def batch_analyze(text, file):
323
+ items = []
324
+
325
+ path = file if isinstance(file, str) else getattr(file, "name", None)
326
+ if path:
327
+ df = pd.read_csv(path)
328
+ df.columns = [c.strip().lower() for c in df.columns]
329
+ h = _pick(df.columns, ["headline", "title", "text", "news", "haber", "baslik"])
330
+ d = _pick(df.columns, ["date", "published", "pubdate", "tarih"])
331
+ c = _pick(df.columns, ["company", "sirket", "şirket"])
332
+ if h is None:
333
+ raise gr.Error("CSV'de 'headline' (veya title/text/news) kolonu bulunamadı.")
334
+ for _, r in df.iterrows():
335
+ if pd.isna(r[h]) or not str(r[h]).strip():
336
+ continue
337
+ items.append({
338
+ "headline": str(r[h]).strip(),
339
+ "date": pd.to_datetime(r[d], utc=True, errors="coerce") if d else None,
340
+ "company": str(r[c]).strip() if c and pd.notna(r[c]) else None,
341
+ })
342
+
343
+ for line in (text or "").splitlines():
344
+ line = line.strip()
345
+ if line:
346
+ items.append({"headline": line, "date": None, "company": None})
347
+
348
+ if not items:
349
+ raise gr.Error("Haber girin (her satıra bir başlık) veya CSV yükleyin.")
350
+ return analyze_items(items)
351
+
352
+
353
+ # ================================================================
354
+ # CANLI TARAMA (Google News RSS)
355
+ # ================================================================
356
+
357
+ def fetch_news(company, days, limit):
358
+ q = quote(f'"{company}" insurance when:{int(days)}d')
359
+ url = f"https://news.google.com/rss/search?q={q}&hl=en-US&gl=US&ceid=US:en"
360
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
361
+ with urllib.request.urlopen(req, timeout=15) as resp:
362
+ root = ET.fromstring(resp.read())
363
+
364
+ items = []
365
+ for it in root.iter("item"):
366
+ title = (it.findtext("title") or "").strip()
367
+ title = re.sub(r"\s+-\s+[^-]+$", "", title) # " - Kaynak" kısmını at
368
+ try:
369
+ date = parsedate_to_datetime(it.findtext("pubDate"))
370
+ except Exception:
371
+ date = None
372
+ if title:
373
+ items.append({"headline": title, "date": date, "company": company})
374
+ if len(items) >= limit:
375
+ break
376
+ return items
377
+
378
+
379
+ def live_scan(company_text, days, limit):
380
+ companies = [c.strip() for c in (company_text or "").split(",") if c.strip()]
381
+ if not companies:
382
+ raise gr.Error("En az bir şirket adı girin.")
383
+
384
+ items, errors = [], []
385
+ for c in companies:
386
+ try:
387
+ items += fetch_news(c, days, int(limit))
388
+ except Exception as e:
389
+ errors.append(f"{c}: {e}")
390
+
391
+ if not items:
392
+ raise gr.Error("Haber alınamadı. İnternet erişimini kontrol edin. " + " | ".join(errors))
393
+ return analyze_items(items)
394
+
395
+
396
+ # ================================================================
397
+ # GRADIO UI
398
+ # ================================================================
399
+
400
+ EXAMPLES = [
401
+ ["Insurance company reports record premium growth"],
402
+ ["Allianz faces major claims increase after severe floods"],
403
+ ["Zurich maintained stable operations in the third quarter."],
404
+ ["AXA downgraded after reserve shortfall and fraud investigation"],
405
+ ["Hurricane losses push reinsurance rates higher"],
406
+ ]
407
+
408
+ BATCH_PLACEHOLDER = (
409
+ "Allianz reports record premium growth\n"
410
+ "Generali hit by wildfire claims surge\n"
411
+ "Zurich faces lawsuit over auto claims"
412
+ )
413
+
414
+ NEWS_COLS = ["date", "companies", "headline", "label", "sentiment", "risk", "risk_score", "reason", "topics"]
415
+
416
+ BRAND = "Goshawk_insurance_bert"
417
+
418
+ LOGO_SVG = """
419
+ <svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-label="Goshawk logo">
420
+ <defs>
421
+ <linearGradient id="gs" x1="0" y1="0" x2="0" y2="1">
422
+ <stop offset="0" stop-color="#1E4F9C"/><stop offset="1" stop-color="#0B1F3A"/>
423
+ </linearGradient>
424
+ </defs>
425
+ <path d="M32 3 L57 12 V32 C57 47 46 57 32 61 C18 57 7 47 7 32 V12 Z"
426
+ fill="url(#gs)" stroke="#F2B134" stroke-width="2.5" stroke-linejoin="round"/>
427
+ <path d="M15 37 C17 25 27 16 40 18 L51 24 L42 27 C45 32 43 38 38 41 L32 51 L28 43 C22 43 17 41 15 37 Z"
428
+ fill="#FFFFFF"/>
429
+ <path d="M51 24 L42 27 L45 33 Z" fill="#F2B134"/>
430
+ <path d="M22 27 L36 23 L35 26 L24 30 Z" fill="#0B1F3A" opacity=".85"/>
431
+ <circle cx="35" cy="29" r="2.2" fill="#F2B134"/>
432
+ <circle cx="35.4" cy="29" r="1" fill="#0B1F3A"/>
433
+ </svg>
434
+ """
435
+
436
+ CSS = """
437
+ .gradio-container {max-width: 1100px !important; margin: auto;
438
+ font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;}
439
+ #brand-header {display:flex; align-items:center; gap:16px; padding:20px 22px;
440
+ border-radius:18px; color:#fff; margin-bottom:10px;
441
+ background: linear-gradient(135deg, #0B1F3A 0%, #1E4F9C 70%, #2B6CC4 100%);
442
+ box-shadow: 0 6px 20px rgba(11,31,58,.25);}
443
+ #brand-header .logo {flex:0 0 auto; filter: drop-shadow(0 2px 6px rgba(0,0,0,.35));}
444
+ #brand-header .title {font-size:1.55rem; font-weight:800; letter-spacing:.2px; line-height:1.15;}
445
+ #brand-header .title span {color:#F2B134;}
446
+ #brand-header .sub {opacity:.85; font-size:.92rem; margin-top:4px;}
447
+ #brand-header .badges {margin-top:10px; display:flex; flex-wrap:wrap; gap:6px;}
448
+ #brand-header .badge {background:rgba(255,255,255,.14); border:1px solid rgba(255,255,255,.25);
449
+ padding:3px 10px; border-radius:999px; font-size:.74rem; backdrop-filter: blur(4px);}
450
+ .legend {display:flex; flex-wrap:wrap; gap:8px; margin:4px 0 12px;}
451
+ .legend .chip {padding:4px 12px; border-radius:999px; font-size:.8rem; font-weight:600;
452
+ border:1px solid rgba(128,128,128,.3);}
453
+ .chip.high {background:#fde8e8; color:#a31212;}
454
+ .chip.med {background:#fff1dc; color:#9a5a00;}
455
+ .chip.low {background:#e3f6e8; color:#14702d;}
456
+ button.primary {background: linear-gradient(135deg,#1E4F9C,#0B1F3A) !important;
457
+ border:none !important; color:#fff !important; font-weight:700 !important;
458
+ border-radius:12px !important;}
459
+ button.primary:hover {filter: brightness(1.15);}
460
+ .tab-nav button, button[role="tab"] {font-weight:600 !important;}
461
+ textarea, input[type="text"] {border-radius:10px !important;}
462
+ #footer {text-align:center; font-size:.82rem; opacity:.75; padding:10px 0 4px;}
463
+ @media (max-width: 640px) {
464
+ #brand-header {padding:14px; gap:12px;}
465
+ #brand-header .title {font-size:1.2rem;}
466
+ #brand-header .logo svg {width:48px; height:48px;}
467
+ }
468
+ """
469
+
470
+ HEADER_HTML = f"""
471
+ <div id="brand-header">
472
+ <div class="logo">{LOGO_SVG}</div>
473
+ <div>
474
+ <div class="title">Goshawk<span>_</span>insurance<span>_</span>bert</div>
475
+ <div class="sub">Sigorta haberleri için sentiment, risk sinyali ve erken uyarı sistemi</div>
476
+ <div class="badges">
477
+ <span class="badge">DistilBERT + LoRA</span>
478
+ <span class="badge">Macro F1 78.06%</span>
479
+ <span class="badge">Early Warning</span>
480
+ <span class="badge">EN headlines</span>
481
+ </div>
482
+ </div>
483
+ </div>
484
+ <div class="legend">
485
+ <span class="chip high">🔴 HIGH RISK</span>
486
+ <span class="chip med">🟠 MEDIUM RISK</span>
487
+ <span class="chip low">🟢 LOW RISK</span>
488
+ </div>
489
+ """
490
+
491
+ with gr.Blocks(
492
+ title=BRAND,
493
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="amber", neutral_hue="slate"),
494
+ css=CSS,
495
+ ) as demo:
496
+
497
+ gr.HTML(HEADER_HTML)
498
+
499
+ # ---------------- TAB 1 ----------------
500
+ with gr.Tab("📰 Tek Haber"):
501
+ with gr.Row():
502
+ with gr.Column():
503
+ text_input = gr.Textbox(label="Insurance News", lines=4,
504
+ placeholder="Allianz faces major claims increase...")
505
+ btn = gr.Button("🔍 Analiz Et", variant="primary")
506
+ with gr.Column():
507
+ out_pred = gr.Textbox(label="Sentiment", lines=3)
508
+ out_prob = gr.Textbox(label="Olasılıklar", lines=3)
509
+ out_risk = gr.Textbox(label="Risk Raporu", lines=5)
510
+ outs = [out_pred, out_prob, out_risk]
511
+ btn.click(analyze_news, text_input, outs)
512
+ text_input.submit(analyze_news, text_input, outs)
513
+ gr.Examples(EXAMPLES, inputs=text_input)
514
+
515
+ # ---------------- TAB 2 ----------------
516
+ with gr.Tab("📚 Toplu Analiz"):
517
+ gr.Markdown(
518
+ "Her satıra bir başlık yazın **veya** CSV yükleyin "
519
+ "(kolonlar: `headline`, opsiyonel `date`, `company`). "
520
+ "Tarih varsa **trend** hesaplanır."
521
+ )
522
+ b_text = gr.Textbox(label="Başlıklar (satır başına bir haber)", lines=6,
523
+ placeholder=BATCH_PLACEHOLDER)
524
+ b_file = gr.File(label="CSV (opsiyonel)", file_types=[".csv"])
525
+ b_btn = gr.Button("🚀 Toplu Analiz", variant="primary")
526
+ gr.Markdown("### 🏢 Şirket Risk Tablosu (Underwriting / Rakip İstihbaratı)")
527
+ b_comp = gr.Dataframe(wrap=True)
528
+ gr.Markdown("### 📈 Sektör Konu Trendi")
529
+ b_topic = gr.Dataframe(wrap=True)
530
+ gr.Markdown("### 🗞️ Haber Detayı (riskten düşüğe)")
531
+ b_news = gr.Dataframe(wrap=True)
532
+ b_btn.click(batch_analyze, [b_text, b_file], [b_news, b_comp, b_topic])
533
+
534
+ # ---------------- TAB 3 ----------------
535
+ with gr.Tab("🌐 Canlı Tarama"):
536
+ gr.Markdown(
537
+ "Google News RSS'ten şirket haberlerini çekip otomatik analiz eder "
538
+ "(sunucunun internet erişimi gerekir)."
539
+ )
540
+ l_comp = gr.Textbox(label="Şirketler (virgülle)", value="Allianz, AXA, Zurich, Generali")
541
+ with gr.Row():
542
+ l_days = gr.Slider(7, 90, value=30, step=1, label="Son kaç gün")
543
+ l_limit = gr.Slider(5, 50, value=20, step=1, label="Şirket başına haber")
544
+ l_btn = gr.Button("🔎 Haberleri Tara", variant="primary")
545
+ gr.Markdown("### 🏢 Rakip Karşılaştırma")
546
+ l_compdf = gr.Dataframe(wrap=True)
547
+ gr.Markdown("### 📈 Konu Trendi")
548
+ l_topic = gr.Dataframe(wrap=True)
549
+ gr.Markdown("### 🗞️ Haberler")
550
+ l_news = gr.Dataframe(wrap=True)
551
+ l_btn.click(live_scan, [l_comp, l_days, l_limit], [l_news, l_compdf, l_topic])
552
+
553
+ gr.HTML('<div id="footer">🦅 <b>Goshawk_insurance_bert</b> · Karar destek aracıdır</div>')
554
+ gr.Markdown(
555
+ "**Not:** Risk skoru = %65 model negatif olasılığı + %35 anahtar-kelime risk sinyali. "
556
+ "Karar destek amaçlıdır; tek başına underwriting kararı için kullanılmamalıdır."
557
+ )
558
+
559
+
560
+ if __name__ == "__main__":
561
+ demo.launch(
562
+ server_name="0.0.0.0",
563
+ server_port=int(os.environ.get("PORT", 7860)),
564
+ show_error=True,
565
+ )