Engin34 commited on
Commit
ce1bf09
Β·
verified Β·
1 Parent(s): 92059f2

update app.py 16may

Browse files
Files changed (1) hide show
  1. app.py +391 -300
app.py CHANGED
@@ -1,20 +1,9 @@
1
- """
2
- LIFT UP Taksonomi SΔ±nΔ±flandΔ±rΔ±cΔ± β€” Backend API v2
3
- Hugging Face Spaces (Docker) üzerinde çalışır.
4
-
5
- YENΔ°LΔ°KLER v2:
6
- - Gemini 2.5 Flash entegrasyonu (opsiyonel, GEMINI_API_KEY secret ile)
7
- - IDF ağırlıklı, title-boost hibrit sınıflandırıcı
8
- - Gemini keyword Γ§Δ±karma + sΔ±nΔ±flandΔ±rma pipeline
9
- - Geriye dΓΆnΓΌk uyumlu aynΔ± /classify endpoint
10
- """
11
-
12
  import os, re, time, math, unicodedata, logging
13
  from contextlib import asynccontextmanager
14
- from collections import Counter, defaultdict
15
  from dataclasses import dataclass, field
16
  from typing import Dict, List, Optional
17
-
18
  import numpy as np
19
  import torch
20
  import torch.nn as nn
@@ -22,117 +11,150 @@ from fastapi import FastAPI, HTTPException
22
  from fastapi.middleware.cors import CORSMiddleware
23
  from pydantic import BaseModel
24
  from transformers import AutoTokenizer, AutoModel, AutoModelForSeq2SeqLM
25
- from sentence_transformers import SentenceTransformer
26
  from keybert import KeyBERT
27
  from huggingface_hub import hf_hub_download, snapshot_download
28
-
29
  logging.basicConfig(level=logging.INFO)
30
  log = logging.getLogger("liftup")
31
-
32
- HF_USERNAME = os.getenv("HF_USERNAME", "Engin34")
33
- HF_TOKEN = os.getenv("HF_TOKEN", "")
34
- GEMINI_KEY = os.getenv("GEMINI_API_KEY", "")
35
-
36
- # ── Global değişkenler ────────────────────────────────────────────────
37
  bert_model = None
38
  bert_tok = None
39
  kw_model = None
40
  generator = None
41
  clf = None
42
  TOP_KEYWORDS = None
43
- gemini_model = None
44
- GEMINI_OK = False
45
  ALL_CATS = []
46
- device = torch.device("cpu")
47
-
48
-
49
- # ═══════════════════════════════════════════════════════════════════
50
- # TAKSONOMΔ° PARSER
51
- # ═══════════════════════════════════════════════════════════════════
52
  def _temizle(k):
53
- k = k.replace('\u200b','').replace('\ufeff','')
54
  k = unicodedata.normalize('NFKC', k)
55
- return re.sub(r'\s+',' ', k).strip().lower()
56
-
57
  def _parantez_ayir(k):
58
  m = re.match(r'^(.+?)\s*\((.+?)\)\s*$', k)
59
- if not m: return [k]
 
60
  ana, ic = m.group(1).strip(), m.group(2).strip()
61
- if any(a in ic.lower() for a in ['bağlam','proses','analiz','anahtarları']):
62
  return [ana]
63
  return [ana] + [p.strip() for p in ic.split('/') if p.strip()]
64
-
65
  def _virgul_ayir(metin):
66
  sonuc, buf, d = [], [], 0
67
  for c in metin:
68
- if c == '(': d += 1; buf.append(c)
69
- elif c == ')': d -= 1; buf.append(c)
70
- elif c == ',' and d == 0: sonuc.append(''.join(buf)); buf = []
71
- else: buf.append(c)
72
- if buf: sonuc.append(''.join(buf))
 
 
 
 
 
 
 
 
73
  return sonuc
74
-
75
  def parse_taksonomi(icerik: str) -> Dict:
76
- icerik = icerik.replace('\u200b','')
77
  matches = list(re.finditer(r'^\s*(\d+)\)\s+(.+?)\s*$', icerik, re.MULTILINE))
78
  tax = {}
79
  for i, m in enumerate(matches):
80
  kat = m.group(2).strip()
81
  govde = icerik[m.end():(matches[i+1].start() if i+1 < len(matches) else len(icerik))].strip()
82
  pm = re.search(r'\((.+)\)', govde, re.DOTALL)
83
- if not pm: continue
 
84
  kw_set = set()
85
  for parca in _virgul_ayir(pm.group(1)):
86
  for alt in _parantez_ayir(parca.strip()):
87
  for k in re.split(r'[/]', alt):
88
  temiz = _temizle(k)
89
- if len(temiz) >= 2: kw_set.add(temiz)
 
90
  tax[kat] = {'keywords': kw_set}
91
  return tax
92
-
93
-
94
- # ═══════════════════════════════════════════════════════════════════
95
- # GELİŞMİŞ HİBRİT SINIFLANDIRICI (IDF + title boost)
96
- # ═══════════════════════════════════════════════════════════════════
97
  _EKSTRA_KW = {
98
- 'Yapay ZekΓ’ & Veri Bilimi': {
99
- 'derin âğrenme','sinir ağı','transformer','lstm','gru',
100
- 'makine âğrenmesi','yapay sinir','classification','regresyon',
101
- 'veri madenciliği','takviyeli âğrenme','xgboost',
102
  },
103
- 'YapΔ±lar, Mukavemet & Analiz': {
104
- 'sonlu elemanlar','fea','burklanma','yorulma','hasar',
105
- 'gerilme','deformasyon','mod analizi','titreşim','nastran',
106
- 'abaqus','ansys','mukavemet','kırılma mekaniği',
107
  },
108
- 'Gârüntü İşleme & Algılama': {
109
- 'nesne tespiti','segmentasyon','lidar','radar','yolo',
110
- 'resnet','konvolüsyon','termal','infrared','optik akış',
111
  },
112
  'Otonom Sistemler & Robotik': {
113
- 'otonom','iha','uav','sΓΌrΓΌ','navigasyon','haritalama',
114
- 'slam','path planning','insansΔ±z',
115
  },
116
- 'Uçuş Mekaniği': {
117
- 'stabilite','trim','manevra','irtifa','gΓΌdΓΌm',
118
- 'uçuş yârüngesi','ağırlık merkezi','balistik',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  },
120
  }
121
-
 
 
 
122
  @dataclass
123
  class KategoriSkoru:
124
- kategori: str
125
- final_skor: float
126
- keyword_skor: float
127
  semantic_skor: float
128
- eslesmeler: list = field(default_factory=list)
129
-
 
 
 
130
  class HibritSiniflandirici:
 
131
  def __init__(self, taxonomy, embedder,
132
- keyword_weight=0.35, semantic_weight=0.65,
133
- title_boost=2.0):
134
- self.taxonomy = self._expand(
135
- {c: {'keywords': {str(k).lower().strip() for k in d.get('keywords', set()) if str(k).strip()}}
136
  for c, d in taxonomy.items()}
137
  )
138
  self.kw_w = keyword_weight
@@ -141,351 +163,420 @@ class HibritSiniflandirici:
141
  self.embedder = embedder
142
  self.idf = self._idf()
143
  self.centroids = self._centroids()
144
- log.info(f"Hibrit sΔ±nΔ±flandΔ±rΔ±cΔ± hazΔ±r: {len(self.taxonomy)} kategori, "
145
- f"alpha={keyword_weight}/{semantic_weight}")
146
-
147
  def _expand(self, tax):
148
- import copy; t = copy.deepcopy(tax)
 
149
  for cat, kws in _EKSTRA_KW.items():
150
- if cat in t: t[cat]['keywords'] |= kws
 
151
  return t
152
-
153
  def _idf(self):
154
  cnt = Counter()
155
  N = len(self.taxonomy)
156
  for d in self.taxonomy.values():
157
- for k in d['keywords']: cnt[k] += 1
158
- return {k: math.log((N+1)/(v+1))+1 for k, v in cnt.items()}
159
-
 
160
  def _centroids(self):
161
  c = {}
162
  for cat, d in self.taxonomy.items():
163
  kws = list(d['keywords'])[:30]
164
- if not kws: c[cat] = None; continue
 
 
165
  embs = self.embedder.encode(kws, show_progress_bar=False, convert_to_numpy=True)
166
  v = np.mean(embs, axis=0)
167
  n = np.linalg.norm(v)
168
  c[cat] = v / n if n > 0 else v
169
  return c
170
-
171
  def _kw_score(self, extracted, title_kws=None):
172
  ext = [k.lower().strip() for k in extracted if k and str(k).strip()]
173
  max_idf = max(self.idf.values(), default=1.0)
174
  results = {}
175
  for cat, d in self.taxonomy.items():
176
- cat_kws = d['keywords']
177
- score, eslm = 0.0, []
178
  for kw in ext:
179
  boost = self.title_boost if (title_kws and kw in title_kws) else 1.0
180
  idf_w = self.idf.get(kw, 1.0)
181
  if kw in cat_kws:
182
- p = 2.0 * boost * idf_w; score += p
 
183
  eslm.append(kw)
184
  elif len(kw) >= 4 and any(kw in ck or ck in kw for ck in cat_kws):
185
- p = 1.0 * boost * idf_w; score += p
 
186
  eslm.append(kw)
187
  max_p = max(len(ext) * 2.0 * max_idf, 1e-6)
188
  results[cat] = (min(score / max_p, 1.0), eslm)
189
  return results
190
-
191
  def _sem_score(self, extracted, text=None):
192
  parts = []
193
- if text and str(text).strip(): parts.append(str(text).strip())
194
- if extracted: parts.append(" ".join(str(k) for k in extracted))
195
- if not parts: return {c: 0.0 for c in self.taxonomy}
196
- emb = self.embedder.encode([" | ".join(parts)],
197
- show_progress_bar=False, convert_to_numpy=True)[0]
 
 
 
 
198
  n = np.linalg.norm(emb)
199
- if n > 0: emb = emb / n
200
- return {cat: max(0.0, min(1.0, (float(np.dot(emb, cn)) + 1.0) / 2.0))
201
- if cn is not None else 0.0
202
- for cat, cn in self.centroids.items()}
203
-
 
 
 
204
  def classify(self, keywords, text=None, title=None, top_k=3):
205
  title_kws = {w.lower() for w in str(title).split()} if title else None
206
- kw_r = self._kw_score(keywords, title_kws)
207
- sem_s = self._sem_score(keywords, text)
208
- ks = {}
209
  for c in self.taxonomy:
210
- kwn, esl = kw_r[c]
211
- f = self.kw_w * kwn + self.sem_w * sem_s[c]
212
- ks[c] = KategoriSkoru(c, f, kwn, sem_s[c], esl)
213
- srt = sorted(ks.values(), key=lambda x: x.final_skor, reverse=True)
214
  return {
215
  'prediction': srt[0].kategori,
216
  'confidence': srt[0].final_skor,
217
  'top_k': srt[:top_k],
 
218
  }
219
-
220
-
221
- # ═══════════════════════════════════════════════════════════════════
222
- # BERT MODEL
223
- # ═══════════════════════════════════════════════════════════════════
224
  class LiftUpBertModel(nn.Module):
 
225
  def __init__(self, num_labels=128):
226
  super().__init__()
227
- self.bert = AutoModel.from_pretrained("dbmdz/bert-base-turkish-cased")
228
  self.dropout = nn.Dropout(0.3)
229
  self.classifier = nn.Linear(768, num_labels)
 
230
  def forward(self, input_ids, attention_mask):
231
  out = self.bert(input_ids=input_ids, attention_mask=attention_mask)
232
  return self.classifier(self.dropout(out.last_hidden_state[:, 0]))
233
-
234
-
235
- # ═══════════════════════════════════════════════════════════════════
236
- # POST-PROCESSOR
237
- # ═══════════════════════════════════════════════════════════════════
238
  class SoftPostProcessor:
239
- BLACKLIST = {'kombinatΓΌr','hesonomik','modΓΌlasyonlarΔ±','difΓΌzΓΆrlΓΌ','optimizasyonlarΔ±nΔ±'}
240
- ACRONYMS = {'CFD','FEA','CAD','ROS','CNN','AI','ML','DL','IoT','GPU','SSD','API'}
241
-
 
242
  def fix_case(self, kw):
243
  parts = []
244
  for w in kw.split():
245
- if w.upper() in self.ACRONYMS: parts.append(w.upper())
246
- elif not parts: parts.append(w.capitalize())
247
- else: parts.append(w.lower())
 
 
 
248
  return ' '.join(parts)
249
-
250
  def ok(self, kw):
251
- return (kw.lower() not in self.BLACKLIST
252
- and 3 <= len(kw) <= 80
253
- and not re.search(r'[^a-zA-ZΓ§Γ‡ΔŸΔžΔ±Δ°ΓΆΓ–ΕŸΕžΓΌΓœ\s\-]', kw))
254
-
 
 
255
  def process(self, kws, min_kw=3):
256
  out, seen = [], set()
257
  for k in kws:
258
- if not self.ok(k): continue
 
259
  f = self.fix_case(k)
260
  if f.lower() not in seen:
261
- out.append(f); seen.add(f.lower())
 
262
  return out[:8] if out else kws[:min_kw]
263
-
264
-
265
- # ═══════════════════════════════════════════════════════════════════
266
- # GEMΔ°NΔ° ENTEGRASYONU
267
- # ═══════════════════════════════════════════════════════════════════
268
- def init_gemini(api_key: str, cat_list: list):
269
- global gemini_model, GEMINI_OK
270
  if not api_key:
271
- log.info("GEMINI_API_KEY tanΔ±mlΔ± değil β€” Gemini devre dışı")
272
  return
273
  try:
274
  import google.generativeai as genai
275
  genai.configure(api_key=api_key)
276
- gemini_model = genai.GenerativeModel("gemini-2.5-flash")
277
- # test isteği
278
- gemini_model.generate_content("Hi")
279
- GEMINI_OK = True
280
- log.info("βœ… Gemini 2.5 Flash hazΔ±r")
281
  except Exception as e:
282
- GEMINI_OK = False
283
- log.warning(f"Gemini yΓΌklenemedi: {e}")
284
-
285
- def gemini_classify(title: str, abstract: str, cat_list: list) -> Optional[str]:
286
- """Gemini'ye hem keyword Γ§Δ±karma hem sΔ±nΔ±flandΔ±rma yaptΔ±rΔ±r."""
287
- if not GEMINI_OK or gemini_model is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  return None
289
- cat_str = "\n".join(f"{i+1}. {c}" for i, c in enumerate(cat_list))
290
- prompt = f"""Sen bir havacΔ±lΔ±k ve savunma sanayi projesi analiz uzmanΔ±sΔ±n.
291
-
292
- PROJE BAŞLIĞI: {title}
293
- PROJE Γ–ZETΔ°: {str(abstract)[:500]}
294
-
295
- GΓ–REV:
296
- 1. Bu projenin teknik anahtar kelimelerini belirle.
297
- 2. Anahtar kelimelere gâre aşağıdaki 15 kategoriden TAM OLARAK BİRİNE ata.
298
-
299
- KATEGORΔ°LER:
300
- {cat_str}
301
-
302
- YANIT FORMATI (sadece bu iki satΔ±r):
303
- ANAHTAR_KELΔ°MELER: kelime1, kelime2, kelime3
304
- KATEGORΔ°: SAYI. Kategori AdΔ±"""
305
  try:
306
- raw = gemini_model.generate_content(prompt).text.strip()
307
- for line in raw.split('\n'):
308
- if 'KATEGORΔ°:' in line.upper():
309
- m = re.search(r'(\d+)\.', line)
310
- if m:
311
- num = int(m.group(1))
312
- if 1 <= num <= len(cat_list):
313
- return cat_list[num - 1]
314
- for c in cat_list:
315
- if c.lower() in line.lower():
316
- return c
317
- except Exception as e:
318
- log.warning(f"Gemini classify hatasΔ±: {e}")
319
  return None
320
-
321
-
322
- # ═══════════════════════════════════════════════════════════════════
323
- # MODEL YÜKLEME
324
- # ═══════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  def load_models():
326
  global bert_model, bert_tok, kw_model, generator, clf, TOP_KEYWORDS, ALL_CATS
327
-
328
- auth = {"token": HF_TOKEN} if HF_TOKEN else {}
329
- log.info("Modeller yΓΌkleniyor...")
330
-
331
- # 1) Taksonomi
332
- tax_path = hf_hub_download(repo_id=f"{HF_USERNAME}/liftup-bert",
333
- filename="taksonomi.txt", **auth)
334
  with open(tax_path, encoding='utf-8') as f:
335
  taxonomy = parse_taksonomi(f.read())
336
  ALL_CATS = list(taxonomy.keys())
337
- log.info(f"Taksonomi: {len(taxonomy)} kategori")
338
-
339
- # 2) BERT checkpoint
340
- ckpt_path = hf_hub_download(repo_id=f"{HF_USERNAME}/liftup-bert",
341
- filename="checkpoint.pth", **auth)
342
- ckpt = torch.load(ckpt_path, map_location="cpu")
343
- TOP_KEYWORDS = ckpt["TOP_KEYWORDS"]
344
-
345
- # 3) BERT model
346
- bert_path = hf_hub_download(repo_id=f"{HF_USERNAME}/liftup-bert",
347
- filename="best_bert_model.pth", **auth)
348
- bert_tok = AutoTokenizer.from_pretrained("dbmdz/bert-base-turkish-cased")
349
  model = LiftUpBertModel(len(TOP_KEYWORDS))
350
- model.load_state_dict(torch.load(bert_path, map_location="cpu"))
351
  model.eval()
352
  bert_model = model
353
- log.info("βœ… BERT hazΔ±r")
354
-
355
- # 4) KeyBERT
356
  kw_model = KeyBERT(model='sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
357
  embedder = kw_model.model.embedding_model
358
- log.info("βœ… KeyBERT hazΔ±r")
359
-
360
- # 5) ByT5
361
- byt5_dir = snapshot_download(repo_id=f"{HF_USERNAME}/liftup-byt5", **auth)
362
- byt5_tok = AutoTokenizer.from_pretrained("google/byt5-small")
363
  byt5_mdl = AutoModelForSeq2SeqLM.from_pretrained(byt5_dir)
364
  byt5_mdl.eval()
365
- post = SoftPostProcessor()
366
-
367
  class Generator:
368
  def __init__(self, tok, mdl, pp):
369
- self.tok, self.mdl, self.pp = tok, mdl, pp
370
- def generate(self, title="", abstract=""):
371
- text = f"keywords: {title} {abstract}".strip()
372
- inp = self.tok(text, max_length=512, truncation=True, return_tensors="pt")
 
 
 
373
  with torch.no_grad():
374
- out = self.mdl.generate(**inp, max_new_tokens=128, do_sample=False,
375
- no_repeat_ngram_size=4, repetition_penalty=1.5)
 
 
376
  pred = self.tok.decode(out[0], skip_special_tokens=True)
377
- if pred.lower().startswith("keywords:"): pred = pred[9:].strip()
378
- kws = [k.strip() for k in pred.split(';') if k.strip()]
 
379
  return self.pp.process(kws)
380
-
381
  generator = Generator(byt5_tok, byt5_mdl, post)
382
- log.info("βœ… ByT5 hazΔ±r")
383
-
384
- # 6) Hibrit sınıflandırıcı (iyileştirilmiş)
385
- clf = HibritSiniflandirici(taxonomy, embedder,
386
- keyword_weight=0.35,
387
- semantic_weight=0.65,
388
- title_boost=2.0)
389
-
390
- # 7) Gemini (opsiyonel)
391
- init_gemini(GEMINI_KEY, ALL_CATS)
392
-
393
- log.info("βœ… TΓΌm modeller hazΔ±r!")
394
-
395
-
396
- # ═══════════════════════════════════════════════════════════════════
397
- # FASTAPI
398
- # ═══════════════════════════════════════════════════════════════════
399
  @asynccontextmanager
400
  async def lifespan(app: FastAPI):
401
  load_models()
402
  yield
403
-
404
- app = FastAPI(title="LIFT UP SΔ±nΔ±flandΔ±rΔ±cΔ± v2", lifespan=lifespan)
405
- app.add_middleware(CORSMiddleware, allow_origins=["*"],
406
- allow_methods=["POST","GET"], allow_headers=["*"])
407
-
 
 
 
 
 
408
  class ClassifyRequest(BaseModel):
409
  baslik: str
410
  ozet: str
411
  keywords: Optional[List[str]] = None
412
- use_gemini: bool = False # True gânderilirse Gemini de çalışır
413
-
414
  class KategoriResponse(BaseModel):
415
  kategori: str
416
  guven: float
417
  keyword_skor: float
418
  semantic_skor: float
419
  eslesmeler: List[str]
420
-
 
421
  class ClassifyResponse(BaseModel):
422
  prediction: str
423
  confidence: float
424
  top_3: List[KategoriResponse]
425
  extracted_keywords: List[str]
 
 
 
426
  processing_time_ms: int
427
- gemini_prediction: Optional[str] = None # Gemini sonucu (varsa)
428
- gemini_available: bool = False
429
-
430
-
431
  def bert_extract(text):
432
- enc = bert_tok(str(text).lower(), truncation=True, padding='max_length',
433
- max_length=256, return_tensors='pt')
 
 
434
  with torch.no_grad():
435
  logits = bert_model(enc['input_ids'], enc['attention_mask'])
436
  probs = torch.sigmoid(logits)[0].numpy()
437
  idxs = np.argsort(probs)[-10:][::-1]
438
  return [TOP_KEYWORDS[i] for i in idxs if probs[i] > 0.01][:5]
439
-
 
440
  def keybert_extract(text):
441
- clean = re.sub(r'[^\w\sΔŸΓΌΕŸΔ±ΓΆΓ§ΔžΓœΕžΔ°Γ–Γ‡]', ' ', text.lower()).strip()
442
  try:
443
- kws = kw_model.extract_keywords(clean, keyphrase_ngram_range=(1,3),
444
- top_n=5, use_mmr=True, diversity=0.2)
 
445
  return [k[0] for k in kws][:3]
446
- except:
447
  return []
448
-
449
-
450
- @app.get("/health")
451
  def health():
452
- return {"status": "ok", "gemini": GEMINI_OK}
453
-
454
- @app.get("/")
 
455
  def root():
456
- return {"message": "LIFT UP API v2 çalışıyor",
457
- "endpoint": "POST /classify",
458
- "gemini_available": GEMINI_OK}
459
-
460
- @app.post("/classify", response_model=ClassifyResponse)
461
  def classify(req: ClassifyRequest):
462
  if not req.baslik.strip() or not req.ozet.strip():
463
- raise HTTPException(400, "Başlık ve âzet zorunludur")
464
-
465
  t0 = time.time()
466
- text = f"{req.baslik} {req.ozet}"
467
-
468
- # ── Keyword Γ§Δ±karma ──────────────────────────────────────────────
469
- bert_kws = bert_extract(text)
470
- kb_kws = keybert_extract(text)
471
- byt5_kws = generator.generate(req.baslik, req.ozet)
472
- extra = req.keywords or []
473
- tum_kws = list(dict.fromkeys(bert_kws + kb_kws + byt5_kws + extra))
474
-
475
- # ── Hibrit sΔ±nΔ±flandΔ±rma ─────────────────────────────────────────
476
- sonuc = clf.classify(tum_kws, text, title=req.baslik, top_k=3)
477
-
478
- # ── Gemini sΔ±nΔ±flandΔ±rma (opsiyonel) ─────────────────────────────
479
- gem_pred = None
480
- if req.use_gemini and GEMINI_OK:
481
- gem_pred = gemini_classify(req.baslik, req.ozet, ALL_CATS)
482
-
483
  ms = int((time.time() - t0) * 1000)
484
-
485
  return ClassifyResponse(
486
- prediction = sonuc['prediction'],
487
- confidence = round(sonuc['confidence'], 4),
488
- top_3 = [
489
  KategoriResponse(
490
  kategori = ks.kategori,
491
  guven = round(ks.final_skor, 4),
@@ -493,11 +584,11 @@ def classify(req: ClassifyRequest):
493
  semantic_skor = round(ks.semantic_skor, 4),
494
  eslesmeler = ks.eslesmeler[:5],
495
  )
496
- for ks in sonuc['top_k']
497
  ],
498
- extracted_keywords = tum_kws,
499
- processing_time_ms = ms,
500
- gemini_prediction = gem_pred,
501
- gemini_available = GEMINI_OK,
 
502
  )
503
-
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os, re, time, math, unicodedata, logging
2
  from contextlib import asynccontextmanager
3
+ from collections import Counter
4
  from dataclasses import dataclass, field
5
  from typing import Dict, List, Optional
6
+
7
  import numpy as np
8
  import torch
9
  import torch.nn as nn
 
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from pydantic import BaseModel
13
  from transformers import AutoTokenizer, AutoModel, AutoModelForSeq2SeqLM
 
14
  from keybert import KeyBERT
15
  from huggingface_hub import hf_hub_download, snapshot_download
16
+
17
  logging.basicConfig(level=logging.INFO)
18
  log = logging.getLogger("liftup")
19
+
20
+ HF_USERNAME = os.getenv("HF_USERNAME", "Engin34")
21
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
22
+ GEMINI_KEY = os.getenv("GEMINI_API_KEY", "")
23
+
 
24
  bert_model = None
25
  bert_tok = None
26
  kw_model = None
27
  generator = None
28
  clf = None
29
  TOP_KEYWORDS = None
30
+ _ext_model = None
31
+ _EXT_OK = False
32
  ALL_CATS = []
33
+
34
+
35
+ # ── Taksonomi parser ─────────────────────────────────────────────────
36
+
 
 
37
  def _temizle(k):
38
+ k = k.replace('\u200b', '').replace('\ufeff', '')
39
  k = unicodedata.normalize('NFKC', k)
40
+ return re.sub(r'\s+', ' ', k).strip().lower()
41
+
42
  def _parantez_ayir(k):
43
  m = re.match(r'^(.+?)\s*\((.+?)\)\s*$', k)
44
+ if not m:
45
+ return [k]
46
  ana, ic = m.group(1).strip(), m.group(2).strip()
47
+ if any(a in ic.lower() for a in ['baglam', 'proses', 'analiz', 'anahtarlari']):
48
  return [ana]
49
  return [ana] + [p.strip() for p in ic.split('/') if p.strip()]
50
+
51
  def _virgul_ayir(metin):
52
  sonuc, buf, d = [], [], 0
53
  for c in metin:
54
+ if c == '(':
55
+ d += 1
56
+ buf.append(c)
57
+ elif c == ')':
58
+ d -= 1
59
+ buf.append(c)
60
+ elif c == ',' and d == 0:
61
+ sonuc.append(''.join(buf))
62
+ buf = []
63
+ else:
64
+ buf.append(c)
65
+ if buf:
66
+ sonuc.append(''.join(buf))
67
  return sonuc
68
+
69
  def parse_taksonomi(icerik: str) -> Dict:
70
+ icerik = icerik.replace('\u200b', '')
71
  matches = list(re.finditer(r'^\s*(\d+)\)\s+(.+?)\s*$', icerik, re.MULTILINE))
72
  tax = {}
73
  for i, m in enumerate(matches):
74
  kat = m.group(2).strip()
75
  govde = icerik[m.end():(matches[i+1].start() if i+1 < len(matches) else len(icerik))].strip()
76
  pm = re.search(r'\((.+)\)', govde, re.DOTALL)
77
+ if not pm:
78
+ continue
79
  kw_set = set()
80
  for parca in _virgul_ayir(pm.group(1)):
81
  for alt in _parantez_ayir(parca.strip()):
82
  for k in re.split(r'[/]', alt):
83
  temiz = _temizle(k)
84
+ if len(temiz) >= 2:
85
+ kw_set.add(temiz)
86
  tax[kat] = {'keywords': kw_set}
87
  return tax
88
+
89
+
90
+ # ── Taksonomi genisletme sozlugu ─────────────────────────────────────
91
+
 
92
  _EKSTRA_KW = {
93
+ 'Yapay Zeka & Veri Bilimi': {
94
+ 'derin ogrenme', 'sinir agi', 'transformer', 'lstm', 'gru',
95
+ 'makine ogrenmesi', 'yapay sinir', 'classification', 'regresyon',
96
+ 'veri madenciligi', 'takviyeli ogrenme', 'xgboost',
97
  },
98
+ 'Yapilar, Mukavemet & Analiz': {
99
+ 'sonlu elemanlar', 'fea', 'burkulma', 'yorulma', 'hasar',
100
+ 'gerilme', 'deformasyon', 'mod analizi', 'titresim', 'nastran',
101
+ 'abaqus', 'ansys', 'mukavemet',
102
  },
103
+ 'Goruntu Isleme & Algilama': {
104
+ 'nesne tespiti', 'segmentasyon', 'lidar', 'radar', 'yolo',
105
+ 'resnet', 'konvolusyon', 'termal', 'infrared', 'optik akis',
106
  },
107
  'Otonom Sistemler & Robotik': {
108
+ 'otonom', 'iha', 'uav', 'suru', 'navigasyon', 'haritalama',
109
+ 'slam', 'path planning', 'insansiz',
110
  },
111
+ 'Ucus Mekanigi': {
112
+ 'stabilite', 'trim', 'manevra', 'irtifa', 'gudum',
113
+ 'ucus yorungesi', 'agirlik merkezi', 'balistik',
114
+ 'ucus dinamigi', 'manevrabilite', 'yukleme', 'flaperon',
115
+ 'elevator', 'rudder', 'aileron', 'moment', 'kaldirma',
116
+ 'surukleme', 'aoa', 'hucum acisi', 'dalis', 'tirmanma',
117
+ },
118
+ 'Kontrol & Aviyonik': {
119
+ 'pid', 'kontrol sistemi', 'geri besleme', 'servo', 'gyro',
120
+ 'ivmeolcer', 'imu', 'kalman', 'fpga', 'gomulu',
121
+ 'mikrodenetleyici', 'aviyonik', 'ucus bilgisayari',
122
+ 'otopilot', 'fms', 'actuator', 'sensor fuzyonu',
123
+ },
124
+ 'Eklemeli Imalat': {
125
+ '3d baski', 'slm', 'fdm', 'sls', 'dmls', 'ebm',
126
+ 'topoloji optimizasyonu', 'kafes yapi', 'metal baski',
127
+ 'polimer', 'eklemeli', 'additive', 'katman katman',
128
+ 'tarama stratejisi', 'goz enek', 'yogunluk',
129
+ },
130
+ 'Yazilim Gelistirme': {
131
+ 'gomulu yazilim', 'rtos', 'misra', 'do-178', 'arinc',
132
+ 'mil-std', 'yazilim dogrulama', 'test otomasyonu',
133
+ 'model tabanli tasarim', 'simulink', 'model based',
134
  },
135
  }
136
+
137
+
138
+ # ── Veri siniflari ───────────────────────────────────────────────────
139
+
140
  @dataclass
141
  class KategoriSkoru:
142
+ kategori: str
143
+ final_skor: float
144
+ keyword_skor: float
145
  semantic_skor: float
146
+ eslesmeler: list = field(default_factory=list)
147
+
148
+
149
+ # ── Hibrit siniflandirici ────────────────────────────────────────────
150
+
151
  class HibritSiniflandirici:
152
+
153
  def __init__(self, taxonomy, embedder,
154
+ keyword_weight=0.35, semantic_weight=0.65, title_boost=2.0):
155
+ self.taxonomy = self._expand(
156
+ {c: {'keywords': {str(k).lower().strip()
157
+ for k in d.get('keywords', set()) if str(k).strip()}}
158
  for c, d in taxonomy.items()}
159
  )
160
  self.kw_w = keyword_weight
 
163
  self.embedder = embedder
164
  self.idf = self._idf()
165
  self.centroids = self._centroids()
166
+ log.info("Hibrit siniflandirici hazir: %d kategori", len(self.taxonomy))
167
+
 
168
  def _expand(self, tax):
169
+ import copy
170
+ t = copy.deepcopy(tax)
171
  for cat, kws in _EKSTRA_KW.items():
172
+ if cat in t:
173
+ t[cat]['keywords'] |= kws
174
  return t
175
+
176
  def _idf(self):
177
  cnt = Counter()
178
  N = len(self.taxonomy)
179
  for d in self.taxonomy.values():
180
+ for k in d['keywords']:
181
+ cnt[k] += 1
182
+ return {k: math.log((N + 1) / (v + 1)) + 1 for k, v in cnt.items()}
183
+
184
  def _centroids(self):
185
  c = {}
186
  for cat, d in self.taxonomy.items():
187
  kws = list(d['keywords'])[:30]
188
+ if not kws:
189
+ c[cat] = None
190
+ continue
191
  embs = self.embedder.encode(kws, show_progress_bar=False, convert_to_numpy=True)
192
  v = np.mean(embs, axis=0)
193
  n = np.linalg.norm(v)
194
  c[cat] = v / n if n > 0 else v
195
  return c
196
+
197
  def _kw_score(self, extracted, title_kws=None):
198
  ext = [k.lower().strip() for k in extracted if k and str(k).strip()]
199
  max_idf = max(self.idf.values(), default=1.0)
200
  results = {}
201
  for cat, d in self.taxonomy.items():
202
+ cat_kws = d['keywords']
203
+ score, eslm = 0.0, []
204
  for kw in ext:
205
  boost = self.title_boost if (title_kws and kw in title_kws) else 1.0
206
  idf_w = self.idf.get(kw, 1.0)
207
  if kw in cat_kws:
208
+ p = 2.0 * boost * idf_w
209
+ score += p
210
  eslm.append(kw)
211
  elif len(kw) >= 4 and any(kw in ck or ck in kw for ck in cat_kws):
212
+ p = 1.0 * boost * idf_w
213
+ score += p
214
  eslm.append(kw)
215
  max_p = max(len(ext) * 2.0 * max_idf, 1e-6)
216
  results[cat] = (min(score / max_p, 1.0), eslm)
217
  return results
218
+
219
  def _sem_score(self, extracted, text=None):
220
  parts = []
221
+ if text and str(text).strip():
222
+ parts.append(str(text).strip())
223
+ if extracted:
224
+ parts.append(' '.join(str(k) for k in extracted))
225
+ if not parts:
226
+ return {c: 0.0 for c in self.taxonomy}
227
+ emb = self.embedder.encode(
228
+ [' | '.join(parts)], show_progress_bar=False, convert_to_numpy=True
229
+ )[0]
230
  n = np.linalg.norm(emb)
231
+ if n > 0:
232
+ emb = emb / n
233
+ return {
234
+ cat: max(0.0, min(1.0, (float(np.dot(emb, cn)) + 1.0) / 2.0))
235
+ if cn is not None else 0.0
236
+ for cat, cn in self.centroids.items()
237
+ }
238
+
239
  def classify(self, keywords, text=None, title=None, top_k=3):
240
  title_kws = {w.lower() for w in str(title).split()} if title else None
241
+ kw_r = self._kw_score(keywords, title_kws)
242
+ sem_s = self._sem_score(keywords, text)
243
+ scores = {}
244
  for c in self.taxonomy:
245
+ kwn, esl = kw_r[c]
246
+ f = self.kw_w * kwn + self.sem_w * sem_s[c]
247
+ scores[c] = KategoriSkoru(c, f, kwn, sem_s[c], esl)
248
+ srt = sorted(scores.values(), key=lambda x: x.final_skor, reverse=True)
249
  return {
250
  'prediction': srt[0].kategori,
251
  'confidence': srt[0].final_skor,
252
  'top_k': srt[:top_k],
253
+ 'all_scores': scores,
254
  }
255
+
256
+
257
+ # ── BERT modeli ──────────────────────────────────────────────────────
258
+
 
259
  class LiftUpBertModel(nn.Module):
260
+
261
  def __init__(self, num_labels=128):
262
  super().__init__()
263
+ self.bert = AutoModel.from_pretrained('dbmdz/bert-base-turkish-cased')
264
  self.dropout = nn.Dropout(0.3)
265
  self.classifier = nn.Linear(768, num_labels)
266
+
267
  def forward(self, input_ids, attention_mask):
268
  out = self.bert(input_ids=input_ids, attention_mask=attention_mask)
269
  return self.classifier(self.dropout(out.last_hidden_state[:, 0]))
270
+
271
+
272
+ # ── Post-processor ───────────────────────────────────────────────────
273
+
 
274
  class SoftPostProcessor:
275
+
276
+ BLACKLIST = {'kombinatur', 'hesonomik', 'modulasyonlari', 'difuzorlue', 'optimizasyonlarini'}
277
+ ACRONYMS = {'CFD', 'FEA', 'CAD', 'ROS', 'CNN', 'AI', 'ML', 'DL', 'IoT', 'GPU', 'SSD', 'API'}
278
+
279
  def fix_case(self, kw):
280
  parts = []
281
  for w in kw.split():
282
+ if w.upper() in self.ACRONYMS:
283
+ parts.append(w.upper())
284
+ elif not parts:
285
+ parts.append(w.capitalize())
286
+ else:
287
+ parts.append(w.lower())
288
  return ' '.join(parts)
289
+
290
  def ok(self, kw):
291
+ return (
292
+ kw.lower() not in self.BLACKLIST
293
+ and 3 <= len(kw) <= 80
294
+ and not re.search(r'[^a-zA-Z\xc7\xe7\u011e\u011f\u0130\u0131\xd6\xf6\u015e\u015f\xdc\xfc\s\-]', kw)
295
+ )
296
+
297
  def process(self, kws, min_kw=3):
298
  out, seen = [], set()
299
  for k in kws:
300
+ if not self.ok(k):
301
+ continue
302
  f = self.fix_case(k)
303
  if f.lower() not in seen:
304
+ out.append(f)
305
+ seen.add(f.lower())
306
  return out[:8] if out else kws[:min_kw]
307
+
308
+
309
+ # ── Ek destek modulu ─────────────────────────────────────────────────
310
+
311
+ def _init_ext(api_key, cat_list):
312
+ global _ext_model, _EXT_OK
 
313
  if not api_key:
 
314
  return
315
  try:
316
  import google.generativeai as genai
317
  genai.configure(api_key=api_key)
318
+ _ext_model = genai.GenerativeModel('gemini-2.5-flash')
319
+ _ext_model.generate_content('Hi')
320
+ _EXT_OK = True
321
+ log.info('Ek destek modulu hazir')
 
322
  except Exception as e:
323
+ _EXT_OK = False
324
+ log.warning('Ek destek modulu: %s', e)
325
+
326
+
327
+ def _ext_keywords(title, abstract):
328
+ if not _EXT_OK or _ext_model is None:
329
+ return []
330
+ try:
331
+ prompt = (
332
+ f'Havacilk ve savunma sanayi alaninda uzman bir arastirmacisin.\n'
333
+ f'Asagidaki projenin teknik anahtar kelimelerini belirle.\n\n'
334
+ f'PROJE BASLIGI: {title}\n'
335
+ f'PROJE OZETI: {abstract[:500]}\n\n'
336
+ f'KURALLAR:\n'
337
+ f'- Yalnizca bu projede gecen teknik terimler, yontemler, malzemeler ve teknolojiler\n'
338
+ f'- Genel kelimeler degil (analiz, sistem, gelistirme gibi) -- ozgun teknik terimler\n'
339
+ f'- Turkce yaz, kisaltmalar buyuk harf (CFD, ROS, CFRP, LSTM gibi)\n'
340
+ f'- VirgΓΌlle ayir, en fazla 6 kelime\n\n'
341
+ f'YANIT (sadece virgΓΌlle ayrilmis kelimeler):'
342
+ )
343
+ raw = _ext_model.generate_content(prompt).text.strip()
344
+ lines = [l.strip() for l in raw.split('\n') if l.strip()]
345
+ raw = lines[-1] if lines else raw
346
+ return [k.strip().strip('.-') for k in raw.split(',') if k.strip()][:6]
347
+ except Exception:
348
+ return []
349
+
350
+
351
+ def _ext_classify(title, abstract, cat_list):
352
+ if not _EXT_OK or _ext_model is None:
353
  return None
354
+ cat_str = '\n'.join(f'{i+1}. {c}' for i, c in enumerate(cat_list))
355
+ prompt = (
356
+ f'Sen bir havacilk ve savunma sanayi Ar-Ge projesi siniflandirma uzmanissin.\n'
357
+ f'Asagidaki projeyi, verilen 15 kategoriden TAM OLARAK BIRINE ata.\n\n'
358
+ f'PROJE BASLIGI: {title}\n'
359
+ f'PROJE OZETI: {abstract[:600]}\n\n'
360
+ f'KATEGORILER:\n{cat_str}\n\n'
361
+ f'KARAR KURALLARI:\n'
362
+ f'- Projenin ana konusuna ve kullandigi yonteme odaklan\n'
363
+ f'- Basliktaki anahtar kelimeler genellikle en guclu ipucudur\n'
364
+ f'- Birden fazla konu varsa, en baskin olani sec\n'
365
+ f'- Sadece listedeki kategorilerden birini sec\n\n'
366
+ f'YANIT FORMATI (baska hicbir sey yazma):\n'
367
+ f'SAYI. Kategori Adi\n\n'
368
+ f'Ornek: 8. Kompozit Yapilar'
369
+ )
370
  try:
371
+ raw = _ext_model.generate_content(prompt).text.strip()
372
+ m = re.search(r'(\d+)\.', raw)
373
+ if m:
374
+ num = int(m.group(1))
375
+ if 1 <= num <= len(cat_list):
376
+ return cat_list[num - 1]
377
+ raw_lower = raw.lower()
378
+ for c in cat_list:
379
+ if c.lower() in raw_lower:
380
+ return c
381
+ except Exception:
382
+ pass
 
383
  return None
384
+
385
+
386
+ def _combine_scores(hybrid_result, extra_cat, boost=0.08):
387
+ if not extra_cat:
388
+ return hybrid_result
389
+ scores = hybrid_result['all_scores']
390
+ if extra_cat in scores:
391
+ old = scores[extra_cat]
392
+ scores[extra_cat] = KategoriSkoru(
393
+ kategori = old.kategori,
394
+ final_skor = min(old.final_skor + boost, 1.0),
395
+ keyword_skor = old.keyword_skor,
396
+ semantic_skor = old.semantic_skor,
397
+ eslesmeler = old.eslesmeler,
398
+ )
399
+ srt = sorted(scores.values(), key=lambda x: x.final_skor, reverse=True)
400
+ return {
401
+ 'prediction': srt[0].kategori,
402
+ 'confidence': srt[0].final_skor,
403
+ 'top_k': srt[:3],
404
+ 'all_scores': scores,
405
+ }
406
+
407
+
408
+ # ── Model yukleme ────────────────────────────────────────────────────
409
+
410
  def load_models():
411
  global bert_model, bert_tok, kw_model, generator, clf, TOP_KEYWORDS, ALL_CATS
412
+
413
+ auth = {'token': HF_TOKEN} if HF_TOKEN else {}
414
+
415
+ tax_path = hf_hub_download(
416
+ repo_id=f'{HF_USERNAME}/liftup-bert', filename='taksonomi.txt', **auth
417
+ )
 
418
  with open(tax_path, encoding='utf-8') as f:
419
  taxonomy = parse_taksonomi(f.read())
420
  ALL_CATS = list(taxonomy.keys())
421
+
422
+ ckpt_path = hf_hub_download(
423
+ repo_id=f'{HF_USERNAME}/liftup-bert', filename='checkpoint.pth', **auth
424
+ )
425
+ ckpt = torch.load(ckpt_path, map_location='cpu')
426
+ TOP_KEYWORDS = ckpt['TOP_KEYWORDS']
427
+
428
+ bert_path = hf_hub_download(
429
+ repo_id=f'{HF_USERNAME}/liftup-bert', filename='best_bert_model.pth', **auth
430
+ )
431
+ bert_tok = AutoTokenizer.from_pretrained('dbmdz/bert-base-turkish-cased')
 
432
  model = LiftUpBertModel(len(TOP_KEYWORDS))
433
+ model.load_state_dict(torch.load(bert_path, map_location='cpu'))
434
  model.eval()
435
  bert_model = model
436
+ log.info('BERT hazir')
437
+
 
438
  kw_model = KeyBERT(model='sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
439
  embedder = kw_model.model.embedding_model
440
+ log.info('KeyBERT hazir')
441
+
442
+ byt5_dir = snapshot_download(repo_id=f'{HF_USERNAME}/liftup-byt5', **auth)
443
+ byt5_tok = AutoTokenizer.from_pretrained('google/byt5-small')
 
444
  byt5_mdl = AutoModelForSeq2SeqLM.from_pretrained(byt5_dir)
445
  byt5_mdl.eval()
446
+ post = SoftPostProcessor()
447
+
448
  class Generator:
449
  def __init__(self, tok, mdl, pp):
450
+ self.tok = tok
451
+ self.mdl = mdl
452
+ self.pp = pp
453
+
454
+ def generate(self, title='', abstract=''):
455
+ text = f'keywords: {title} {abstract}'.strip()
456
+ inp = self.tok(text, max_length=512, truncation=True, return_tensors='pt')
457
  with torch.no_grad():
458
+ out = self.mdl.generate(
459
+ **inp, max_new_tokens=128, do_sample=False,
460
+ no_repeat_ngram_size=4, repetition_penalty=1.5
461
+ )
462
  pred = self.tok.decode(out[0], skip_special_tokens=True)
463
+ if pred.lower().startswith('keywords:'):
464
+ pred = pred[9:].strip()
465
+ kws = [k.strip() for k in pred.split(';') if k.strip()]
466
  return self.pp.process(kws)
467
+
468
  generator = Generator(byt5_tok, byt5_mdl, post)
469
+ log.info('ByT5 hazir')
470
+
471
+ clf = HibritSiniflandirici(
472
+ taxonomy, embedder,
473
+ keyword_weight=0.35, semantic_weight=0.65, title_boost=2.0
474
+ )
475
+
476
+ _init_ext(GEMINI_KEY, ALL_CATS)
477
+ log.info('Tum modeller hazir')
478
+
479
+
480
+ # ── FastAPI ──────────────────────────────────────────────────────────
481
+
 
 
 
 
482
  @asynccontextmanager
483
  async def lifespan(app: FastAPI):
484
  load_models()
485
  yield
486
+
487
+ app = FastAPI(title='LIFT UP Siniflandirici', lifespan=lifespan)
488
+ app.add_middleware(
489
+ CORSMiddleware,
490
+ allow_origins=['*'],
491
+ allow_methods=['POST', 'GET'],
492
+ allow_headers=['*'],
493
+ )
494
+
495
+
496
  class ClassifyRequest(BaseModel):
497
  baslik: str
498
  ozet: str
499
  keywords: Optional[List[str]] = None
500
+
501
+
502
  class KategoriResponse(BaseModel):
503
  kategori: str
504
  guven: float
505
  keyword_skor: float
506
  semantic_skor: float
507
  eslesmeler: List[str]
508
+
509
+
510
  class ClassifyResponse(BaseModel):
511
  prediction: str
512
  confidence: float
513
  top_3: List[KategoriResponse]
514
  extracted_keywords: List[str]
515
+ bert_keywords: List[str]
516
+ keybert_keywords: List[str]
517
+ byt5_keywords: List[str]
518
  processing_time_ms: int
519
+
520
+
 
 
521
  def bert_extract(text):
522
+ enc = bert_tok(
523
+ str(text).lower(), truncation=True,
524
+ padding='max_length', max_length=256, return_tensors='pt'
525
+ )
526
  with torch.no_grad():
527
  logits = bert_model(enc['input_ids'], enc['attention_mask'])
528
  probs = torch.sigmoid(logits)[0].numpy()
529
  idxs = np.argsort(probs)[-10:][::-1]
530
  return [TOP_KEYWORDS[i] for i in idxs if probs[i] > 0.01][:5]
531
+
532
+
533
  def keybert_extract(text):
534
+ clean = re.sub(r'[^\w\s\u011f\u011e\xfc\xdc\u015f\u015e\u0131\u0130\xf6\xd6\xe7\xc7]', ' ', text.lower()).strip()
535
  try:
536
+ kws = kw_model.extract_keywords(
537
+ clean, keyphrase_ngram_range=(1, 3), top_n=5, use_mmr=True, diversity=0.2
538
+ )
539
  return [k[0] for k in kws][:3]
540
+ except Exception:
541
  return []
542
+
543
+
544
+ @app.get('/health')
545
  def health():
546
+ return {'status': 'ok'}
547
+
548
+
549
+ @app.get('/')
550
  def root():
551
+ return {'message': 'API calisiyor', 'endpoint': 'POST /classify'}
552
+
553
+
554
+ @app.post('/classify', response_model=ClassifyResponse)
 
555
  def classify(req: ClassifyRequest):
556
  if not req.baslik.strip() or not req.ozet.strip():
557
+ raise HTTPException(400, 'Baslik ve ozet zorunludur')
558
+
559
  t0 = time.time()
560
+ text = f'{req.baslik} {req.ozet}'
561
+
562
+ bert_kws = bert_extract(text)
563
+ kb_kws = keybert_extract(text)
564
+ byt5_kws = generator.generate(req.baslik, req.ozet)
565
+ ext_kws = _ext_keywords(req.baslik, req.ozet)
566
+ extra = req.keywords or []
567
+
568
+ tum_kws = list(dict.fromkeys(bert_kws + kb_kws + byt5_kws + ext_kws + extra))
569
+
570
+ hybrid = clf.classify(tum_kws, text, title=req.baslik, top_k=3)
571
+ ext_cat = _ext_classify(req.baslik, req.ozet, ALL_CATS)
572
+ final = _combine_scores(hybrid, ext_cat)
573
+
 
 
 
574
  ms = int((time.time() - t0) * 1000)
575
+
576
  return ClassifyResponse(
577
+ prediction = final['prediction'],
578
+ confidence = round(final['confidence'], 4),
579
+ top_3 = [
580
  KategoriResponse(
581
  kategori = ks.kategori,
582
  guven = round(ks.final_skor, 4),
 
584
  semantic_skor = round(ks.semantic_skor, 4),
585
  eslesmeler = ks.eslesmeler[:5],
586
  )
587
+ for ks in final['top_k']
588
  ],
589
+ extracted_keywords = tum_kws,
590
+ bert_keywords = bert_kws,
591
+ keybert_keywords = kb_kws,
592
+ byt5_keywords = byt5_kws,
593
+ processing_time_ms = ms,
594
  )