Spaces:
Sleeping
Sleeping
⚡ Build optimization: Python 3.11 + CPU torch + lazy load
Browse files- README.md +28 -7
- app.py +801 -0
- gradio_app_css.py +215 -0
- requirements.txt +13 -0
README.md
CHANGED
|
@@ -1,12 +1,33 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CV Matching Engine
|
| 3 |
+
emoji: ⚡
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.9.1
|
| 8 |
+
python_version: "3.11"
|
| 9 |
app_file: app.py
|
| 10 |
+
pinned: true
|
| 11 |
+
license: apache-2.0
|
| 12 |
+
short_description: PDF CV → Semantic NLP → Hybrid Scoring → Explainable Ranking
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# ⚡ CV Matching Engine
|
| 16 |
+
|
| 17 |
+
PDF CV dosyalarını yükle, iş ilanını gir — anında semantik analiz, hibrit skor ve XAI ile sıralama.
|
| 18 |
+
|
| 19 |
+
## Özellikler
|
| 20 |
+
- 📄 PDF CV ayrıştırma (dual-column destekli)
|
| 21 |
+
- 🧠 100+ canonical skill ontolojisi (Türkçe dahil)
|
| 22 |
+
- 🔢 Sentence-BERT semantik embedding
|
| 23 |
+
- ⚖️ Ayarlanabilir hibrit skor (Semantic + Skill + Deneyim)
|
| 24 |
+
- 🔍 Explainable AI — Neden bu skor?
|
| 25 |
+
- 📊 Plotly interaktif dashboard
|
| 26 |
+
|
| 27 |
+
## Kullanım
|
| 28 |
+
1. PDF CV dosyalarını yükle (çoklu seçim desteklenir)
|
| 29 |
+
2. İş ilanı açıklamasını gir
|
| 30 |
+
3. Ağırlık slider'larını ayarla (opsiyonel)
|
| 31 |
+
4. **▶ ANALİZİ BAŞLAT** butonuna tıkla
|
| 32 |
+
|
| 33 |
+
Geliştiren: [cihatyldz](https://huggingface.co/cihatyldz)
|
app.py
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 2 |
+
# CV MATCHING ENGINE — Hugging Face Space
|
| 3 |
+
# Auto-generated from notebook
|
| 4 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 5 |
+
|
| 6 |
+
import os, re, json, warnings, tempfile
|
| 7 |
+
|
| 8 |
+
# ════════════════════════════════════════════════════════════════════════════
|
| 9 |
+
# MONKEY-PATCH: gradio_client bug fix
|
| 10 |
+
# gradio 4.44.0'ın get_type() fonksiyonu additionalProperties=True (bool)
|
| 11 |
+
# durumunu işleyemiyor, "TypeError: argument of type bool is not iterable"
|
| 12 |
+
# veriyor. Aşağıdaki patch bool/None schema'ları "Any" olarak işler.
|
| 13 |
+
# ════════════════════════════════════════════════════════════════════════════
|
| 14 |
+
import gradio_client.utils as _gcu
|
| 15 |
+
_orig_get_type = _gcu.get_type
|
| 16 |
+
def _safe_get_type(schema):
|
| 17 |
+
if not isinstance(schema, dict):
|
| 18 |
+
return "Any"
|
| 19 |
+
return _orig_get_type(schema)
|
| 20 |
+
_gcu.get_type = _safe_get_type
|
| 21 |
+
|
| 22 |
+
_orig_json_schema = _gcu._json_schema_to_python_type
|
| 23 |
+
def _safe_json_schema(schema, defs=None):
|
| 24 |
+
if not isinstance(schema, dict):
|
| 25 |
+
return "Any"
|
| 26 |
+
return _orig_json_schema(schema, defs)
|
| 27 |
+
_gcu._json_schema_to_python_type = _safe_json_schema
|
| 28 |
+
|
| 29 |
+
import fitz
|
| 30 |
+
import spacy
|
| 31 |
+
import gradio as gr
|
| 32 |
+
import pandas as pd
|
| 33 |
+
import numpy as np
|
| 34 |
+
import plotly.graph_objects as go
|
| 35 |
+
from sentence_transformers import SentenceTransformer
|
| 36 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 37 |
+
from collections import defaultdict
|
| 38 |
+
from typing import List, Dict
|
| 39 |
+
|
| 40 |
+
warnings.filterwarnings("ignore")
|
| 41 |
+
|
| 42 |
+
# ── Model yükleme — LAZY LOAD ───────────────────────────────────────────────
|
| 43 |
+
# Modeller startup'ta değil, ilk kullanımda yüklenir. Bu sayede:
|
| 44 |
+
# - Space çabuk başlar (timeout yemez)
|
| 45 |
+
# - RAM verimli kullanılır
|
| 46 |
+
# - HF cache'i ikinci başlatmada modelleri tekrar indirmez
|
| 47 |
+
_nlp = None
|
| 48 |
+
_embed_model = None
|
| 49 |
+
|
| 50 |
+
def get_nlp():
|
| 51 |
+
global _nlp
|
| 52 |
+
if _nlp is None:
|
| 53 |
+
print("[INFO] spaCy modeli yükleniyor (ilk kullanım)...")
|
| 54 |
+
_nlp = spacy.load("en_core_web_sm")
|
| 55 |
+
return _nlp
|
| 56 |
+
|
| 57 |
+
def get_embed_model():
|
| 58 |
+
global _embed_model
|
| 59 |
+
if _embed_model is None:
|
| 60 |
+
print("[INFO] Sentence-BERT modeli yükleniyor (ilk kullanım)...")
|
| 61 |
+
_embed_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
|
| 62 |
+
return _embed_model
|
| 63 |
+
|
| 64 |
+
print("[INFO] App başlatıldı — modeller lazy load modunda.")
|
| 65 |
+
|
| 66 |
+
# ────────────────────────────────────────────────────────────────────────────
|
| 67 |
+
# SKILL ONTOLOJİSİ
|
| 68 |
+
# ────────────────────────────────────────────────────────────────────────────
|
| 69 |
+
SKILL_ONTOLOGY: Dict[str, List[str]] = {
|
| 70 |
+
"python" : ["python3", "python 3", "py", "cpython"],
|
| 71 |
+
"java" : ["java se", "java ee", "java 8", "java 11", "java 17", "jvm", "openjdk"],
|
| 72 |
+
"c#" : ["csharp", "c sharp", ".net c#", "dotnet csharp"],
|
| 73 |
+
"c++" : ["cpp", "c plus plus", "c/c++"],
|
| 74 |
+
"c" : ["c language", "c programming", "ansi c", "embedded c"],
|
| 75 |
+
"javascript" : ["js", "ecmascript", "es6", "es2015", "vanilla js"],
|
| 76 |
+
"typescript" : ["ts", "typed javascript"],
|
| 77 |
+
"kotlin" : ["kotlin jvm", "kotlin android"],
|
| 78 |
+
"swift" : ["swift ios", "swiftui", "apple swift"],
|
| 79 |
+
"go" : ["golang", "go language"],
|
| 80 |
+
"rust" : ["rust lang", "rust programming"],
|
| 81 |
+
"php" : ["php7", "php8"],
|
| 82 |
+
"ruby" : ["ruby on rails", "ror", "rails"],
|
| 83 |
+
"scala" : ["scala jvm", "akka"],
|
| 84 |
+
"r" : ["rstudio", "r programming", "r language"],
|
| 85 |
+
"matlab" : ["matlab/simulink", "simulink", "mathworks"],
|
| 86 |
+
"dart" : ["dart flutter"],
|
| 87 |
+
"bash" : ["shell scripting", "bash scripting", "shell script", "sh", "zsh"],
|
| 88 |
+
"powershell" : ["ps1", "windows powershell", "pwsh"],
|
| 89 |
+
"vba" : ["visual basic for applications", "excel vba"],
|
| 90 |
+
"react" : ["reactjs", "react.js", "react hooks", "next.js", "nextjs"],
|
| 91 |
+
"angular" : ["angularjs", "angular 2+", "angular cli", "ng"],
|
| 92 |
+
"vue" : ["vuejs", "vue.js", "vue 3", "nuxt", "nuxt.js"],
|
| 93 |
+
"svelte" : ["sveltekit"],
|
| 94 |
+
"html" : ["html5"],
|
| 95 |
+
"css" : ["css3", "scss", "sass", "less"],
|
| 96 |
+
"tailwind" : ["tailwindcss", "tailwind css"],
|
| 97 |
+
"bootstrap" : ["bootstrap 5", "bootstrap css"],
|
| 98 |
+
"graphql" : ["graphql api", "apollo graphql"],
|
| 99 |
+
"node.js" : ["nodejs", "node js", "express", "express.js", "expressjs"],
|
| 100 |
+
"django" : ["django rest", "drf", "django framework"],
|
| 101 |
+
"flask" : ["flask api", "flask python"],
|
| 102 |
+
"fastapi" : ["fast api", "fastapi python"],
|
| 103 |
+
"spring boot" : ["spring", "spring framework", "spring mvc", "spring security"],
|
| 104 |
+
"asp.net" : ["asp net", "asp.net core", ".net core", "dotnet core", "aspnet", "mvc .net"],
|
| 105 |
+
"laravel" : ["laravel php", "laravel framework"],
|
| 106 |
+
"rest api" : ["restful api", "rest", "restful", "web api", "http api"],
|
| 107 |
+
"microservices" : ["micro services", "soa", "distributed systems"],
|
| 108 |
+
"android" : ["android development", "android studio", "android sdk"],
|
| 109 |
+
"ios" : ["ios development", "xcode", "objective-c"],
|
| 110 |
+
"flutter" : ["flutter dart", "flutter mobile"],
|
| 111 |
+
"react native" : ["react-native", "rn mobile"],
|
| 112 |
+
"sql" : ["structured query language", "ansi sql"],
|
| 113 |
+
"mysql" : ["mysql database", "mysql server"],
|
| 114 |
+
"postgresql" : ["postgres", "psql", "postgresql database"],
|
| 115 |
+
"mssql" : ["sql server", "microsoft sql server", "t-sql", "tsql", "ms sql"],
|
| 116 |
+
"oracle" : ["oracle database", "oracle db", "pl/sql", "plsql"],
|
| 117 |
+
"sqlite" : ["sqlite3"],
|
| 118 |
+
"mongodb" : ["mongo", "mongo db", "mongodb atlas"],
|
| 119 |
+
"redis" : ["redis cache", "redis server"],
|
| 120 |
+
"cassandra" : ["apache cassandra", "cql"],
|
| 121 |
+
"elasticsearch" : ["elastic search", "elk stack", "opensearch"],
|
| 122 |
+
"dynamodb" : ["aws dynamodb", "amazon dynamodb"],
|
| 123 |
+
"neo4j" : ["graph database", "cypher query"],
|
| 124 |
+
"firebase" : ["firebase realtime", "firestore"],
|
| 125 |
+
"aws" : ["amazon web services", "amazon aws", "ec2", "s3", "lambda",
|
| 126 |
+
"rds", "eks", "ecs", "sagemaker", "cloudwatch"],
|
| 127 |
+
"azure" : ["microsoft azure", "azure cloud", "azure devops",
|
| 128 |
+
"azure functions", "aks", "azure blob", "azure sql"],
|
| 129 |
+
"google cloud" : ["gcp", "google cloud platform", "gke", "bigquery",
|
| 130 |
+
"cloud run", "vertex ai"],
|
| 131 |
+
"docker" : ["containerization", "container", "dockerfile", "docker-compose"],
|
| 132 |
+
"kubernetes" : ["k8s", "helm", "kubectl", "container orchestration", "openshift"],
|
| 133 |
+
"git" : ["github", "gitlab", "bitbucket", "version control", "git flow"],
|
| 134 |
+
"jenkins" : ["jenkins ci", "jenkins pipeline"],
|
| 135 |
+
"github actions" : ["gh actions", "github ci", "github workflow"],
|
| 136 |
+
"gitlab ci" : ["gitlab cicd", "gitlab pipeline"],
|
| 137 |
+
"terraform" : ["hashicorp terraform", "infrastructure as code", "iac"],
|
| 138 |
+
"ansible" : ["ansible playbook", "ansible automation"],
|
| 139 |
+
"prometheus" : ["prometheus monitoring"],
|
| 140 |
+
"grafana" : ["grafana dashboard", "grafana monitoring"],
|
| 141 |
+
"nginx" : ["nginx server", "nginx reverse proxy"],
|
| 142 |
+
"linux" : ["ubuntu", "centos", "debian", "rhel", "red hat",
|
| 143 |
+
"linux server", "unix", "linux administration"],
|
| 144 |
+
"apache spark" : ["spark", "pyspark", "spark streaming"],
|
| 145 |
+
"hadoop" : ["apache hadoop", "hdfs", "mapreduce", "hive"],
|
| 146 |
+
"kafka" : ["apache kafka", "kafka streaming", "event streaming"],
|
| 147 |
+
"airflow" : ["apache airflow", "airflow dag"],
|
| 148 |
+
"dbt" : ["data build tool", "dbt core"],
|
| 149 |
+
"etl" : ["extract transform load", "data pipeline", "data integration"],
|
| 150 |
+
"data warehouse" : ["snowflake", "redshift", "bigquery", "azure synapse", "data lake"],
|
| 151 |
+
"data analysis" : ["veri analizi", "data analytics", "exploratory data analysis",
|
| 152 |
+
"eda", "data mining", "veri madenciliği"],
|
| 153 |
+
"pandas" : ["pandas dataframe", "pandas python"],
|
| 154 |
+
"numpy" : ["numpy array", "numerical python"],
|
| 155 |
+
"tableau" : ["tableau desktop", "tableau server"],
|
| 156 |
+
"power bi" : ["powerbi", "microsoft power bi", "power bi desktop"],
|
| 157 |
+
"machine learning": ["ml", "makine öğrenmesi", "makine öğrenimi",
|
| 158 |
+
"supervised learning", "unsupervised learning",
|
| 159 |
+
"classification", "regression", "clustering"],
|
| 160 |
+
"deep learning" : ["dl", "derin öğrenme", "neural network", "ann", "dnn",
|
| 161 |
+
"cnn", "rnn", "lstm", "gru"],
|
| 162 |
+
"natural language processing": ["nlp", "doğal dil işleme", "text mining",
|
| 163 |
+
"transformers", "text classification", "ner", "sentiment analysis"],
|
| 164 |
+
"computer vision": ["cv", "image processing", "görüntü işleme", "opencv",
|
| 165 |
+
"object detection", "yolo", "image classification"],
|
| 166 |
+
"reinforcement learning": ["rl", "pekiştirmeli öğrenme", "dqn", "ppo"],
|
| 167 |
+
"large language model": ["llm", "gpt", "bert", "llama", "fine-tuning", "rag",
|
| 168 |
+
"prompt engineering", "chatgpt", "gemini", "mistral"],
|
| 169 |
+
"tensorflow" : ["tf", "keras", "tensorflow 2"],
|
| 170 |
+
"pytorch" : ["torch", "pytorch lightning"],
|
| 171 |
+
"scikit-learn" : ["sklearn", "scikit learn"],
|
| 172 |
+
"hugging face" : ["huggingface", "transformers library", "hf hub"],
|
| 173 |
+
"langchain" : ["lang chain", "langchain python"],
|
| 174 |
+
"xgboost" : ["extreme gradient boosting", "xgb", "lightgbm", "lgbm", "catboost"],
|
| 175 |
+
"time series" : ["zaman serisi", "forecasting", "talep tahmini",
|
| 176 |
+
"arima", "sarima", "prophet"],
|
| 177 |
+
"cybersecurity" : ["siber güvenlik", "information security", "bilgi güvenliği"],
|
| 178 |
+
"penetration testing": ["pentest", "ethical hacking", "metasploit", "burp suite"],
|
| 179 |
+
"soc" : ["security operations center", "siem", "soc analyst", "splunk"],
|
| 180 |
+
"devsecops" : ["dev sec ops", "sast", "dast"],
|
| 181 |
+
"identity management": ["iam", "active directory", "ldap", "sso", "okta"],
|
| 182 |
+
"software architecture": ["yazılım mimarisi", "solution architecture",
|
| 183 |
+
"system design", "design patterns", "solid principles"],
|
| 184 |
+
"unit testing" : ["tdd", "test driven development", "junit", "pytest",
|
| 185 |
+
"nunit", "xunit", "jest"],
|
| 186 |
+
"agile" : ["scrum", "kanban", "sprint", "backlog", "çevik", "safe"],
|
| 187 |
+
"sap" : ["sap erp", "sap s/4hana", "sap hana", "sap abap",
|
| 188 |
+
"sap bw", "sap fi", "sap mm", "sap sd"],
|
| 189 |
+
"salesforce" : ["salesforce crm", "apex salesforce", "soql"],
|
| 190 |
+
"microsoft dynamics": ["dynamics 365", "dynamics crm", "business central"],
|
| 191 |
+
"servicenow" : ["service now", "itsm servicenow"],
|
| 192 |
+
"jira" : ["jira software", "atlassian jira", "confluence"],
|
| 193 |
+
"selenium" : ["selenium webdriver", "selenium automation"],
|
| 194 |
+
"cypress" : ["cypress testing", "cypress e2e"],
|
| 195 |
+
"playwright" : ["playwright testing", "playwright e2e"],
|
| 196 |
+
"postman" : ["postman api testing", "api testing"],
|
| 197 |
+
"qa" : ["quality assurance", "kalite güvencesi", "test automation"],
|
| 198 |
+
"networking" : ["tcp/ip", "dns", "dhcp", "routing", "switching", "cisco"],
|
| 199 |
+
"windows server" : ["windows server 2019", "windows server 2022", "iis"],
|
| 200 |
+
"virtualization" : ["vmware", "vsphere", "hyper-v", "virtualbox", "proxmox"],
|
| 201 |
+
"iot" : ["internet of things", "nesnelerin interneti", "mqtt"],
|
| 202 |
+
"embedded systems": ["gömülü sistemler", "rtos", "freertos", "microcontroller"],
|
| 203 |
+
"arduino" : ["arduino uno", "arduino ide"],
|
| 204 |
+
"raspberry pi" : ["rpi", "raspberry pi os"],
|
| 205 |
+
"fpga" : ["vhdl", "verilog", "xilinx"],
|
| 206 |
+
"ros" : ["robot operating system", "ros2"],
|
| 207 |
+
"control systems": ["kontrol sistemleri", "pid controller", "pid", "control theory"],
|
| 208 |
+
"robotics" : ["robot programlama", "robot kinematics"],
|
| 209 |
+
"plc" : ["programmable logic controller", "siemens plc", "ladder logic", "scada"],
|
| 210 |
+
"solidworks" : ["cad", "catia", "autocad", "fusion 360"],
|
| 211 |
+
"supply chain" : ["tedarik zinciri", "logistics", "lojistik", "scm"],
|
| 212 |
+
"vehicle routing": ["vrp", "route optimization", "güzergah optimizasyonu"],
|
| 213 |
+
"demand forecasting": ["talep tahmini", "demand planning", "inventory forecasting"],
|
| 214 |
+
"warehouse management": ["wms", "depo yönetimi"],
|
| 215 |
+
"operations research": ["yöneylem araştırması", "linear programming",
|
| 216 |
+
"optimization", "mathematical optimization"],
|
| 217 |
+
"project management": ["proje yönetimi", "pmp", "prince2", "waterfall"],
|
| 218 |
+
"team leadership" : ["takım liderliği", "people management", "tech lead", "mentoring"],
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
SKILL_CATEGORIES: Dict[str, str] = {
|
| 222 |
+
"python":"Prog. Dili","java":"Prog. Dili","c#":"Prog. Dili","c++":"Prog. Dili",
|
| 223 |
+
"c":"Prog. Dili","javascript":"Prog. Dili","typescript":"Prog. Dili",
|
| 224 |
+
"kotlin":"Prog. Dili","swift":"Prog. Dili","go":"Prog. Dili","rust":"Prog. Dili",
|
| 225 |
+
"php":"Prog. Dili","ruby":"Prog. Dili","scala":"Prog. Dili","r":"Prog. Dili",
|
| 226 |
+
"matlab":"Prog. Dili","dart":"Prog. Dili","bash":"Prog. Dili",
|
| 227 |
+
"powershell":"Prog. Dili","vba":"Prog. Dili",
|
| 228 |
+
"react":"Web Frontend","angular":"Web Frontend","vue":"Web Frontend",
|
| 229 |
+
"svelte":"Web Frontend","html":"Web Frontend","css":"Web Frontend",
|
| 230 |
+
"tailwind":"Web Frontend","bootstrap":"Web Frontend","graphql":"Web Frontend",
|
| 231 |
+
"node.js":"Backend","django":"Backend","flask":"Backend","fastapi":"Backend",
|
| 232 |
+
"spring boot":"Backend","asp.net":"Backend","laravel":"Backend",
|
| 233 |
+
"rest api":"Backend","microservices":"Backend",
|
| 234 |
+
"android":"Mobil","ios":"Mobil","flutter":"Mobil","react native":"Mobil",
|
| 235 |
+
"sql":"Veritabanı","mysql":"Veritabanı","postgresql":"Veritabanı",
|
| 236 |
+
"mssql":"Veritabanı","oracle":"Veritabanı","sqlite":"Veritabanı",
|
| 237 |
+
"mongodb":"Veritabanı","redis":"Veritabanı","cassandra":"Veritabanı",
|
| 238 |
+
"elasticsearch":"Veritabanı","dynamodb":"Veritabanı","neo4j":"Veritabanı",
|
| 239 |
+
"firebase":"Veritabanı",
|
| 240 |
+
"aws":"Bulut","azure":"Bulut","google cloud":"Bulut",
|
| 241 |
+
"docker":"DevOps","kubernetes":"DevOps","git":"DevOps","jenkins":"DevOps",
|
| 242 |
+
"github actions":"DevOps","gitlab ci":"DevOps","terraform":"DevOps",
|
| 243 |
+
"ansible":"DevOps","prometheus":"DevOps","grafana":"DevOps",
|
| 244 |
+
"nginx":"DevOps","linux":"DevOps",
|
| 245 |
+
"apache spark":"Veri Müh.","hadoop":"Veri Müh.","kafka":"Veri Müh.",
|
| 246 |
+
"airflow":"Veri Müh.","dbt":"Veri Müh.","etl":"Veri Müh.",
|
| 247 |
+
"data warehouse":"Veri Müh.","data analysis":"Veri Müh.",
|
| 248 |
+
"pandas":"Veri Müh.","numpy":"Veri Müh.","tableau":"Veri Müh.","power bi":"Veri Müh.",
|
| 249 |
+
"machine learning":"Yapay Zeka","deep learning":"Yapay Zeka",
|
| 250 |
+
"natural language processing":"Yapay Zeka","computer vision":"Yapay Zeka",
|
| 251 |
+
"reinforcement learning":"Yapay Zeka","large language model":"Yapay Zeka",
|
| 252 |
+
"tensorflow":"Yapay Zeka","pytorch":"Yapay Zeka","scikit-learn":"Yapay Zeka",
|
| 253 |
+
"hugging face":"Yapay Zeka","langchain":"Yapay Zeka",
|
| 254 |
+
"xgboost":"Yapay Zeka","time series":"Yapay Zeka",
|
| 255 |
+
"cybersecurity":"Siber Güv.","penetration testing":"Siber Güv.",
|
| 256 |
+
"soc":"Siber Güv.","devsecops":"Siber Güv.","identity management":"Siber Güv.",
|
| 257 |
+
"software architecture":"Yazılım Mimarisi","unit testing":"Yazılım Mimarisi",
|
| 258 |
+
"agile":"Yazılım Mimarisi",
|
| 259 |
+
"sap":"ERP/CRM","salesforce":"ERP/CRM","microsoft dynamics":"ERP/CRM",
|
| 260 |
+
"servicenow":"ERP/CRM","jira":"ERP/CRM",
|
| 261 |
+
"selenium":"Testing/QA","cypress":"Testing/QA","playwright":"Testing/QA",
|
| 262 |
+
"postman":"Testing/QA","qa":"Testing/QA",
|
| 263 |
+
"networking":"Sistem/Altyapı","windows server":"Sistem/Altyapı",
|
| 264 |
+
"virtualization":"Sistem/Altyapı",
|
| 265 |
+
"iot":"IoT/Gömülü","embedded systems":"IoT/Gömülü",
|
| 266 |
+
"arduino":"IoT/Gömülü","raspberry pi":"IoT/Gömülü","fpga":"IoT/Gömülü",
|
| 267 |
+
"ros":"Robotik","control systems":"Robotik","robotics":"Robotik",
|
| 268 |
+
"plc":"Robotik","solidworks":"Robotik",
|
| 269 |
+
"supply chain":"Lojistik","vehicle routing":"Lojistik",
|
| 270 |
+
"demand forecasting":"Lojistik","warehouse management":"Lojistik",
|
| 271 |
+
"operations research":"Lojistik",
|
| 272 |
+
"project management":"Soft Skill","team leadership":"Soft Skill",
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
SYNONYM_TO_CANONICAL: Dict[str, str] = {}
|
| 276 |
+
for canonical, synonyms in SKILL_ONTOLOGY.items():
|
| 277 |
+
SYNONYM_TO_CANONICAL[canonical] = canonical
|
| 278 |
+
for syn in synonyms:
|
| 279 |
+
SYNONYM_TO_CANONICAL[syn.lower()] = canonical
|
| 280 |
+
|
| 281 |
+
SCORE_WEIGHTS = {"semantic": 0.55, "skill_overlap": 0.35, "experience": 0.10}
|
| 282 |
+
|
| 283 |
+
# ── Utility functions ────────────────────────────────────────────────────────
|
| 284 |
+
def extract_text_from_pdf(pdf_path, dual_column=False):
|
| 285 |
+
doc = fitz.open(pdf_path)
|
| 286 |
+
full_text = ""
|
| 287 |
+
for page in doc:
|
| 288 |
+
if dual_column:
|
| 289 |
+
w = page.rect.width
|
| 290 |
+
full_text += page.get_text("text", clip=fitz.Rect(0,0,w/2,page.rect.height))
|
| 291 |
+
full_text += page.get_text("text", clip=fitz.Rect(w/2,0,w,page.rect.height))
|
| 292 |
+
else:
|
| 293 |
+
full_text += page.get_text("text")
|
| 294 |
+
doc.close()
|
| 295 |
+
return re.sub(r"\s+", " ", full_text).strip()
|
| 296 |
+
|
| 297 |
+
def extract_skills(text):
|
| 298 |
+
text_lower = text.lower()
|
| 299 |
+
canonical_found = set()
|
| 300 |
+
for term, canonical in SYNONYM_TO_CANONICAL.items():
|
| 301 |
+
if re.search(r"\b" + re.escape(term) + r"\b", text_lower):
|
| 302 |
+
canonical_found.add(canonical)
|
| 303 |
+
by_category = defaultdict(list)
|
| 304 |
+
for skill in canonical_found:
|
| 305 |
+
by_category[SKILL_CATEGORIES.get(skill, "Diğer")].append(skill)
|
| 306 |
+
return {"canonical_skills": sorted(canonical_found), "by_category": dict(by_category)}
|
| 307 |
+
|
| 308 |
+
def get_embedding(text):
|
| 309 |
+
return get_embed_model().encode(text, show_progress_bar=False)
|
| 310 |
+
|
| 311 |
+
def compute_semantic_similarity(t1, t2):
|
| 312 |
+
return float(cosine_similarity([get_embedding(t1)], [get_embedding(t2)])[0][0])
|
| 313 |
+
|
| 314 |
+
def skill_overlap_score(cv_skills, job_skills):
|
| 315 |
+
if not job_skills: return 0.0
|
| 316 |
+
s1, s2 = set(cv_skills), set(job_skills)
|
| 317 |
+
return len(s1 & s2) / len(s1 | s2) if (s1 | s2) else 0.0
|
| 318 |
+
|
| 319 |
+
def extract_experience_years(text):
|
| 320 |
+
m = re.search(r"(\d+)\+?\s*(?:years?|yıl)\s*(?:of\s+)?(?:experience|deneyim)", text.lower())
|
| 321 |
+
return float(m.group(1)) if m else 0.0
|
| 322 |
+
|
| 323 |
+
def hybrid_score(sem, skill, exp, req_years, weights):
|
| 324 |
+
exp_sc = min(exp / req_years, 1.0) if req_years > 0 else 1.0
|
| 325 |
+
return weights["semantic"]*sem + weights["skill_overlap"]*skill + weights["experience"]*exp_sc, exp_sc
|
| 326 |
+
|
| 327 |
+
def evaluate_candidate(cv_text, job_description, job_skills, req_years, name, weights):
|
| 328 |
+
cv_sr = extract_skills(cv_text)
|
| 329 |
+
cv_skills = cv_sr["canonical_skills"]
|
| 330 |
+
sem = compute_semantic_similarity(cv_text, job_description)
|
| 331 |
+
jac = skill_overlap_score(cv_skills, job_skills)
|
| 332 |
+
exp = extract_experience_years(cv_text)
|
| 333 |
+
final, exp_sc = hybrid_score(sem, jac, exp, req_years, weights)
|
| 334 |
+
matched = sorted(set(cv_skills) & set(job_skills))
|
| 335 |
+
missing = sorted(set(job_skills) - set(cv_skills))
|
| 336 |
+
extra = sorted(set(cv_skills) - set(job_skills))
|
| 337 |
+
verdict = ("🟢 Güçlü Eşleşme" if final>=0.75 else
|
| 338 |
+
"🟡 Orta Eşleşme" if final>=0.55 else "🔴 Zayıf Eşleşme")
|
| 339 |
+
return {"name":name,"cv_skills":cv_skills,"skill_by_category":cv_sr["by_category"],
|
| 340 |
+
"semantic_score":round(sem,4),"skill_score":round(jac,4),
|
| 341 |
+
"experience_years":exp,"experience_score":round(exp_sc,4),
|
| 342 |
+
"final_score":round(final,4),
|
| 343 |
+
"explanation":{"verdict":verdict,"matched_skills":matched,
|
| 344 |
+
"missing_skills":missing,"extra_skills":extra}}
|
| 345 |
+
|
| 346 |
+
# ── HTML / Plotly helpers ────────────────────────────────────────────────────
|
| 347 |
+
def make_score_pill(score):
|
| 348 |
+
cls = "score-high" if score>=0.75 else "score-mid" if score>=0.55 else "score-low"
|
| 349 |
+
return f"<span class='score-pill {cls}'>{score:.3f}</span>"
|
| 350 |
+
|
| 351 |
+
def make_skill_tags(skills, kind):
|
| 352 |
+
if not skills: return "<span style='color:#3d5878;font-size:0.8rem'>—</span>"
|
| 353 |
+
return " ".join(f"<span class='skill-tag skill-{kind}'>{s}</span>" for s in skills)
|
| 354 |
+
|
| 355 |
+
def make_progress_bar(label, value, color="#00d4aa"):
|
| 356 |
+
pct = int(value*100)
|
| 357 |
+
return (f"<div class='prog-bar-wrap'><div class='prog-bar-label'>"
|
| 358 |
+
f"<span>{label}</span><span style='font-family:var(--font-mono);color:{color}'>{value:.4f}</span></div>"
|
| 359 |
+
f"<div class='prog-bar-track'><div class='prog-bar-fill' style='width:{pct}%;background:{color};'></div></div></div>")
|
| 360 |
+
|
| 361 |
+
PLOTLY_LAYOUT = dict(
|
| 362 |
+
paper_bgcolor="rgba(13,24,41,0)", plot_bgcolor="rgba(13,24,41,0)",
|
| 363 |
+
font=dict(family="Rajdhani, Inter, sans-serif", color="#6b8ab0"),
|
| 364 |
+
title_font=dict(family="Rajdhani, sans-serif", color="#00d4aa", size=16),
|
| 365 |
+
margin=dict(l=20,r=20,t=50,b=20),
|
| 366 |
+
colorway=["#00d4aa","#f59e0b","#a78bfa","#ef4444","#22c55e"],
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
def plotly_ranking_bar(df):
|
| 370 |
+
colors = ["#22c55e" if s>=0.75 else "#f59e0b" if s>=0.55 else "#ef4444" for s in df["Final Skor"]]
|
| 371 |
+
fig = go.Figure(go.Bar(
|
| 372 |
+
x=df["Final Skor"], y=df["Aday"], orientation="h",
|
| 373 |
+
marker_color=colors, text=[f"{s:.3f}" for s in df["Final Skor"]],
|
| 374 |
+
textposition="outside", textfont=dict(family="JetBrains Mono",color="#e2eaf6",size=12)))
|
| 375 |
+
fig.add_vline(x=0.75,line_dash="dot",line_color="rgba(34,197,94,0.5)",
|
| 376 |
+
annotation_text="Güçlü",annotation_font_color="#22c55e")
|
| 377 |
+
fig.add_vline(x=0.55,line_dash="dot",line_color="rgba(245,158,11,0.5)",
|
| 378 |
+
annotation_text="Orta",annotation_font_color="#f59e0b")
|
| 379 |
+
fig.update_layout(**PLOTLY_LAYOUT,title="🏆 Nihai Aday Sıralaması",
|
| 380 |
+
xaxis=dict(range=[0,1.05],gridcolor="rgba(30,48,80,0.8)",title="Skor"),
|
| 381 |
+
yaxis=dict(autorange="reversed",gridcolor="rgba(30,48,80,0.8)"),
|
| 382 |
+
height=max(280,len(df)*62))
|
| 383 |
+
return fig
|
| 384 |
+
|
| 385 |
+
def plotly_component_stacked(df):
|
| 386 |
+
w = SCORE_WEIGHTS
|
| 387 |
+
names = df["Aday"].tolist()
|
| 388 |
+
fig = go.Figure()
|
| 389 |
+
fig.add_trace(go.Bar(name="Semantik",x=names,y=(df["Semantic"]*w["semantic"]).tolist(),marker_color="#00d4aa",opacity=0.9))
|
| 390 |
+
fig.add_trace(go.Bar(name="Skill Overlap",x=names,y=(df["Skill Overlap"]*w["skill_overlap"]).tolist(),marker_color="#a78bfa",opacity=0.9))
|
| 391 |
+
fig.add_trace(go.Bar(name="Deneyim",x=names,y=(df["Deneyim (Yıl)"].apply(lambda x:min(x/3,1))*w["experience"]).tolist(),marker_color="#f59e0b",opacity=0.9))
|
| 392 |
+
fig.update_layout(**PLOTLY_LAYOUT,barmode="stack",title="📊 Skor Bileşeni Dağılımı",
|
| 393 |
+
xaxis=dict(gridcolor="rgba(30,48,80,0.8)"),
|
| 394 |
+
yaxis=dict(gridcolor="rgba(30,48,80,0.8)",title="Katkı"),
|
| 395 |
+
legend=dict(bgcolor="rgba(13,24,41,0.8)",bordercolor="#1e3050",borderwidth=1),
|
| 396 |
+
height=360)
|
| 397 |
+
return fig
|
| 398 |
+
|
| 399 |
+
def plotly_skill_gap(df):
|
| 400 |
+
names = df["Aday"].tolist()
|
| 401 |
+
fig = go.Figure()
|
| 402 |
+
fig.add_trace(go.Bar(name="Eşleşen ✅",x=names,y=df["Eşleşen Skill"].tolist(),marker_color="rgba(34,197,94,0.8)"))
|
| 403 |
+
fig.add_trace(go.Bar(name="Eksik ❌",x=names,y=df["Eksik Skill"].tolist(),marker_color="rgba(239,68,68,0.7)"))
|
| 404 |
+
fig.update_layout(**PLOTLY_LAYOUT,barmode="group",title="🔍 Skill Gap Analizi",
|
| 405 |
+
xaxis=dict(gridcolor="rgba(30,48,80,0.8)"),
|
| 406 |
+
yaxis=dict(gridcolor="rgba(30,48,80,0.8)",title="Skill Sayısı"),
|
| 407 |
+
legend=dict(bgcolor="rgba(13,24,41,0.8)",bordercolor="#1e3050",borderwidth=1),
|
| 408 |
+
height=340)
|
| 409 |
+
return fig
|
| 410 |
+
|
| 411 |
+
def plotly_scatter(df):
|
| 412 |
+
fig = go.Figure(go.Scatter(
|
| 413 |
+
x=df["Semantic"],y=df["Skill Overlap"],mode="markers+text",
|
| 414 |
+
text=df["Aday"].apply(lambda n:n.split()[0]),textposition="top center",
|
| 415 |
+
textfont=dict(family="Rajdhani",color="#e2eaf6",size=12),
|
| 416 |
+
marker=dict(size=18,color=df["Final Skor"],
|
| 417 |
+
colorscale=[[0,"#ef4444"],[0.5,"#f59e0b"],[1,"#00d4aa"]],
|
| 418 |
+
showscale=True,colorbar=dict(title="Final Skor",tickfont=dict(color="#6b8ab0")),
|
| 419 |
+
line=dict(width=1,color="#1e3050")),
|
| 420 |
+
hovertemplate="<b>%{text}</b><br>Semantic: %{x:.3f}<br>Skill: %{y:.3f}<extra></extra>"))
|
| 421 |
+
fig.update_layout(**PLOTLY_LAYOUT,title="🔵 Semantic vs Skill Dağılımı",
|
| 422 |
+
xaxis=dict(title="Semantic Similarity",gridcolor="rgba(30,48,80,0.8)",range=[0,1]),
|
| 423 |
+
yaxis=dict(title="Skill Overlap (Jaccard)",gridcolor="rgba(30,48,80,0.8)",range=[0,1]),
|
| 424 |
+
height=360)
|
| 425 |
+
return fig
|
| 426 |
+
|
| 427 |
+
def build_ranking_table(df):
|
| 428 |
+
rows = ""
|
| 429 |
+
for _,r in df.iterrows():
|
| 430 |
+
verdict = r["Değerlendirme"]
|
| 431 |
+
badge_cls = "verdict-strong" if "🟢" in verdict else "verdict-mid" if "🟡" in verdict else "verdict-weak"
|
| 432 |
+
badge_txt = "Güçlü" if "🟢" in verdict else "Orta" if "🟡" in verdict else "Zayıf"
|
| 433 |
+
rows += (f"<tr><td><span class='rank-num'>#{r['Sıra']}</span></td>"
|
| 434 |
+
f"<td><strong style='color:var(--text-primary)'>{r['Aday']}</strong></td>"
|
| 435 |
+
f"<td>{make_score_pill(r['Final Skor'])}</td>"
|
| 436 |
+
f"<td><span style='font-family:var(--font-mono);color:var(--text-muted)'>{r['Semantic']:.3f}</span></td>"
|
| 437 |
+
f"<td><span style='font-family:var(--font-mono);color:var(--text-muted)'>{r['Skill Overlap']:.3f}</span></td>"
|
| 438 |
+
f"<td><span style='font-family:var(--font-mono);color:var(--amber)'>{int(r['Deneyim (Yıl)'])} yıl</span></td>"
|
| 439 |
+
f"<td><span style='color:var(--green)'>{int(r['Eşleşen Skill'])}</span> / "
|
| 440 |
+
f"<span style='color:var(--red)'>{int(r['Eksik Skill'])}</span></td>"
|
| 441 |
+
f"<td><span class='score-pill {badge_cls}'>{badge_txt}</span></td></tr>")
|
| 442 |
+
return ("<table class='rank-table'><thead><tr>"
|
| 443 |
+
"<th>Sıra</th><th>Aday</th><th>Final Skor</th>"
|
| 444 |
+
"<th>Semantik</th><th>Skill</th><th>Deneyim</th>"
|
| 445 |
+
"<th>Eşleşen/Eksik</th><th>Durum</th></tr></thead>"
|
| 446 |
+
f"<tbody>{rows}</tbody></table>")
|
| 447 |
+
|
| 448 |
+
_empty_cat_html = "<span style='color:var(--text-dim);font-size:0.85rem'>Bulunamadı</span>"
|
| 449 |
+
|
| 450 |
+
def build_xai_panel(result):
|
| 451 |
+
exp = result["explanation"]
|
| 452 |
+
verdict = exp["verdict"]
|
| 453 |
+
badge_cls = "verdict-strong" if "🟢" in verdict else "verdict-mid" if "🟡" in verdict else "verdict-weak"
|
| 454 |
+
sem = result["semantic_score"]
|
| 455 |
+
skill = result["skill_score"]
|
| 456 |
+
expsc = result["experience_score"]
|
| 457 |
+
final = result["final_score"]
|
| 458 |
+
w = SCORE_WEIGHTS
|
| 459 |
+
bars = (make_progress_bar(f"Semantik Benzerlik (×{w['semantic']})",sem*w["semantic"],"#00d4aa") +
|
| 460 |
+
make_progress_bar(f"Skill Örtüşmesi (×{w['skill_overlap']})",skill*w["skill_overlap"],"#a78bfa") +
|
| 461 |
+
make_progress_bar(f"Deneyim Bonusu (×{w['experience']})",expsc*w["experience"],"#f59e0b"))
|
| 462 |
+
cats = result.get("skill_by_category",{})
|
| 463 |
+
cat_html = "".join(
|
| 464 |
+
f"<div style='margin-bottom:10px'><div style='font-family:var(--font-display);font-size:0.75rem;letter-spacing:1px;color:var(--text-dim);text-transform:uppercase;margin-bottom:5px'>{cat}</div>{make_skill_tags(skills,'extra')}</div>"
|
| 465 |
+
for cat,skills in cats.items())
|
| 466 |
+
return (f"<div style='display:grid;grid-template-columns:1fr 1fr;gap:20px'>"
|
| 467 |
+
f"<div><div class='panel-title'>📊 Skor Analizi</div>"
|
| 468 |
+
f"<div class='score-grid'>"
|
| 469 |
+
f"<div class='score-card'><div class='val'>{final:.3f}</div><div class='lbl'>Final Skor</div></div>"
|
| 470 |
+
f"<div class='score-card'><div class='val' style='color:#a78bfa'>{sem:.3f}</div><div class='lbl'>Semantik</div></div>"
|
| 471 |
+
f"<div class='score-card'><div class='val' style='color:#f59e0b'>{skill:.3f}</div><div class='lbl'>Skill</div></div>"
|
| 472 |
+
f"<div class='score-card'><div class='val' style='color:#22c55e'>{result['experience_years']:.0f} yıl</div><div class='lbl'>Deneyim</div></div>"
|
| 473 |
+
f"</div><div style='margin:16px 0 8px'><div class='panel-title' style='font-size:0.85rem'>⚖️ Bileşen Katkıları</div></div>"
|
| 474 |
+
f"{bars}"
|
| 475 |
+
f"<div style='margin-top:16px;padding:12px 16px;border-radius:8px' class='{badge_cls}'>"
|
| 476 |
+
f"<div style='font-family:var(--font-display);font-weight:700;font-size:1rem;letter-spacing:1px'>{verdict}</div></div></div>"
|
| 477 |
+
f"<div><div class='panel-title'>🔍 Skill Gap Analizi</div>"
|
| 478 |
+
f"<div style='margin-bottom:14px'><div style='font-family:var(--font-display);font-size:0.78rem;letter-spacing:1px;color:var(--green);margin-bottom:6px'>✅ EŞLEŞEN ({len(exp['matched_skills'])})</div>{make_skill_tags(exp['matched_skills'],'match')}</div>"
|
| 479 |
+
f"<div style='margin-bottom:14px'><div style='font-family:var(--font-display);font-size:0.78rem;letter-spacing:1px;color:var(--red);margin-bottom:6px'>❌ EKSİK ({len(exp['missing_skills'])})</div>{make_skill_tags(exp['missing_skills'],'missing')}</div>"
|
| 480 |
+
f"<hr class='section-divider'><div class='panel-title' style='font-size:0.85rem'>📚 CV Kategorileri</div>{cat_html or _empty_cat_html}</div></div>")
|
| 481 |
+
|
| 482 |
+
# ── Globals ─────────────────────────────────────────────────────────────────
|
| 483 |
+
_last_results = []
|
| 484 |
+
|
| 485 |
+
# ── Main callback ────────────────────────────────────────────────────────────
|
| 486 |
+
def run_analysis(pdf_files, job_desc, sem_weight, skill_weight, exp_weight, required_years, dual_col):
|
| 487 |
+
global _last_results
|
| 488 |
+
ERR = lambda msg: (f"<div class='log-box'><span class='log-warn'>{msg}</span></div>",
|
| 489 |
+
None,None,None,None,gr.update(choices=[],value=None),
|
| 490 |
+
f"<div class='log-box'><span class='log-warn'>{msg}</span></div>")
|
| 491 |
+
if not pdf_files: return ERR("⚠ Lütfen en az bir PDF CV yükleyin.")
|
| 492 |
+
if not job_desc.strip(): return ERR("⚠ İş ilanı boş olamaz.")
|
| 493 |
+
total = sem_weight + skill_weight + exp_weight or 1
|
| 494 |
+
weights = {"semantic":sem_weight/total,"skill_overlap":skill_weight/total,"experience":exp_weight/total}
|
| 495 |
+
candidates, log_lines = [], ["<span class='log-info'>► Analiz başlatıldı...</span>"]
|
| 496 |
+
for f in pdf_files:
|
| 497 |
+
path = f.name if hasattr(f,"name") else f
|
| 498 |
+
fname = os.path.basename(path)
|
| 499 |
+
try:
|
| 500 |
+
text = extract_text_from_pdf(path, dual_column=dual_col)
|
| 501 |
+
candidates.append({"name":fname.replace(".pdf",""),"text":text})
|
| 502 |
+
log_lines.append(f"<span class='log-ok'>✔ Yüklendi: {fname}</span>")
|
| 503 |
+
except Exception as e:
|
| 504 |
+
log_lines.append(f"<span class='log-warn'>✘ Hata ({fname}): {e}</span>")
|
| 505 |
+
if not candidates:
|
| 506 |
+
lh = "<div class='log-box'>"+"<br>".join(log_lines)+"</div>"
|
| 507 |
+
return lh,None,None,None,None,gr.update(choices=[],value=None),lh
|
| 508 |
+
job_skills = extract_skills(job_desc)["canonical_skills"]
|
| 509 |
+
results = []
|
| 510 |
+
for cand in candidates:
|
| 511 |
+
res = evaluate_candidate(cand["text"],job_desc,job_skills,required_years,cand["name"],weights)
|
| 512 |
+
results.append(res)
|
| 513 |
+
log_lines.append(f"<span class='log-ok'>✔ {cand['name']} → {res['final_score']:.3f}</span>")
|
| 514 |
+
_last_results = results
|
| 515 |
+
df = pd.DataFrame([{"Sıra":0,"Aday":r["name"],"Final Skor":r["final_score"],
|
| 516 |
+
"Semantic":r["semantic_score"],"Skill Overlap":r["skill_score"],
|
| 517 |
+
"Deneyim (Yıl)":r["experience_years"],
|
| 518 |
+
"Eşleşen Skill":len(r["explanation"]["matched_skills"]),
|
| 519 |
+
"Eksik Skill":len(r["explanation"]["missing_skills"]),
|
| 520 |
+
"Değerlendirme":r["explanation"]["verdict"],
|
| 521 |
+
"_raw":r} for r in results])
|
| 522 |
+
df = df.sort_values("Final Skor",ascending=False).reset_index(drop=True)
|
| 523 |
+
df["Sıra"] = df.index+1
|
| 524 |
+
log_lines.append(f"<span class='log-ok'>✔ Tamamlandı — {len(candidates)} aday değerlendirildi.</span>")
|
| 525 |
+
log_html = "<div class='log-box'>"+"<br>".join(log_lines)+"</div>"
|
| 526 |
+
return (build_ranking_table(df),plotly_ranking_bar(df),plotly_component_stacked(df),
|
| 527 |
+
plotly_skill_gap(df),plotly_scatter(df),
|
| 528 |
+
gr.update(choices=[r["name"] for r in results],value=results[0]["name"]),log_html)
|
| 529 |
+
|
| 530 |
+
def show_xai(selected_name):
|
| 531 |
+
if not selected_name or not _last_results:
|
| 532 |
+
return "<div class='log-box'><span class='log-warn'>Önce analiz çalıştırın.</span></div>"
|
| 533 |
+
result = next((r for r in _last_results if r["name"]==selected_name),None)
|
| 534 |
+
return build_xai_panel(result) if result else "<div class='log-box'>Aday bulunamadı.</div>"
|
| 535 |
+
|
| 536 |
+
# ── CSS (inline) ────────────────────────────────────────────────────────────
|
| 537 |
+
CUSTOM_CSS = r"""
|
| 538 |
+
|
| 539 |
+
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400;500&display=swap');
|
| 540 |
+
|
| 541 |
+
:root {
|
| 542 |
+
--bg-primary: #060d1a;
|
| 543 |
+
--bg-card: #0d1829;
|
| 544 |
+
--bg-input: #111e33;
|
| 545 |
+
--bg-hover: #162440;
|
| 546 |
+
--border: #1e3050;
|
| 547 |
+
--border-glow: #00d4aa40;
|
| 548 |
+
--teal: #00d4aa;
|
| 549 |
+
--teal-dim: #00a886;
|
| 550 |
+
--amber: #f59e0b;
|
| 551 |
+
--amber-dim: #d97706;
|
| 552 |
+
--red: #ef4444;
|
| 553 |
+
--green: #22c55e;
|
| 554 |
+
--text-primary: #e2eaf6;
|
| 555 |
+
--text-muted: #6b8ab0;
|
| 556 |
+
--text-dim: #3d5878;
|
| 557 |
+
--font-display: 'Rajdhani', sans-serif;
|
| 558 |
+
--font-mono: 'JetBrains Mono', monospace;
|
| 559 |
+
--font-body: 'Inter', sans-serif;
|
| 560 |
+
--radius: 8px;
|
| 561 |
+
--shadow: 0 4px 24px rgba(0,0,0,0.6);
|
| 562 |
+
--glow: 0 0 20px rgba(0,212,170,0.15);
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
/* ── Global ── */
|
| 566 |
+
body, .gradio-container { background: var(--bg-primary) !important; color: var(--text-primary) !important; font-family: var(--font-body) !important; }
|
| 567 |
+
.gradio-container { max-width: 100% !important; width: 100% !important; padding: 0 16px !important; margin: 0 auto !important; }
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
/* ── Header Banner ── */
|
| 571 |
+
#hero-banner {
|
| 572 |
+
background: linear-gradient(135deg, #0d1829 0%, #061224 50%, #0a1f10 100%);
|
| 573 |
+
border: 1px solid var(--border);
|
| 574 |
+
border-bottom: 2px solid var(--teal);
|
| 575 |
+
border-radius: var(--radius);
|
| 576 |
+
padding: 28px 36px;
|
| 577 |
+
margin-bottom: 20px;
|
| 578 |
+
position: relative;
|
| 579 |
+
overflow: hidden;
|
| 580 |
+
box-shadow: var(--shadow), var(--glow);
|
| 581 |
+
}
|
| 582 |
+
#hero-banner::before {
|
| 583 |
+
content: '';
|
| 584 |
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
| 585 |
+
background: repeating-linear-gradient(90deg, transparent, transparent 40px, rgba(0,212,170,0.02) 40px, rgba(0,212,170,0.02) 41px),
|
| 586 |
+
repeating-linear-gradient(0deg, transparent, transparent 40px, rgba(0,212,170,0.02) 40px, rgba(0,212,170,0.02) 41px);
|
| 587 |
+
pointer-events: none;
|
| 588 |
+
}
|
| 589 |
+
#hero-banner h1 { font-family: var(--font-display) !important; font-size: 2.4rem !important; font-weight: 700 !important; color: var(--teal) !important; letter-spacing: 2px !important; margin: 0 0 6px 0 !important; text-shadow: 0 0 30px rgba(0,212,170,0.4); }
|
| 590 |
+
#hero-banner p { font-family: var(--font-body) !important; color: var(--text-muted) !important; font-size: 0.9rem !important; margin: 0 !important; letter-spacing: 0.5px; }
|
| 591 |
+
.hero-badge { display: inline-block; background: rgba(0,212,170,0.1); border: 1px solid var(--teal); color: var(--teal); font-family: var(--font-mono); font-size: 0.7rem; padding: 2px 10px; border-radius: 20px; margin-right: 8px; letter-spacing: 1px; }
|
| 592 |
+
|
| 593 |
+
/* ── Tabs ── */
|
| 594 |
+
.tab-nav { background: var(--bg-card) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 4px !important; margin-bottom: 16px !important; }
|
| 595 |
+
.tab-nav button { font-family: var(--font-display) !important; font-size: 0.95rem !important; font-weight: 600 !important; letter-spacing: 1px !important; color: var(--text-muted) !important; background: transparent !important; border: none !important; border-radius: 6px !important; padding: 10px 20px !important; transition: all 0.2s !important; }
|
| 596 |
+
.tab-nav button:hover { color: var(--teal) !important; background: rgba(0,212,170,0.08) !important; }
|
| 597 |
+
.tab-nav button.selected { color: var(--bg-primary) !important; background: var(--teal) !important; box-shadow: 0 0 15px rgba(0,212,170,0.3) !important; }
|
| 598 |
+
|
| 599 |
+
/* ── Cards / Panels ── */
|
| 600 |
+
.panel-card {
|
| 601 |
+
background: var(--bg-card);
|
| 602 |
+
border: 1px solid var(--border);
|
| 603 |
+
border-radius: var(--radius);
|
| 604 |
+
padding: 20px 24px;
|
| 605 |
+
box-shadow: var(--shadow);
|
| 606 |
+
}
|
| 607 |
+
.panel-title {
|
| 608 |
+
font-family: var(--font-display);
|
| 609 |
+
font-size: 1.1rem;
|
| 610 |
+
font-weight: 700;
|
| 611 |
+
letter-spacing: 1.5px;
|
| 612 |
+
color: var(--teal);
|
| 613 |
+
text-transform: uppercase;
|
| 614 |
+
border-bottom: 1px solid var(--border);
|
| 615 |
+
padding-bottom: 10px;
|
| 616 |
+
margin-bottom: 14px;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
/* ── Inputs ── */
|
| 620 |
+
textarea, input[type='text'] {
|
| 621 |
+
background: var(--bg-input) !important;
|
| 622 |
+
border: 1px solid var(--border) !important;
|
| 623 |
+
color: var(--text-primary) !important;
|
| 624 |
+
font-family: var(--font-body) !important;
|
| 625 |
+
border-radius: 6px !important;
|
| 626 |
+
transition: border-color 0.2s, box-shadow 0.2s !important;
|
| 627 |
+
}
|
| 628 |
+
textarea:focus, input[type='text']:focus {
|
| 629 |
+
border-color: var(--teal) !important;
|
| 630 |
+
box-shadow: 0 0 0 2px rgba(0,212,170,0.15) !important;
|
| 631 |
+
outline: none !important;
|
| 632 |
+
}
|
| 633 |
+
label, .label-wrap span { font-family: var(--font-display) !important; font-weight: 600 !important; letter-spacing: 0.8px !important; color: var(--text-muted) !important; font-size: 0.85rem !important; }
|
| 634 |
+
|
| 635 |
+
/* ── Buttons ── */
|
| 636 |
+
button.primary-btn, #analyze-btn {
|
| 637 |
+
background: linear-gradient(135deg, var(--teal) 0%, var(--teal-dim) 100%) !important;
|
| 638 |
+
color: var(--bg-primary) !important;
|
| 639 |
+
font-family: var(--font-display) !important;
|
| 640 |
+
font-size: 1rem !important;
|
| 641 |
+
font-weight: 700 !important;
|
| 642 |
+
letter-spacing: 2px !important;
|
| 643 |
+
border: none !important;
|
| 644 |
+
border-radius: var(--radius) !important;
|
| 645 |
+
padding: 14px 32px !important;
|
| 646 |
+
cursor: pointer !important;
|
| 647 |
+
transition: all 0.2s !important;
|
| 648 |
+
box-shadow: 0 4px 20px rgba(0,212,170,0.3) !important;
|
| 649 |
+
width: 100% !important;
|
| 650 |
+
}
|
| 651 |
+
button.primary-btn:hover, #analyze-btn:hover {
|
| 652 |
+
transform: translateY(-2px) !important;
|
| 653 |
+
box-shadow: 0 8px 30px rgba(0,212,170,0.45) !important;
|
| 654 |
+
}
|
| 655 |
+
.secondary-btn {
|
| 656 |
+
background: transparent !important;
|
| 657 |
+
color: var(--teal) !important;
|
| 658 |
+
border: 1px solid var(--teal) !important;
|
| 659 |
+
font-family: var(--font-display) !important;
|
| 660 |
+
font-weight: 600 !important;
|
| 661 |
+
letter-spacing: 1px !important;
|
| 662 |
+
border-radius: 6px !important;
|
| 663 |
+
padding: 8px 20px !important;
|
| 664 |
+
transition: all 0.2s !important;
|
| 665 |
+
}
|
| 666 |
+
.secondary-btn:hover { background: rgba(0,212,170,0.1) !important; }
|
| 667 |
+
|
| 668 |
+
/* ── File Upload ── */
|
| 669 |
+
.upload-zone {
|
| 670 |
+
background: var(--bg-input) !important;
|
| 671 |
+
border: 2px dashed var(--border) !important;
|
| 672 |
+
border-radius: var(--radius) !important;
|
| 673 |
+
transition: border-color 0.2s, background 0.2s !important;
|
| 674 |
+
}
|
| 675 |
+
.upload-zone:hover { border-color: var(--teal) !important; background: rgba(0,212,170,0.05) !important; }
|
| 676 |
+
.upload-zone .icon { color: var(--teal) !important; }
|
| 677 |
+
|
| 678 |
+
/* ── Slider ── */
|
| 679 |
+
input[type='range'] { accent-color: var(--teal) !important; }
|
| 680 |
+
|
| 681 |
+
/* ── Score Cards (HTML içinde) ── */
|
| 682 |
+
.score-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 12px 0; }
|
| 683 |
+
.score-card {
|
| 684 |
+
background: var(--bg-input);
|
| 685 |
+
border: 1px solid var(--border);
|
| 686 |
+
border-radius: 8px;
|
| 687 |
+
padding: 14px 16px;
|
| 688 |
+
text-align: center;
|
| 689 |
+
position: relative;
|
| 690 |
+
overflow: hidden;
|
| 691 |
+
}
|
| 692 |
+
.score-card::after {
|
| 693 |
+
content: '';
|
| 694 |
+
position: absolute; bottom: 0; left: 0; right: 0; height: 3px;
|
| 695 |
+
background: linear-gradient(90deg, var(--teal), var(--amber));
|
| 696 |
+
}
|
| 697 |
+
.score-card .val { font-family: var(--font-mono); font-size: 1.8rem; font-weight: 500; color: var(--teal); }
|
| 698 |
+
.score-card .lbl { font-family: var(--font-display); font-size: 0.7rem; letter-spacing: 1px; color: var(--text-muted); text-transform: uppercase; margin-top: 4px; }
|
| 699 |
+
|
| 700 |
+
.verdict-strong { background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.4); color: #22c55e; }
|
| 701 |
+
.verdict-mid { background: rgba(245,158,11,0.12); border: 1px solid rgba(245,158,11,0.4); color: #f59e0b; }
|
| 702 |
+
.verdict-weak { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.4); color: #ef4444; }
|
| 703 |
+
|
| 704 |
+
/* ── Skill Tags ── */
|
| 705 |
+
.skill-tag {
|
| 706 |
+
display: inline-block;
|
| 707 |
+
font-family: var(--font-mono);
|
| 708 |
+
font-size: 0.72rem;
|
| 709 |
+
padding: 3px 10px;
|
| 710 |
+
border-radius: 20px;
|
| 711 |
+
margin: 3px;
|
| 712 |
+
}
|
| 713 |
+
.skill-match { background: rgba(34,197,94,0.15); border: 1px solid rgba(34,197,94,0.5); color: #22c55e; }
|
| 714 |
+
.skill-missing { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.4); color: #ef4444; }
|
| 715 |
+
.skill-extra { background: rgba(0,212,170,0.1); border: 1px solid rgba(0,212,170,0.35); color: #00d4aa; }
|
| 716 |
+
|
| 717 |
+
/* ── Progress Bar ── */
|
| 718 |
+
.prog-bar-wrap { margin: 8px 0; }
|
| 719 |
+
.prog-bar-label { font-family: var(--font-display); font-size: 0.78rem; letter-spacing: 0.5px; color: var(--text-muted); display: flex; justify-content: space-between; margin-bottom: 4px; }
|
| 720 |
+
.prog-bar-track { background: var(--bg-input); border-radius: 4px; height: 8px; overflow: hidden; }
|
| 721 |
+
.prog-bar-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
|
| 722 |
+
|
| 723 |
+
/* ── Ranking Table ── */
|
| 724 |
+
.rank-table { width: 100%; border-collapse: collapse; font-family: var(--font-body); font-size: 0.88rem; }
|
| 725 |
+
.rank-table thead tr { background: rgba(0,212,170,0.08); border-bottom: 2px solid var(--teal); }
|
| 726 |
+
.rank-table th { font-family: var(--font-display); font-weight: 700; letter-spacing: 1px; color: var(--teal); padding: 12px 14px; text-align: left; font-size: 0.8rem; text-transform: uppercase; }
|
| 727 |
+
.rank-table td { padding: 12px 14px; border-bottom: 1px solid var(--border); color: var(--text-primary); vertical-align: middle; }
|
| 728 |
+
.rank-table tr:hover td { background: var(--bg-hover); }
|
| 729 |
+
.rank-num { font-family: var(--font-mono); font-size: 1.1rem; color: var(--teal); font-weight: 500; }
|
| 730 |
+
.score-pill { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 500; padding: 4px 12px; border-radius: 20px; }
|
| 731 |
+
.score-high { background: rgba(34,197,94,0.15); color: #22c55e; }
|
| 732 |
+
.score-mid { background: rgba(245,158,11,0.15); color: #f59e0b; }
|
| 733 |
+
.score-low { background: rgba(239,68,68,0.12); color: #ef4444; }
|
| 734 |
+
|
| 735 |
+
/* ── Status / Log ── */
|
| 736 |
+
.log-box { background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 14px 18px; font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); line-height: 1.8; max-height: 120px; overflow-y: auto; }
|
| 737 |
+
.log-ok { color: var(--green); }
|
| 738 |
+
.log-warn { color: var(--amber); }
|
| 739 |
+
.log-info { color: var(--teal); }
|
| 740 |
+
|
| 741 |
+
/* ── Dividers ── */
|
| 742 |
+
.section-divider { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
|
| 743 |
+
|
| 744 |
+
/* ── Scrollbar ── */
|
| 745 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 746 |
+
::-webkit-scrollbar-track { background: var(--bg-card); }
|
| 747 |
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
| 748 |
+
::-webkit-scrollbar-thumb:hover { background: var(--teal-dim); }
|
| 749 |
+
|
| 750 |
+
"""
|
| 751 |
+
|
| 752 |
+
HERO_HTML = """<div id='hero-banner'><h1>⚡ CV MATCHING ENGINE</h1>
|
| 753 |
+
<p>PDF CV'leri yükle · İş ilanını gir · Semantik analiz & XAI ile sırala</p>
|
| 754 |
+
<div style='margin-top:14px'><span class='hero-badge'>SEMANTIC NLP</span>
|
| 755 |
+
<span class='hero-badge'>SKILL ONTOLOGY</span><span class='hero-badge'>HYBRID SCORING</span>
|
| 756 |
+
<span class='hero-badge'>EXPLAINABLE AI</span></div></div>"""
|
| 757 |
+
|
| 758 |
+
with gr.Blocks(css=CUSTOM_CSS, title="CV Matching Engine") as demo:
|
| 759 |
+
gr.HTML(HERO_HTML)
|
| 760 |
+
with gr.Row():
|
| 761 |
+
with gr.Column(scale=1, min_width=310):
|
| 762 |
+
gr.HTML("<div class='panel-title'>📂 CV YÜKLEME</div>")
|
| 763 |
+
pdf_input = gr.File(label="PDF CV Dosyaları",file_types=[".pdf"],file_count="multiple")
|
| 764 |
+
gr.HTML("<div style='margin-top:20px' class='panel-title'>📋 İŞ İLANI</div>")
|
| 765 |
+
job_input = gr.Textbox(label="İş Tanımı",placeholder="Aranan pozisyon ve beceriler...",lines=7,
|
| 766 |
+
value="""We are looking for a Senior C# Developer with ASP.NET Core, Azure, SQL Server.
|
| 767 |
+
Nice to have: Docker, Kubernetes, React.""")
|
| 768 |
+
gr.HTML("<div style='margin-top:20px' class='panel-title'>⚙️ SKOR AĞIRLIKLARI</div>")
|
| 769 |
+
sem_w = gr.Slider(0.0,1.0,value=0.55,step=0.05,label="Semantik Benzerlik")
|
| 770 |
+
skill_w = gr.Slider(0.0,1.0,value=0.35,step=0.05,label="Skill Örtüşmesi")
|
| 771 |
+
exp_w = gr.Slider(0.0,1.0,value=0.10,step=0.05,label="Deneyim Bonusu")
|
| 772 |
+
gr.HTML("<div style='margin-top:16px' class='panel-title'>🔧 SEÇENEKLER</div>")
|
| 773 |
+
req_years = gr.Slider(0,15,value=3,step=0.5,label="Gerekli Deneyim (Yıl)")
|
| 774 |
+
dual_col = gr.Checkbox(label="İki sütunlu CV formatı",value=False)
|
| 775 |
+
analyze_btn = gr.Button("▶ ANALİZİ BAŞLAT",elem_id="analyze-btn",variant="primary")
|
| 776 |
+
gr.HTML("<div style='margin-top:16px' class='panel-title'>📟 DURUM</div>")
|
| 777 |
+
status_out = gr.HTML("<div class='log-box'><span style='color:var(--text-dim)'>Hazır.</span></div>")
|
| 778 |
+
with gr.Column(scale=3):
|
| 779 |
+
with gr.Tabs():
|
| 780 |
+
with gr.Tab("🏆 Sıralama"):
|
| 781 |
+
gr.HTML("<div style='margin-bottom:12px' class='panel-title'>ADAY SIRALAMA TABLOSU</div>")
|
| 782 |
+
table_out = gr.HTML(value="<div class='log-box' style='text-align:center;padding:40px'><span style='color:var(--text-dim);font-family:var(--font-display);letter-spacing:2px'>PDF CV yükleyip analizi başlatın</span></div>")
|
| 783 |
+
gr.HTML("<div style='margin-top:24px' class='panel-title'>📊 SKOR GRAFİĞİ</div>")
|
| 784 |
+
bar_plot = gr.Plot(label="")
|
| 785 |
+
with gr.Tab("📊 Dashboard"):
|
| 786 |
+
with gr.Row():
|
| 787 |
+
stack_plot = gr.Plot(label="")
|
| 788 |
+
scatter_plot = gr.Plot(label="")
|
| 789 |
+
with gr.Row():
|
| 790 |
+
gap_plot = gr.Plot(label="")
|
| 791 |
+
with gr.Tab("🔍 XAI Detay"):
|
| 792 |
+
gr.HTML("<div style='margin-bottom:14px' class='panel-title'>AÇIKLANAB İ LİR AI — ADAY DETAY ANALİZİ</div>")
|
| 793 |
+
xai_selector = gr.Dropdown(choices=[],label="Aday Seçin",interactive=True)
|
| 794 |
+
xai_out = gr.HTML(value="<div class='log-box' style='text-align:center;padding:40px'><span style='color:var(--text-dim)'>Analiz tamamlandıktan sonra aday seçin</span></div>")
|
| 795 |
+
analyze_btn.click(fn=run_analysis,
|
| 796 |
+
inputs=[pdf_input,job_input,sem_w,skill_w,exp_w,req_years,dual_col],
|
| 797 |
+
outputs=[table_out,bar_plot,stack_plot,gap_plot,scatter_plot,xai_selector,status_out])
|
| 798 |
+
xai_selector.change(fn=show_xai,inputs=[xai_selector],outputs=[xai_out])
|
| 799 |
+
|
| 800 |
+
if __name__ == "__main__":
|
| 801 |
+
demo.launch()
|
gradio_app_css.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Auto-generated CSS module
|
| 2 |
+
CUSTOM_CSS = r'''
|
| 3 |
+
|
| 4 |
+
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@300;400;500&display=swap');
|
| 5 |
+
|
| 6 |
+
:root {
|
| 7 |
+
--bg-primary: #060d1a;
|
| 8 |
+
--bg-card: #0d1829;
|
| 9 |
+
--bg-input: #111e33;
|
| 10 |
+
--bg-hover: #162440;
|
| 11 |
+
--border: #1e3050;
|
| 12 |
+
--border-glow: #00d4aa40;
|
| 13 |
+
--teal: #00d4aa;
|
| 14 |
+
--teal-dim: #00a886;
|
| 15 |
+
--amber: #f59e0b;
|
| 16 |
+
--amber-dim: #d97706;
|
| 17 |
+
--red: #ef4444;
|
| 18 |
+
--green: #22c55e;
|
| 19 |
+
--text-primary: #e2eaf6;
|
| 20 |
+
--text-muted: #6b8ab0;
|
| 21 |
+
--text-dim: #3d5878;
|
| 22 |
+
--font-display: 'Rajdhani', sans-serif;
|
| 23 |
+
--font-mono: 'JetBrains Mono', monospace;
|
| 24 |
+
--font-body: 'Inter', sans-serif;
|
| 25 |
+
--radius: 8px;
|
| 26 |
+
--shadow: 0 4px 24px rgba(0,0,0,0.6);
|
| 27 |
+
--glow: 0 0 20px rgba(0,212,170,0.15);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* ── Global ── */
|
| 31 |
+
body, .gradio-container { background: var(--bg-primary) !important; color: var(--text-primary) !important; font-family: var(--font-body) !important; }
|
| 32 |
+
.gradio-container { max-width: 100% !important; width: 100% !important; padding: 0 16px !important; margin: 0 auto !important; }
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
/* ── Header Banner ── */
|
| 36 |
+
#hero-banner {
|
| 37 |
+
background: linear-gradient(135deg, #0d1829 0%, #061224 50%, #0a1f10 100%);
|
| 38 |
+
border: 1px solid var(--border);
|
| 39 |
+
border-bottom: 2px solid var(--teal);
|
| 40 |
+
border-radius: var(--radius);
|
| 41 |
+
padding: 28px 36px;
|
| 42 |
+
margin-bottom: 20px;
|
| 43 |
+
position: relative;
|
| 44 |
+
overflow: hidden;
|
| 45 |
+
box-shadow: var(--shadow), var(--glow);
|
| 46 |
+
}
|
| 47 |
+
#hero-banner::before {
|
| 48 |
+
content: '';
|
| 49 |
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
| 50 |
+
background: repeating-linear-gradient(90deg, transparent, transparent 40px, rgba(0,212,170,0.02) 40px, rgba(0,212,170,0.02) 41px),
|
| 51 |
+
repeating-linear-gradient(0deg, transparent, transparent 40px, rgba(0,212,170,0.02) 40px, rgba(0,212,170,0.02) 41px);
|
| 52 |
+
pointer-events: none;
|
| 53 |
+
}
|
| 54 |
+
#hero-banner h1 { font-family: var(--font-display) !important; font-size: 2.4rem !important; font-weight: 700 !important; color: var(--teal) !important; letter-spacing: 2px !important; margin: 0 0 6px 0 !important; text-shadow: 0 0 30px rgba(0,212,170,0.4); }
|
| 55 |
+
#hero-banner p { font-family: var(--font-body) !important; color: var(--text-muted) !important; font-size: 0.9rem !important; margin: 0 !important; letter-spacing: 0.5px; }
|
| 56 |
+
.hero-badge { display: inline-block; background: rgba(0,212,170,0.1); border: 1px solid var(--teal); color: var(--teal); font-family: var(--font-mono); font-size: 0.7rem; padding: 2px 10px; border-radius: 20px; margin-right: 8px; letter-spacing: 1px; }
|
| 57 |
+
|
| 58 |
+
/* ── Tabs ── */
|
| 59 |
+
.tab-nav { background: var(--bg-card) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; padding: 4px !important; margin-bottom: 16px !important; }
|
| 60 |
+
.tab-nav button { font-family: var(--font-display) !important; font-size: 0.95rem !important; font-weight: 600 !important; letter-spacing: 1px !important; color: var(--text-muted) !important; background: transparent !important; border: none !important; border-radius: 6px !important; padding: 10px 20px !important; transition: all 0.2s !important; }
|
| 61 |
+
.tab-nav button:hover { color: var(--teal) !important; background: rgba(0,212,170,0.08) !important; }
|
| 62 |
+
.tab-nav button.selected { color: var(--bg-primary) !important; background: var(--teal) !important; box-shadow: 0 0 15px rgba(0,212,170,0.3) !important; }
|
| 63 |
+
|
| 64 |
+
/* ── Cards / Panels ── */
|
| 65 |
+
.panel-card {
|
| 66 |
+
background: var(--bg-card);
|
| 67 |
+
border: 1px solid var(--border);
|
| 68 |
+
border-radius: var(--radius);
|
| 69 |
+
padding: 20px 24px;
|
| 70 |
+
box-shadow: var(--shadow);
|
| 71 |
+
}
|
| 72 |
+
.panel-title {
|
| 73 |
+
font-family: var(--font-display);
|
| 74 |
+
font-size: 1.1rem;
|
| 75 |
+
font-weight: 700;
|
| 76 |
+
letter-spacing: 1.5px;
|
| 77 |
+
color: var(--teal);
|
| 78 |
+
text-transform: uppercase;
|
| 79 |
+
border-bottom: 1px solid var(--border);
|
| 80 |
+
padding-bottom: 10px;
|
| 81 |
+
margin-bottom: 14px;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* ── Inputs ── */
|
| 85 |
+
textarea, input[type='text'] {
|
| 86 |
+
background: var(--bg-input) !important;
|
| 87 |
+
border: 1px solid var(--border) !important;
|
| 88 |
+
color: var(--text-primary) !important;
|
| 89 |
+
font-family: var(--font-body) !important;
|
| 90 |
+
border-radius: 6px !important;
|
| 91 |
+
transition: border-color 0.2s, box-shadow 0.2s !important;
|
| 92 |
+
}
|
| 93 |
+
textarea:focus, input[type='text']:focus {
|
| 94 |
+
border-color: var(--teal) !important;
|
| 95 |
+
box-shadow: 0 0 0 2px rgba(0,212,170,0.15) !important;
|
| 96 |
+
outline: none !important;
|
| 97 |
+
}
|
| 98 |
+
label, .label-wrap span { font-family: var(--font-display) !important; font-weight: 600 !important; letter-spacing: 0.8px !important; color: var(--text-muted) !important; font-size: 0.85rem !important; }
|
| 99 |
+
|
| 100 |
+
/* ── Buttons ── */
|
| 101 |
+
button.primary-btn, #analyze-btn {
|
| 102 |
+
background: linear-gradient(135deg, var(--teal) 0%, var(--teal-dim) 100%) !important;
|
| 103 |
+
color: var(--bg-primary) !important;
|
| 104 |
+
font-family: var(--font-display) !important;
|
| 105 |
+
font-size: 1rem !important;
|
| 106 |
+
font-weight: 700 !important;
|
| 107 |
+
letter-spacing: 2px !important;
|
| 108 |
+
border: none !important;
|
| 109 |
+
border-radius: var(--radius) !important;
|
| 110 |
+
padding: 14px 32px !important;
|
| 111 |
+
cursor: pointer !important;
|
| 112 |
+
transition: all 0.2s !important;
|
| 113 |
+
box-shadow: 0 4px 20px rgba(0,212,170,0.3) !important;
|
| 114 |
+
width: 100% !important;
|
| 115 |
+
}
|
| 116 |
+
button.primary-btn:hover, #analyze-btn:hover {
|
| 117 |
+
transform: translateY(-2px) !important;
|
| 118 |
+
box-shadow: 0 8px 30px rgba(0,212,170,0.45) !important;
|
| 119 |
+
}
|
| 120 |
+
.secondary-btn {
|
| 121 |
+
background: transparent !important;
|
| 122 |
+
color: var(--teal) !important;
|
| 123 |
+
border: 1px solid var(--teal) !important;
|
| 124 |
+
font-family: var(--font-display) !important;
|
| 125 |
+
font-weight: 600 !important;
|
| 126 |
+
letter-spacing: 1px !important;
|
| 127 |
+
border-radius: 6px !important;
|
| 128 |
+
padding: 8px 20px !important;
|
| 129 |
+
transition: all 0.2s !important;
|
| 130 |
+
}
|
| 131 |
+
.secondary-btn:hover { background: rgba(0,212,170,0.1) !important; }
|
| 132 |
+
|
| 133 |
+
/* ── File Upload ── */
|
| 134 |
+
.upload-zone {
|
| 135 |
+
background: var(--bg-input) !important;
|
| 136 |
+
border: 2px dashed var(--border) !important;
|
| 137 |
+
border-radius: var(--radius) !important;
|
| 138 |
+
transition: border-color 0.2s, background 0.2s !important;
|
| 139 |
+
}
|
| 140 |
+
.upload-zone:hover { border-color: var(--teal) !important; background: rgba(0,212,170,0.05) !important; }
|
| 141 |
+
.upload-zone .icon { color: var(--teal) !important; }
|
| 142 |
+
|
| 143 |
+
/* ── Slider ── */
|
| 144 |
+
input[type='range'] { accent-color: var(--teal) !important; }
|
| 145 |
+
|
| 146 |
+
/* ── Score Cards (HTML içinde) ── */
|
| 147 |
+
.score-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 12px 0; }
|
| 148 |
+
.score-card {
|
| 149 |
+
background: var(--bg-input);
|
| 150 |
+
border: 1px solid var(--border);
|
| 151 |
+
border-radius: 8px;
|
| 152 |
+
padding: 14px 16px;
|
| 153 |
+
text-align: center;
|
| 154 |
+
position: relative;
|
| 155 |
+
overflow: hidden;
|
| 156 |
+
}
|
| 157 |
+
.score-card::after {
|
| 158 |
+
content: '';
|
| 159 |
+
position: absolute; bottom: 0; left: 0; right: 0; height: 3px;
|
| 160 |
+
background: linear-gradient(90deg, var(--teal), var(--amber));
|
| 161 |
+
}
|
| 162 |
+
.score-card .val { font-family: var(--font-mono); font-size: 1.8rem; font-weight: 500; color: var(--teal); }
|
| 163 |
+
.score-card .lbl { font-family: var(--font-display); font-size: 0.7rem; letter-spacing: 1px; color: var(--text-muted); text-transform: uppercase; margin-top: 4px; }
|
| 164 |
+
|
| 165 |
+
.verdict-strong { background: rgba(34,197,94,0.12); border: 1px solid rgba(34,197,94,0.4); color: #22c55e; }
|
| 166 |
+
.verdict-mid { background: rgba(245,158,11,0.12); border: 1px solid rgba(245,158,11,0.4); color: #f59e0b; }
|
| 167 |
+
.verdict-weak { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.4); color: #ef4444; }
|
| 168 |
+
|
| 169 |
+
/* ── Skill Tags ── */
|
| 170 |
+
.skill-tag {
|
| 171 |
+
display: inline-block;
|
| 172 |
+
font-family: var(--font-mono);
|
| 173 |
+
font-size: 0.72rem;
|
| 174 |
+
padding: 3px 10px;
|
| 175 |
+
border-radius: 20px;
|
| 176 |
+
margin: 3px;
|
| 177 |
+
}
|
| 178 |
+
.skill-match { background: rgba(34,197,94,0.15); border: 1px solid rgba(34,197,94,0.5); color: #22c55e; }
|
| 179 |
+
.skill-missing { background: rgba(239,68,68,0.12); border: 1px solid rgba(239,68,68,0.4); color: #ef4444; }
|
| 180 |
+
.skill-extra { background: rgba(0,212,170,0.1); border: 1px solid rgba(0,212,170,0.35); color: #00d4aa; }
|
| 181 |
+
|
| 182 |
+
/* ── Progress Bar ── */
|
| 183 |
+
.prog-bar-wrap { margin: 8px 0; }
|
| 184 |
+
.prog-bar-label { font-family: var(--font-display); font-size: 0.78rem; letter-spacing: 0.5px; color: var(--text-muted); display: flex; justify-content: space-between; margin-bottom: 4px; }
|
| 185 |
+
.prog-bar-track { background: var(--bg-input); border-radius: 4px; height: 8px; overflow: hidden; }
|
| 186 |
+
.prog-bar-fill { height: 100%; border-radius: 4px; transition: width 0.6s ease; }
|
| 187 |
+
|
| 188 |
+
/* ── Ranking Table ── */
|
| 189 |
+
.rank-table { width: 100%; border-collapse: collapse; font-family: var(--font-body); font-size: 0.88rem; }
|
| 190 |
+
.rank-table thead tr { background: rgba(0,212,170,0.08); border-bottom: 2px solid var(--teal); }
|
| 191 |
+
.rank-table th { font-family: var(--font-display); font-weight: 700; letter-spacing: 1px; color: var(--teal); padding: 12px 14px; text-align: left; font-size: 0.8rem; text-transform: uppercase; }
|
| 192 |
+
.rank-table td { padding: 12px 14px; border-bottom: 1px solid var(--border); color: var(--text-primary); vertical-align: middle; }
|
| 193 |
+
.rank-table tr:hover td { background: var(--bg-hover); }
|
| 194 |
+
.rank-num { font-family: var(--font-mono); font-size: 1.1rem; color: var(--teal); font-weight: 500; }
|
| 195 |
+
.score-pill { font-family: var(--font-mono); font-size: 0.85rem; font-weight: 500; padding: 4px 12px; border-radius: 20px; }
|
| 196 |
+
.score-high { background: rgba(34,197,94,0.15); color: #22c55e; }
|
| 197 |
+
.score-mid { background: rgba(245,158,11,0.15); color: #f59e0b; }
|
| 198 |
+
.score-low { background: rgba(239,68,68,0.12); color: #ef4444; }
|
| 199 |
+
|
| 200 |
+
/* ── Status / Log ── */
|
| 201 |
+
.log-box { background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; padding: 14px 18px; font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted); line-height: 1.8; max-height: 120px; overflow-y: auto; }
|
| 202 |
+
.log-ok { color: var(--green); }
|
| 203 |
+
.log-warn { color: var(--amber); }
|
| 204 |
+
.log-info { color: var(--teal); }
|
| 205 |
+
|
| 206 |
+
/* ── Dividers ── */
|
| 207 |
+
.section-divider { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
|
| 208 |
+
|
| 209 |
+
/* ── Scrollbar ── */
|
| 210 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 211 |
+
::-webkit-scrollbar-track { background: var(--bg-card); }
|
| 212 |
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
| 213 |
+
::-webkit-scrollbar-thumb:hover { background: var(--teal-dim); }
|
| 214 |
+
|
| 215 |
+
''
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
+
|
| 3 |
+
torch==2.2.2+cpu
|
| 4 |
+
|
| 5 |
+
pymupdf==1.24.10
|
| 6 |
+
spacy==3.7.5
|
| 7 |
+
https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl
|
| 8 |
+
|
| 9 |
+
sentence-transformers==2.7.0
|
| 10 |
+
scikit-learn==1.5.2
|
| 11 |
+
pandas==2.2.3
|
| 12 |
+
numpy==1.26.4
|
| 13 |
+
plotly==5.24.1
|