Engin34 commited on
Commit
5ced407
Β·
verified Β·
1 Parent(s): e6967c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +327 -231
app.py CHANGED
@@ -1,34 +1,20 @@
1
  """
2
- LIFT UP Taksonomi SΔ±nΔ±flandΔ±rΔ±cΔ± β€” Backend API
3
  Hugging Face Spaces (Docker) ΓΌzerinde Γ§alışır.
4
-
5
- Endpoint:
6
- POST /classify
7
- {
8
- "baslik": "Proje başlığı",
9
- "ozet": "Proje ΓΆzeti",
10
- "keywords": ["opsiyonel", "liste"] # opsiyonel
11
- }
12
- β†’
13
- {
14
- "prediction": "Kompozit YapΔ±lar",
15
- "confidence": 0.82,
16
- "top_3": [...],
17
- "extracted_keywords": [...],
18
- "processing_time_ms": 1240
19
- }
20
  """
21
-
22
- import os
23
- import re
24
- import time
25
- import unicodedata
26
- import logging
27
  from contextlib import asynccontextmanager
28
- from collections import Counter
29
  from dataclasses import dataclass, field
30
- from typing import Dict, List, Optional, Set
31
-
32
  import numpy as np
33
  import torch
34
  import torch.nn as nn
@@ -39,23 +25,27 @@ from transformers import AutoTokenizer, AutoModel, AutoModelForSeq2SeqLM
39
  from sentence_transformers import SentenceTransformer
40
  from keybert import KeyBERT
41
  from huggingface_hub import hf_hub_download, snapshot_download
42
-
43
  logging.basicConfig(level=logging.INFO)
44
  log = logging.getLogger("liftup")
45
-
46
- HF_USERNAME = os.getenv("HF_USERNAME", "Engin34")
47
- HF_TOKEN = os.getenv("HF_TOKEN", "") # Space secret olarak eklenecek
48
-
49
- # ─── Global model değişkenleri ───────────────────────────────────────
 
50
  bert_model = None
51
  bert_tok = None
52
  kw_model = None
53
  generator = None
54
  clf = None
55
  TOP_KEYWORDS = None
 
 
 
56
  device = torch.device("cpu")
57
-
58
-
59
  # ═══════════════════════════════════════════════════════════════════
60
  # TAKSONOMΔ° PARSER
61
  # ═══════════════════════════════════════════════════════════════════
@@ -63,15 +53,15 @@ def _temizle(k):
63
  k = k.replace('\u200b','').replace('\ufeff','')
64
  k = unicodedata.normalize('NFKC', k)
65
  return re.sub(r'\s+',' ', k).strip().lower()
66
-
67
  def _parantez_ayir(k):
68
  m = re.match(r'^(.+?)\s*\((.+?)\)\s*$', k)
69
  if not m: return [k]
70
  ana, ic = m.group(1).strip(), m.group(2).strip()
71
- if any(a in ic.lower() for a in ['bağlam','kısmı','tarafı','proses','analiz','anahtarları','servisleme']):
72
  return [ana]
73
  return [ana] + [p.strip() for p in ic.split('/') if p.strip()]
74
-
75
  def _virgul_ayir(metin):
76
  sonuc, buf, d = [], [], 0
77
  for c in metin:
@@ -81,15 +71,15 @@ def _virgul_ayir(metin):
81
  else: buf.append(c)
82
  if buf: sonuc.append(''.join(buf))
83
  return sonuc
84
-
85
  def parse_taksonomi(icerik: str) -> Dict:
86
  icerik = icerik.replace('\u200b','')
87
  matches = list(re.finditer(r'^\s*(\d+)\)\s+(.+?)\s*$', icerik, re.MULTILINE))
88
  tax = {}
89
  for i, m in enumerate(matches):
90
- kat = m.group(2).strip()
91
  govde = icerik[m.end():(matches[i+1].start() if i+1 < len(matches) else len(icerik))].strip()
92
- pm = re.search(r'\((.+)\)', govde, re.DOTALL)
93
  if not pm: continue
94
  kw_set = set()
95
  for parca in _virgul_ayir(pm.group(1)):
@@ -99,208 +89,310 @@ def parse_taksonomi(icerik: str) -> Dict:
99
  if len(temiz) >= 2: kw_set.add(temiz)
100
  tax[kat] = {'keywords': kw_set}
101
  return tax
102
-
103
-
104
  # ═══════════════════════════════════════════════════════════════════
105
- # HΔ°BRΔ°T SINIFLANDIRICI
106
  # ═══════════════════════════════════════════════════════════════════
107
- @dataclass
108
- class EslesmeBilgisi:
109
- keyword: str; eslesme_tipi: str; eslesen_taksonomi_kw: str; puan: float
110
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  @dataclass
112
  class KategoriSkoru:
113
- kategori: str; final_skor: float; keyword_skor: float; semantic_skor: float
 
 
 
114
  eslesmeler: list = field(default_factory=list)
115
-
116
  class HibritSiniflandirici:
117
- def __init__(self, taxonomy, embedder, keyword_weight=0.4, semantic_weight=0.6):
118
- self.taxonomy = {c: {'keywords':{str(k).lower().strip() for k in d.get('keywords',set()) if str(k).strip()}}
119
- for c,d in taxonomy.items()}
120
- self.kw_w, self.sem_w = keyword_weight, semantic_weight
121
- self.embedder = embedder
122
- log.info("Centroid'ler hesaplanΔ±yor...")
123
- self.centroids = self._centroids()
124
- self.idf = self._idf()
125
- log.info(f"HazΔ±r: {len(self.taxonomy)} kategori")
126
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  def _centroids(self):
128
  c = {}
129
  for cat, d in self.taxonomy.items():
130
- kws = list(d['keywords'])
131
- if not kws: c[cat]=None; continue
132
  embs = self.embedder.encode(kws, show_progress_bar=False, convert_to_numpy=True)
133
- v = np.mean(embs, axis=0); n = np.linalg.norm(v)
134
- c[cat] = v/n if n>0 else v
 
135
  return c
136
-
137
- def _idf(self):
138
- cnt = Counter()
139
- for d in self.taxonomy.values():
140
- for k in d['keywords']: cnt[k]+=1
141
- N = len(self.taxonomy)
142
- return {k: np.log(N/v)+1.0 for k,v in cnt.items()}
143
-
144
- def _kw_score(self, extracted):
145
- ext = [k.lower().strip() for k in extracted if k and str(k).strip()]
146
  max_idf = max(self.idf.values(), default=1.0)
147
  results = {}
148
  for cat, d in self.taxonomy.items():
149
- cat_kws = d['keywords']; score, eslm = 0.0, []
 
150
  for kw in ext:
 
151
  idf_w = self.idf.get(kw, 1.0)
152
  if kw in cat_kws:
153
- p=2.0*idf_w; score+=p; eslm.append(EslesmeBilgisi(kw,'exact',kw,p)); continue
154
- if len(kw)<4: continue
155
- for ck in cat_kws:
156
- if len(ck)>=4 and (kw in ck or ck in kw):
157
- p=1.0*idf_w; score+=p; eslm.append(EslesmeBilgisi(kw,'partial',ck,p)); break
158
- max_p = max(len(ext)*2.0*max_idf, 1e-6)
159
- results[cat] = (min(score/max_p,1.0), eslm)
160
  return results
161
-
162
  def _sem_score(self, extracted, text=None):
163
  parts = []
164
  if text and str(text).strip(): parts.append(str(text).strip())
165
- if extracted: parts.append(" ".join(extracted))
166
- if not parts: return {c:0.0 for c in self.taxonomy}
167
- emb = self.embedder.encode([" | ".join(parts)], show_progress_bar=False, convert_to_numpy=True)[0]
 
168
  n = np.linalg.norm(emb)
169
- if n>0: emb=emb/n
170
- return {c: max(0.0, min(1.0,(float(np.dot(emb,cn))+1.0)/2.0)) if cn is not None else 0.0
171
- for c,cn in self.centroids.items()}
172
-
173
- def classify(self, keywords, text=None, top_k=3):
174
- kw_r = self._kw_score(keywords)
 
 
175
  sem_s = self._sem_score(keywords, text)
176
  ks = {}
177
  for c in self.taxonomy:
178
  kwn, esl = kw_r[c]
179
- f = self.kw_w*kwn + self.sem_w*sem_s[c]
180
  ks[c] = KategoriSkoru(c, f, kwn, sem_s[c], esl)
181
  srt = sorted(ks.values(), key=lambda x: x.final_skor, reverse=True)
182
- return {'prediction': srt[0].kategori, 'confidence': srt[0].final_skor, 'top_k': srt[:top_k]}
183
-
184
-
 
 
 
 
185
  # ═══════════════════════════════════════════════════════════════════
186
  # BERT MODEL
187
  # ═══════════════════════════════════════════════════════════════════
188
  class LiftUpBertModel(nn.Module):
189
  def __init__(self, num_labels=128):
190
  super().__init__()
191
- self.bert = AutoModel.from_pretrained("dbmdz/bert-base-turkish-cased")
192
- self.dropout = nn.Dropout(0.3)
193
  self.classifier = nn.Linear(768, num_labels)
194
  def forward(self, input_ids, attention_mask):
195
  out = self.bert(input_ids=input_ids, attention_mask=attention_mask)
196
- return self.classifier(self.dropout(out.last_hidden_state[:,0]))
197
-
198
-
199
  # ═══════════════════════════════════════════════════════════════════
200
  # POST-PROCESSOR
201
  # ═══════════════════════════════════════════════════════════════════
202
  class SoftPostProcessor:
203
- def __init__(self):
204
- self.blacklist = {'kombinatΓΌr','hesonomik','modΓΌlasyonlarΔ±','difΓΌzΓΆrlΓΌ','optimizasyonlarΔ±nΔ±'}
205
- self.acronyms = {'CFD','FEA','CAD','ROS','CNN','AI','ML','DL','IoT','GPU','SSD'}
206
- def is_acronym(self,w): return w.isupper() and 2<=len(w)<=5
207
- def fix_case(self,kw):
208
- out=[]
209
  for w in kw.split():
210
- if w.upper() in self.acronyms or self.is_acronym(w): out.append(w.upper())
211
- elif not out: out.append(w.capitalize())
212
- else: out.append(w.lower())
213
- return ' '.join(out)
214
- def should_filter(self,kw):
215
- if kw.lower() in self.blacklist: return True
216
- if not(3<=len(kw)<=80): return True
217
- if re.search(r'[^a-zA-ZΓ§Γ‡ΔŸΔžΔ±Δ°ΓΆΓ–ΕŸΕžΓΌΓœ\s\-]',kw): return True
218
- return False
219
- def process(self,keywords,min_kw=3):
220
- processed=[]
221
- for kw in keywords:
222
- if self.should_filter(kw): continue
223
- fixed=self.fix_case(kw)
224
- if not any(p.lower()==fixed.lower() for p in processed): processed.append(fixed)
225
- return processed[:8] if processed else keywords[:3]
226
-
227
-
 
 
228
  # ═══════════════════════════════════════════════════════════════════
229
- # MODEL YÜKLEME (startup)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  # ═══════════════════════════════════════════════════════════════════
231
  def load_models():
232
- global bert_model, bert_tok, kw_model, generator, clf, TOP_KEYWORDS
233
-
234
  auth = {"token": HF_TOKEN} if HF_TOKEN else {}
235
  log.info("Modeller yΓΌkleniyor...")
236
-
237
- # 1) taksonomi
238
- log.info("Taksonomi indiriliyor...")
239
- tax_path = hf_hub_download(
240
- repo_id=f"{HF_USERNAME}/liftup-bert",
241
- filename="taksonomi.txt", **auth
242
- )
243
  with open(tax_path, encoding='utf-8') as f:
244
  taxonomy = parse_taksonomi(f.read())
245
-
246
- # 2) BERT checkpoint (TOP_KEYWORDS iΓ§in)
247
- log.info("BERT checkpoint indiriliyor...")
248
- ckpt_path = hf_hub_download(
249
- repo_id=f"{HF_USERNAME}/liftup-bert",
250
- filename="checkpoint.pth", **auth
251
- )
252
  ckpt = torch.load(ckpt_path, map_location="cpu")
253
  TOP_KEYWORDS = ckpt["TOP_KEYWORDS"]
254
-
255
- # 3) BERT model ağırlıkları
256
- log.info("BERT model ağırlıkları indiriliyor (422 MB)...")
257
- bert_path = hf_hub_download(
258
- repo_id=f"{HF_USERNAME}/liftup-bert",
259
- filename="best_bert_model.pth", **auth
260
- )
261
  bert_tok = AutoTokenizer.from_pretrained("dbmdz/bert-base-turkish-cased")
262
- model = LiftUpBertModel(len(TOP_KEYWORDS))
263
  model.load_state_dict(torch.load(bert_path, map_location="cpu"))
264
  model.eval()
265
  bert_model = model
266
-
267
- # 4) KeyBERT (aynΔ± zamanda hibrit sΔ±nΔ±flandΔ±rΔ±cΔ±nΔ±n embedder'Δ±)
268
- log.info("KeyBERT yΓΌkleniyor...")
269
  kw_model = KeyBERT(model='sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
270
  embedder = kw_model.model.embedding_model
271
-
 
272
  # 5) ByT5
273
- log.info("ByT5 indiriliyor (1.1 GB)...")
274
- byt5_dir = snapshot_download(
275
- repo_id=f"{HF_USERNAME}/liftup-byt5", **auth
276
- )
277
  byt5_tok = AutoTokenizer.from_pretrained("google/byt5-small")
278
  byt5_mdl = AutoModelForSeq2SeqLM.from_pretrained(byt5_dir)
279
  byt5_mdl.eval()
280
  post = SoftPostProcessor()
281
-
282
  class Generator:
283
  def __init__(self, tok, mdl, pp):
284
  self.tok, self.mdl, self.pp = tok, mdl, pp
285
  def generate(self, title="", abstract=""):
286
  text = f"keywords: {title} {abstract}".strip()
287
- inp = self.tok(text, max_length=512, truncation=True, return_tensors="pt")
288
  with torch.no_grad():
289
  out = self.mdl.generate(**inp, max_new_tokens=128, do_sample=False,
290
  no_repeat_ngram_size=4, repetition_penalty=1.5)
291
  pred = self.tok.decode(out[0], skip_special_tokens=True)
292
- if pred.lower().startswith("keywords:"): pred=pred[9:].strip()
293
- kws = [k.strip() for k in pred.split(';') if k.strip()]
294
  return self.pp.process(kws)
295
-
296
  generator = Generator(byt5_tok, byt5_mdl, post)
297
-
298
- # 6) Hibrit sΔ±nΔ±flandΔ±rΔ±cΔ±
299
- log.info("Hibrit sınıflandırıcı başlatılıyor...")
300
- clf = HibritSiniflandirici(taxonomy, embedder)
 
 
 
 
 
 
 
301
  log.info("βœ… TΓΌm modeller hazΔ±r!")
302
-
303
-
304
  # ═══════════════════════════════════════════════════════════════════
305
  # FASTAPI
306
  # ═══════════════════════════════════════════════════════════════════
@@ -308,100 +400,104 @@ def load_models():
308
  async def lifespan(app: FastAPI):
309
  load_models()
310
  yield
311
-
312
- app = FastAPI(title="LIFT UP SΔ±nΔ±flandΔ±rΔ±cΔ±", lifespan=lifespan)
313
-
314
- app.add_middleware(
315
- CORSMiddleware,
316
- allow_origins=["*"],
317
- allow_methods=["POST","GET"],
318
- allow_headers=["*"],
319
- )
320
-
321
  class ClassifyRequest(BaseModel):
322
- baslik: str
323
- ozet: str
324
  keywords: Optional[List[str]] = None
325
-
326
-
327
  class KategoriResponse(BaseModel):
328
- kategori: str
329
- guven: float
330
- keyword_skor: float
331
  semantic_skor: float
332
- eslesmeler: List[str]
333
-
334
-
335
  class ClassifyResponse(BaseModel):
336
- prediction: str
337
- confidence: float
338
- top_3: List[KategoriResponse]
339
- extracted_keywords: List[str]
340
- processing_time_ms: int
341
-
342
-
 
 
343
  def bert_extract(text):
344
  enc = bert_tok(str(text).lower(), truncation=True, padding='max_length',
345
  max_length=256, return_tensors='pt')
346
  with torch.no_grad():
347
  logits = bert_model(enc['input_ids'], enc['attention_mask'])
348
- probs = torch.sigmoid(logits)[0].numpy()
349
  idxs = np.argsort(probs)[-10:][::-1]
350
- return [TOP_KEYWORDS[i] for i in idxs if probs[i]>0.01][:5]
351
-
352
  def keybert_extract(text):
353
- clean = re.sub(r'[^\w\sΔŸΓΌΕŸΔ±ΓΆΓ§ΔžΓœΕžΔ°Γ–Γ‡]',' ', text.lower()).strip()
354
  try:
355
  kws = kw_model.extract_keywords(clean, keyphrase_ngram_range=(1,3),
356
  top_n=5, use_mmr=True, diversity=0.2)
357
  return [k[0] for k in kws][:3]
358
  except:
359
  return []
360
-
361
-
362
  @app.get("/health")
363
- def health(): return {"status": "ok"}
364
-
365
-
366
  @app.get("/")
367
- def root(): return {"message": "LIFT UP API çalışıyor", "endpoint": "POST /classify"}
368
-
369
-
 
 
370
  @app.post("/classify", response_model=ClassifyResponse)
371
  def classify(req: ClassifyRequest):
372
  if not req.baslik.strip() or not req.ozet.strip():
373
  raise HTTPException(400, "BaşlΔ±k ve ΓΆzet zorunludur")
374
-
375
- t0 = time.time()
376
  text = f"{req.baslik} {req.ozet}"
377
-
378
- # Keyword extraction
379
  bert_kws = bert_extract(text)
380
  kb_kws = keybert_extract(text)
381
  byt5_kws = generator.generate(req.baslik, req.ozet)
382
-
383
- # KullanΔ±cΔ± keyword'leri varsa ekle
384
- extra = req.keywords or []
385
- tum_kws = list(dict.fromkeys(bert_kws + kb_kws + byt5_kws + extra))
386
-
387
- # SΔ±nΔ±flandΔ±rma
388
- sonuc = clf.classify(tum_kws, text, top_k=3)
389
-
390
- ms = int((time.time()-t0)*1000)
391
-
 
 
 
392
  return ClassifyResponse(
393
- prediction=sonuc['prediction'],
394
- confidence=round(sonuc['confidence'], 4),
395
- top_3=[
396
  KategoriResponse(
397
- kategori=ks.kategori,
398
- guven=round(ks.final_skor, 4),
399
- keyword_skor=round(ks.keyword_skor, 4),
400
- semantic_skor=round(ks.semantic_skor, 4),
401
- eslesmeler=[e.keyword for e in ks.eslesmeler],
402
  )
403
  for ks in sonuc['top_k']
404
  ],
405
- extracted_keywords=tum_kws,
406
- processing_time_ms=ms,
 
 
407
  )
 
 
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
 
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
  # ═══════════════════════════════════════════════════════════════════
 
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:
 
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)):
 
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
139
+ self.sem_w = semantic_weight
140
+ self.title_boost = title_boost
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
  # ═══════════════════════════════════════════════════════════════════
 
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),
492
+ keyword_skor = round(ks.keyword_skor, 4),
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
+