cihatyldz commited on
Commit
4fb5592
·
verified ·
1 Parent(s): 26239bb

⚡ Build optimization: Python 3.11 + CPU torch + lazy load

Browse files
Files changed (4) hide show
  1. README.md +28 -7
  2. app.py +801 -0
  3. gradio_app_css.py +215 -0
  4. requirements.txt +13 -0
README.md CHANGED
@@ -1,12 +1,33 @@
1
  ---
2
- title: Cv Matching Engine
3
- emoji: 🦀
4
- colorFrom: yellow
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.12.0
 
8
  app_file: app.py
9
- pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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