Spaces:
Sleeping
Sleeping
Commit Β·
92feaec
1
Parent(s): f6d8deb
Make the pipelines functional with HF Hub + Local Files
Browse files- environment.yml +29 -336
- pipelines/app_pipeline.py +106 -57
- pipelines/recruiter_pipeline.py +109 -81
- src/feature_engg/bert_embedding_data.py +36 -17
- src/feature_engg/tfidf_vectorizing_data.py +19 -14
- src/matching/matching_engine.py +3 -3
- src/processing/text_cleaning.py +21 -1
environment.yml
CHANGED
|
@@ -3,343 +3,36 @@
|
|
| 3 |
|
| 4 |
name: resume-matcher
|
| 5 |
channels:
|
| 6 |
-
- pytorch
|
| 7 |
-
- nvidia
|
| 8 |
- conda-forge
|
| 9 |
- defaults
|
| 10 |
dependencies:
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
-
|
| 14 |
-
-
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
-
|
| 20 |
-
-
|
| 21 |
-
-
|
| 22 |
-
-
|
| 23 |
-
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
-
-
|
| 27 |
-
-
|
| 28 |
-
|
| 29 |
-
-
|
| 30 |
-
-
|
| 31 |
-
-
|
| 32 |
-
-
|
| 33 |
-
-
|
| 34 |
-
-
|
| 35 |
-
|
| 36 |
-
- aws-sdk-cpp=1.11.606
|
| 37 |
-
- babel=2.17.0
|
| 38 |
-
- beautifulsoup4=4.13.4
|
| 39 |
-
- blas=1.0
|
| 40 |
-
- bleach=6.2.0
|
| 41 |
-
- bleach-with-css=6.2.0
|
| 42 |
-
- bottleneck=1.4.2
|
| 43 |
-
- brotli=1.1.0
|
| 44 |
-
- brotli-bin=1.1.0
|
| 45 |
-
- brotli-python=1.1.0
|
| 46 |
-
- bzip2=1.0.8
|
| 47 |
-
- c-ares=1.34.5
|
| 48 |
-
- ca-certificates=2025.8.3
|
| 49 |
-
- cached-property=1.5.2
|
| 50 |
-
- cached_property=1.5.2
|
| 51 |
-
- cairo=1.18.4
|
| 52 |
-
- certifi=2025.8.3
|
| 53 |
-
- cffi=1.17.1
|
| 54 |
-
- click=8.1.8
|
| 55 |
-
- colorama=0.4.6
|
| 56 |
-
- comm=0.2.3
|
| 57 |
-
- contourpy=1.3.2
|
| 58 |
-
- cpython=3.10.18
|
| 59 |
-
- cryptography=45.0.6
|
| 60 |
-
- cuda-cccl=13.0.50
|
| 61 |
-
- cuda-cccl_win-64=13.0.50
|
| 62 |
-
- cuda-cudart=12.1.105
|
| 63 |
-
- cuda-cudart-dev=12.1.105
|
| 64 |
-
- cuda-cupti=12.1.105
|
| 65 |
-
- cuda-libraries=12.1.0
|
| 66 |
-
- cuda-libraries-dev=12.1.0
|
| 67 |
-
- cuda-nvrtc=12.1.105
|
| 68 |
-
- cuda-nvrtc-dev=12.1.105
|
| 69 |
-
- cuda-nvtx=12.1.105
|
| 70 |
-
- cuda-opencl=13.0.39
|
| 71 |
-
- cuda-opencl-dev=13.0.39
|
| 72 |
-
- cuda-profiler-api=13.0.39
|
| 73 |
-
- cuda-runtime=12.1.0
|
| 74 |
-
- cuda-version=13.0
|
| 75 |
-
- cycler=0.12.1
|
| 76 |
-
- datasets=2.14.4
|
| 77 |
-
- debugpy=1.8.15
|
| 78 |
-
- decorator=5.2.1
|
| 79 |
-
- defusedxml=0.7.1
|
| 80 |
-
- dill=0.3.7
|
| 81 |
-
- double-conversion=3.3.1
|
| 82 |
-
- exceptiongroup=1.3.0
|
| 83 |
-
- executing=2.2.0
|
| 84 |
-
- font-ttf-dejavu-sans-mono=2.37
|
| 85 |
-
- font-ttf-inconsolata=3.000
|
| 86 |
-
- font-ttf-source-code-pro=2.038
|
| 87 |
-
- font-ttf-ubuntu=0.83
|
| 88 |
-
- fontconfig=2.15.0
|
| 89 |
-
- fonts-conda-ecosystem=1
|
| 90 |
-
- fonts-conda-forge=1
|
| 91 |
-
- fonttools=4.59.0
|
| 92 |
-
- fqdn=1.5.1
|
| 93 |
-
- freetype=2.13.3
|
| 94 |
-
- frozenlist=1.7.0
|
| 95 |
-
- fsspec=2025.7.0
|
| 96 |
-
- gmp=6.3.0
|
| 97 |
-
- gmpy2=2.2.1
|
| 98 |
-
- graphite2=1.3.14
|
| 99 |
-
- h11=0.16.0
|
| 100 |
-
- h2=4.2.0
|
| 101 |
-
- harfbuzz=11.3.2
|
| 102 |
-
- hf-xet=1.1.7
|
| 103 |
-
- hpack=4.1.0
|
| 104 |
-
- httpcore=1.0.9
|
| 105 |
-
- httpx=0.28.1
|
| 106 |
-
- huggingface_hub=0.34.4
|
| 107 |
-
- hyperframe=6.1.0
|
| 108 |
-
- icc_rt=2022.1.0
|
| 109 |
-
- icu=75.1
|
| 110 |
-
- idna=3.10
|
| 111 |
-
- importlib-metadata=8.7.0
|
| 112 |
-
- intel-openmp=2023.1.0
|
| 113 |
-
- ipykernel=6.29.5
|
| 114 |
-
- ipython=8.37.0
|
| 115 |
-
- ipywidgets=8.1.7
|
| 116 |
-
- isoduration=20.11.0
|
| 117 |
-
- jedi=0.19.2
|
| 118 |
-
- jinja2=3.1.6
|
| 119 |
-
- joblib=1.5.1
|
| 120 |
-
- json5=0.12.0
|
| 121 |
-
- jsonpointer=3.0.0
|
| 122 |
-
- jsonschema=4.25.0
|
| 123 |
-
- jsonschema-specifications=2025.4.1
|
| 124 |
-
- jsonschema-with-format-nongpl=4.25.0
|
| 125 |
-
- jupyter=1.1.1
|
| 126 |
-
- jupyter-lsp=2.2.6
|
| 127 |
-
- jupyter_client=8.6.3
|
| 128 |
-
- jupyter_console=6.6.3
|
| 129 |
-
- jupyter_core=5.8.1
|
| 130 |
-
- jupyter_events=0.12.0
|
| 131 |
-
- jupyter_server=2.16.0
|
| 132 |
-
- jupyter_server_terminals=0.5.3
|
| 133 |
-
- jupyterlab=4.4.5
|
| 134 |
-
- jupyterlab_pygments=0.3.0
|
| 135 |
-
- jupyterlab_server=2.27.3
|
| 136 |
-
- jupyterlab_widgets=3.0.15
|
| 137 |
-
- khronos-opencl-icd-loader=2024.05.08
|
| 138 |
-
- kiwisolver=1.4.8
|
| 139 |
-
- krb5=1.21.3
|
| 140 |
-
- lark=1.2.2
|
| 141 |
-
- lcms2=2.17
|
| 142 |
-
- lerc=4.0.0
|
| 143 |
-
- libabseil=20250512.1
|
| 144 |
-
- libarrow=21.0.0
|
| 145 |
-
- libarrow-acero=21.0.0
|
| 146 |
-
- libarrow-compute=21.0.0
|
| 147 |
-
- libarrow-dataset=21.0.0
|
| 148 |
-
- libarrow-substrait=21.0.0
|
| 149 |
-
- libbrotlicommon=1.1.0
|
| 150 |
-
- libbrotlidec=1.1.0
|
| 151 |
-
- libbrotlienc=1.1.0
|
| 152 |
-
- libclang13=20.1.8
|
| 153 |
-
- libcrc32c=1.1.2
|
| 154 |
-
- libcublas=12.1.0.26
|
| 155 |
-
- libcublas-dev=12.1.0.26
|
| 156 |
-
- libcufft=11.0.2.4
|
| 157 |
-
- libcufft-dev=11.0.2.4
|
| 158 |
-
- libcurand=10.4.0.35
|
| 159 |
-
- libcurand-dev=10.4.0.35
|
| 160 |
-
- libcurl=8.14.1
|
| 161 |
-
- libcusolver=11.4.4.55
|
| 162 |
-
- libcusolver-dev=11.4.4.55
|
| 163 |
-
- libcusparse=12.0.2.55
|
| 164 |
-
- libcusparse-dev=12.0.2.55
|
| 165 |
-
- libdeflate=1.24
|
| 166 |
-
- libevent=2.1.12
|
| 167 |
-
- libexpat=2.7.1
|
| 168 |
-
- libffi=3.4.6
|
| 169 |
-
- libfreetype=2.13.3
|
| 170 |
-
- libfreetype6=2.13.3
|
| 171 |
-
- libgcc=15.1.0
|
| 172 |
-
- libglib=2.84.2
|
| 173 |
-
- libgomp=15.1.0
|
| 174 |
-
- libgoogle-cloud=2.39.0
|
| 175 |
-
- libgoogle-cloud-storage=2.39.0
|
| 176 |
-
- libgrpc=1.73.1
|
| 177 |
-
- libhwloc=2.11.2
|
| 178 |
-
- libiconv=1.18
|
| 179 |
-
- libintl=0.22.5
|
| 180 |
-
- libjpeg-turbo=3.1.0
|
| 181 |
-
- liblzma=5.8.1
|
| 182 |
-
- libnpp=12.0.2.50
|
| 183 |
-
- libnpp-dev=12.0.2.50
|
| 184 |
-
- libnvjitlink=12.1.105
|
| 185 |
-
- libnvjitlink-dev=12.1.105
|
| 186 |
-
- libnvjpeg=12.1.1.14
|
| 187 |
-
- libnvjpeg-dev=12.1.1.14
|
| 188 |
-
- libparquet=21.0.0
|
| 189 |
-
- libpng=1.6.50
|
| 190 |
-
- libprotobuf=6.31.1
|
| 191 |
-
- libre2-11=2025.07.22
|
| 192 |
-
- libsodium=1.0.20
|
| 193 |
-
- libsqlite=3.50.3
|
| 194 |
-
- libssh2=1.11.1
|
| 195 |
-
- libthrift=0.22.0
|
| 196 |
-
- libtiff=4.7.0
|
| 197 |
-
- libutf8proc=2.10.0
|
| 198 |
-
- libuv=1.48.0
|
| 199 |
-
- libwebp-base=1.6.0
|
| 200 |
-
- libwinpthread=12.0.0.r4.gg4f2fc60ca
|
| 201 |
-
- libxcb=1.17.0
|
| 202 |
-
- libxml2=2.13.8
|
| 203 |
-
- libxslt=1.1.43
|
| 204 |
-
- libzlib=1.3.1
|
| 205 |
-
- lxml=6.0.0
|
| 206 |
-
- lz4-c=1.10.0
|
| 207 |
-
- markupsafe=3.0.2
|
| 208 |
-
- matplotlib=3.10.3
|
| 209 |
-
- matplotlib-base=3.10.3
|
| 210 |
-
- matplotlib-inline=0.1.7
|
| 211 |
-
- mistune=3.1.3
|
| 212 |
-
- mkl=2023.1.0
|
| 213 |
-
- mkl-service=2.4.0
|
| 214 |
-
- mkl_fft=1.3.11
|
| 215 |
-
- mkl_random=1.2.8
|
| 216 |
-
- mpc=1.3.1
|
| 217 |
-
- mpfr=4.2.1
|
| 218 |
-
- mpmath=1.3.0
|
| 219 |
-
- multidict=6.6.3
|
| 220 |
-
- multiprocess=0.70.15
|
| 221 |
-
- munkres=1.1.4
|
| 222 |
-
- nbclient=0.10.2
|
| 223 |
-
- nbconvert-core=7.16.6
|
| 224 |
-
- nbformat=5.10.4
|
| 225 |
-
- nest-asyncio=1.6.0
|
| 226 |
-
- networkx=3.3
|
| 227 |
-
- nltk=3.7
|
| 228 |
-
- notebook=7.4.4
|
| 229 |
-
- notebook-shim=0.2.4
|
| 230 |
-
- numexpr=2.11.0
|
| 231 |
-
- openjpeg=2.5.3
|
| 232 |
-
- openssl=3.5.2
|
| 233 |
-
- orc=2.2.0
|
| 234 |
-
- overrides=7.7.0
|
| 235 |
-
- packaging=25.0
|
| 236 |
-
- pandas=1.5.3
|
| 237 |
-
- pandocfilters=1.5.0
|
| 238 |
-
- parso=0.8.4
|
| 239 |
-
- patsy=1.0.1
|
| 240 |
-
- pcre2=10.45
|
| 241 |
-
- pdfminer.six=20250506
|
| 242 |
-
- pickleshare=0.7.5
|
| 243 |
-
- pillow=11.3.0
|
| 244 |
-
- pip=25.1.1
|
| 245 |
-
- pixman=0.46.4
|
| 246 |
-
- platformdirs=4.3.8
|
| 247 |
-
- prometheus_client=0.22.1
|
| 248 |
-
- prompt_toolkit=3.0.51
|
| 249 |
-
- propcache=0.3.1
|
| 250 |
-
- psutil=7.0.0
|
| 251 |
-
- pthread-stubs=0.4
|
| 252 |
-
- pure_eval=0.2.3
|
| 253 |
-
- pycparser=2.22
|
| 254 |
-
- pygments=2.19.2
|
| 255 |
-
- pyparsing=3.2.3
|
| 256 |
-
- pyside6=6.9.1
|
| 257 |
-
- pysocks=1.7.1
|
| 258 |
-
- python=3.10.18
|
| 259 |
-
- python-dateutil=2.9.0.post0
|
| 260 |
-
- python-docx=1.2.0
|
| 261 |
-
- python-fastjsonschema=2.21.1
|
| 262 |
-
- python-gil=3.10.18
|
| 263 |
-
- python-json-logger=2.0.7
|
| 264 |
-
- python-xxhash=3.5.0
|
| 265 |
-
- python_abi=3.10
|
| 266 |
-
- pytorch-cuda=12.1
|
| 267 |
-
- pytorch-mutex=1.0
|
| 268 |
-
- pytz=2025.2
|
| 269 |
-
- pywin32=311
|
| 270 |
-
- pywinpty=2.0.15
|
| 271 |
-
- pyyaml=6.0.2
|
| 272 |
-
- pyzmq=27.0.0
|
| 273 |
-
- qhull=2020.2
|
| 274 |
-
- qt6-main=6.9.1
|
| 275 |
-
- re2=2025.07.22
|
| 276 |
-
- referencing=0.36.2
|
| 277 |
-
- rfc3339-validator=0.1.4
|
| 278 |
-
- rfc3986-validator=0.1.1
|
| 279 |
-
- rfc3987-syntax=1.1.0
|
| 280 |
-
- rpds-py=0.26.0
|
| 281 |
-
- safetensors=0.6.2
|
| 282 |
-
- scikit-learn=1.7.1
|
| 283 |
-
- scipy=1.15.3
|
| 284 |
-
- seaborn=0.13.2
|
| 285 |
-
- seaborn-base=0.13.2
|
| 286 |
-
- send2trash=1.8.3
|
| 287 |
-
- sentence-transformers=5.1.0
|
| 288 |
-
- setuptools=72.1.0
|
| 289 |
-
- six=1.17.0
|
| 290 |
-
- snappy=1.2.2
|
| 291 |
-
- sniffio=1.3.1
|
| 292 |
-
- soupsieve=2.7
|
| 293 |
-
- stack_data=0.6.3
|
| 294 |
-
- statsmodels=0.14.5
|
| 295 |
-
- tbb=2021.13.0
|
| 296 |
-
- terminado=0.18.1
|
| 297 |
-
- threadpoolctl=3.6.0
|
| 298 |
-
- tinycss2=1.4.0
|
| 299 |
-
- tk=8.6.13
|
| 300 |
-
- tomli=2.2.1
|
| 301 |
-
- tornado=6.5.1
|
| 302 |
-
- tqdm=4.67.1
|
| 303 |
-
- traitlets=5.14.3
|
| 304 |
-
- types-python-dateutil=2.9.0.20250708
|
| 305 |
-
- typing_extensions=4.14.1
|
| 306 |
-
- typing_utils=0.1.0
|
| 307 |
-
- tzdata=2025b
|
| 308 |
-
- ucrt=10.0.22621.0
|
| 309 |
-
- unicodedata2=16.0.0
|
| 310 |
-
- uri-template=1.3.0
|
| 311 |
-
- urllib3=2.5.0
|
| 312 |
-
- vc=14.3
|
| 313 |
-
- vc14_runtime=14.44.35208
|
| 314 |
-
- vs2015_runtime=14.44.35208
|
| 315 |
-
- wcwidth=0.2.13
|
| 316 |
-
- webcolors=24.11.1
|
| 317 |
-
- webencodings=0.5.1
|
| 318 |
-
- websocket-client=1.8.0
|
| 319 |
-
- wheel=0.45.1
|
| 320 |
-
- widgetsnbextension=4.0.14
|
| 321 |
-
- win_inet_pton=1.1.0
|
| 322 |
-
- winpty=0.4.3
|
| 323 |
-
- wordcloud=1.9.4
|
| 324 |
-
- xorg-libxau=1.0.12
|
| 325 |
-
- xorg-libxdmcp=1.1.5
|
| 326 |
-
- xxhash=0.8.3
|
| 327 |
-
- yaml=0.2.5
|
| 328 |
-
- yarl=1.20.1
|
| 329 |
-
- zeromq=4.3.5
|
| 330 |
-
- zipp=3.23.0
|
| 331 |
-
- zstandard=0.23.0
|
| 332 |
-
- zstd=1.5.7
|
| 333 |
- pip:
|
| 334 |
-
-
|
| 335 |
-
-
|
| 336 |
-
-
|
| 337 |
-
-
|
| 338 |
-
- regex==2025.7.34
|
| 339 |
-
- requests==2.32.5
|
| 340 |
-
- sympy==1.13.1
|
| 341 |
-
- tokenizers==0.19.1
|
| 342 |
-
- torch==2.8.0
|
| 343 |
-
- torchaudio==2.5.1+cu121
|
| 344 |
-
- torchvision==0.20.1+cu121
|
| 345 |
-
- transformers==4.41.2
|
|
|
|
| 3 |
|
| 4 |
name: resume-matcher
|
| 5 |
channels:
|
|
|
|
|
|
|
| 6 |
- conda-forge
|
| 7 |
- defaults
|
| 8 |
dependencies:
|
| 9 |
+
- python=3.10
|
| 10 |
+
- pip
|
| 11 |
+
# Core scientific stack
|
| 12 |
+
- numpy
|
| 13 |
+
- pandas
|
| 14 |
+
- scikit-learn
|
| 15 |
+
- scipy
|
| 16 |
+
- joblib
|
| 17 |
+
- tqdm
|
| 18 |
+
- matplotlib
|
| 19 |
+
- seaborn
|
| 20 |
+
- wordcloud
|
| 21 |
+
# NLP / ML essentials
|
| 22 |
+
- faiss-cpu
|
| 23 |
+
- nltk
|
| 24 |
+
- statsmodels
|
| 25 |
+
- huggingface_hub
|
| 26 |
+
# File handling
|
| 27 |
+
- openpyxl
|
| 28 |
+
- lxml
|
| 29 |
+
- pillow
|
| 30 |
+
- pyyaml
|
| 31 |
+
- python-docx
|
| 32 |
+
- pdfminer.six
|
| 33 |
+
# pip-only packages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
- pip:
|
| 35 |
+
- sentence-transformers
|
| 36 |
+
- transformers
|
| 37 |
+
- accelerate
|
| 38 |
+
- datasets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pipelines/app_pipeline.py
CHANGED
|
@@ -1,97 +1,138 @@
|
|
| 1 |
import argparse
|
| 2 |
import os
|
| 3 |
import pandas as pd
|
| 4 |
-
import
|
| 5 |
-
from
|
| 6 |
-
from src.
|
| 7 |
-
from src.processing.text_cleaning import clean_text
|
| 8 |
from src.matching.matching_engine import compute_similarity_matrix, top_n_tfidf_matches, top_n_bert_matches
|
| 9 |
from src.utils.file_reader import extract_text_from_file
|
| 10 |
|
| 11 |
|
| 12 |
def load_job_titles(job_csv_path: str):
|
| 13 |
df = pd.read_csv(job_csv_path)
|
| 14 |
-
if
|
| 15 |
raise ValueError("Job CSV must contain a 'title' column.")
|
| 16 |
return df
|
| 17 |
|
| 18 |
|
|
|
|
| 19 |
def run_tfidf_pipeline(args, raw_resume: str):
|
| 20 |
|
| 21 |
-
# Step
|
| 22 |
cleaned_resume = clean_text(raw_resume)
|
| 23 |
|
| 24 |
-
# Step
|
| 25 |
-
vectorizer =
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
resume_vector = vectorizer.transform([cleaned_resume])
|
| 30 |
|
| 31 |
-
# Step
|
| 32 |
sim_matrix = compute_similarity_matrix(resume_vector, job_matrix)
|
| 33 |
|
| 34 |
-
# Step
|
| 35 |
-
job_df = load_job_titles(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
for job_idx, score in matches[0]:
|
| 42 |
print(f"πΉ {job_df.iloc[job_idx]['title']} (score: {score:0.4f})")
|
| 43 |
|
| 44 |
-
# Optional debug
|
| 45 |
if args.debug:
|
| 46 |
print("\n================ DEBUG MODE ================")
|
| 47 |
print("\nπ--- [DEBUG - TFIDF] Cleaned Resume Preview:\n", cleaned_resume[:1000], "---")
|
| 48 |
-
print(f"\n--- [DEBUG - TFIDF] Raw Similarity Scores (top {
|
| 49 |
for job_idx, score in matches[0]:
|
| 50 |
print(f"[{job_idx}] {job_df.iloc[job_idx]['title']} β {score:0.6f}")
|
| 51 |
print("==============================================")
|
| 52 |
|
| 53 |
|
|
|
|
| 54 |
def run_bert_pipeline(args, raw_resume: str):
|
| 55 |
-
# Step 2: Load SentenceTransformer model
|
| 56 |
-
model = SentenceTransformer(args.bert_model_path)
|
| 57 |
|
| 58 |
-
# Step
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
# Step 4:
|
| 62 |
-
resume_embedding = model.encode(
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
# Step 5: Search
|
| 65 |
-
# Search across all job embeddings in FAISS
|
| 66 |
n_jobs = job_index.ntotal
|
| 67 |
D, I = job_index.search(resume_embedding, n_jobs)
|
| 68 |
|
| 69 |
# Step 6: Load job titles
|
| 70 |
-
job_df = load_job_titles(
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
for idx, score in matches:
|
| 76 |
print(f"πΉ {job_df.iloc[idx]['title']} (score: {score:0.4f})")
|
| 77 |
|
| 78 |
-
# Optional debug
|
| 79 |
if args.debug:
|
| 80 |
print("\n================ DEBUG MODE ================")
|
| 81 |
-
print(f"\n--- [DEBUG - BERT/FAISS] Raw Similarity Scores (top {
|
| 82 |
for idx, score in matches:
|
| 83 |
-
print(f"
|
| 84 |
print("==============================================")
|
| 85 |
|
| 86 |
|
|
|
|
| 87 |
def main(args):
|
| 88 |
try:
|
| 89 |
-
# Step 1: Load raw resume text
|
| 90 |
if not os.path.exists(args.resume_path):
|
| 91 |
-
raise FileNotFoundError(f"Resume file not found: {args.resume_path}")
|
|
|
|
| 92 |
raw_resume = extract_text_from_file(args.resume_path)
|
|
|
|
| 93 |
|
| 94 |
-
#
|
|
|
|
| 95 |
if args.model == "bert":
|
| 96 |
run_bert_pipeline(args, raw_resume)
|
| 97 |
else:
|
|
@@ -103,28 +144,36 @@ def main(args):
|
|
| 103 |
|
| 104 |
if __name__ == "__main__":
|
| 105 |
parser = argparse.ArgumentParser(description="Match a resume to top relevant job titles")
|
| 106 |
-
parser.add_argument('--resume_path', type=str, required=True, help="Path to resume file")
|
| 107 |
-
parser.add_argument('--model', type=str, choices=['tfidf', 'bert'], default='tfidf',
|
| 108 |
-
help="Which model pipeline to use: 'tfidf' or 'bert'")
|
| 109 |
-
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
parser.add_argument(
|
| 113 |
-
parser.add_argument(
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
parser.add_argument(
|
| 117 |
-
help="
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
parser.add_argument(
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
args = parser.parse_args()
|
| 129 |
main(args)
|
| 130 |
-
|
|
|
|
| 1 |
import argparse
|
| 2 |
import os
|
| 3 |
import pandas as pd
|
| 4 |
+
from src.feature_engg.tfidf_vectorizing_data import load_tfidf_vectorizer, load_tfidf_matrix
|
| 5 |
+
from src.feature_engg.bert_embedding_data import get_bert_model, load_faiss_index
|
| 6 |
+
from src.processing.text_cleaning import clean_text, clean_text_for_bert
|
|
|
|
| 7 |
from src.matching.matching_engine import compute_similarity_matrix, top_n_tfidf_matches, top_n_bert_matches
|
| 8 |
from src.utils.file_reader import extract_text_from_file
|
| 9 |
|
| 10 |
|
| 11 |
def load_job_titles(job_csv_path: str):
|
| 12 |
df = pd.read_csv(job_csv_path)
|
| 13 |
+
if "title" not in df.columns:
|
| 14 |
raise ValueError("Job CSV must contain a 'title' column.")
|
| 15 |
return df
|
| 16 |
|
| 17 |
|
| 18 |
+
# ------------------------- TF-IDF PIPELINE -------------------------
|
| 19 |
def run_tfidf_pipeline(args, raw_resume: str):
|
| 20 |
|
| 21 |
+
# Step 1: Clean resume
|
| 22 |
cleaned_resume = clean_text(raw_resume)
|
| 23 |
|
| 24 |
+
# Step 2: Load vectorizer + job matrix (local first, fallback HF)
|
| 25 |
+
vectorizer = load_tfidf_vectorizer(
|
| 26 |
+
local_vectorizer_path=args.local_vectorizer_path,
|
| 27 |
+
repo_id=args.tfidf_repo_id,
|
| 28 |
+
filename=args.vectorizer_filename
|
| 29 |
+
)
|
| 30 |
+
job_matrix = load_tfidf_matrix(
|
| 31 |
+
local_matrix_path=args.local_matrix_path,
|
| 32 |
+
repo_id=args.tfidf_repo_id,
|
| 33 |
+
filename=args.matrix_filename
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Step 3: Vectorize resume
|
| 37 |
resume_vector = vectorizer.transform([cleaned_resume])
|
| 38 |
|
| 39 |
+
# Step 4: Compute cosine similarity
|
| 40 |
sim_matrix = compute_similarity_matrix(resume_vector, job_matrix)
|
| 41 |
|
| 42 |
+
# Step 5: Load job titles
|
| 43 |
+
job_df = load_job_titles("data/app_data/tfidf_job_titles.csv")
|
| 44 |
+
|
| 45 |
+
# Step 6: Get top-N job matches
|
| 46 |
+
top_k = args.top_k
|
| 47 |
+
|
| 48 |
+
if args.top_k > len(job_df['title'].unique()):
|
| 49 |
+
print(f"β οΈ Requested top_k={args.top_k} exceeds unique job titles={len(job_df['title'].unique())}. Reducing top_k.")
|
| 50 |
+
top_k = len(job_df['title'].unique())
|
| 51 |
|
| 52 |
+
elif args.top_k is None:
|
| 53 |
+
top_k = len(job_df['title'].unique())
|
| 54 |
+
print(f"\nβΉοΈ Showing all {top_k} job titles.\n")
|
| 55 |
|
| 56 |
+
matches = top_n_tfidf_matches(sim_matrix, top_n=top_k, job_df=job_df)
|
| 57 |
+
|
| 58 |
+
print(f"\nπ― Top {top_k} Job Matches for the Resume (TF-IDF):")
|
| 59 |
for job_idx, score in matches[0]:
|
| 60 |
print(f"πΉ {job_df.iloc[job_idx]['title']} (score: {score:0.4f})")
|
| 61 |
|
|
|
|
| 62 |
if args.debug:
|
| 63 |
print("\n================ DEBUG MODE ================")
|
| 64 |
print("\nπ--- [DEBUG - TFIDF] Cleaned Resume Preview:\n", cleaned_resume[:1000], "---")
|
| 65 |
+
print(f"\n--- [DEBUG - TFIDF] Raw Similarity Scores (top {top_k}) ---")
|
| 66 |
for job_idx, score in matches[0]:
|
| 67 |
print(f"[{job_idx}] {job_df.iloc[job_idx]['title']} β {score:0.6f}")
|
| 68 |
print("==============================================")
|
| 69 |
|
| 70 |
|
| 71 |
+
# ------------------------- BERT PIPELINE -------------------------
|
| 72 |
def run_bert_pipeline(args, raw_resume: str):
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
# Step 1: Load fine-tuned ST model (local or HF Hub)
|
| 75 |
+
model = get_bert_model(args.local_bert_path or args.bert_repo_id)
|
| 76 |
+
|
| 77 |
+
# Step 2: Load FAISS index (local or HF Hub)
|
| 78 |
+
job_index = load_faiss_index(
|
| 79 |
+
local_index_path=args.local_index_path,
|
| 80 |
+
repo_id=args.bert_repo_id,
|
| 81 |
+
filename=args.index_filename
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Step 3: Clean resume text for transformer
|
| 85 |
+
cleaned_resume = clean_text_for_bert(raw_resume)
|
| 86 |
|
| 87 |
+
# Step 4: Embed
|
| 88 |
+
resume_embedding = model.encode(
|
| 89 |
+
[cleaned_resume],
|
| 90 |
+
normalize_embeddings=True
|
| 91 |
+
)
|
| 92 |
|
| 93 |
+
# Step 5: Search
|
|
|
|
| 94 |
n_jobs = job_index.ntotal
|
| 95 |
D, I = job_index.search(resume_embedding, n_jobs)
|
| 96 |
|
| 97 |
# Step 6: Load job titles
|
| 98 |
+
job_df = load_job_titles("data/app_data/bert_job_titles.csv")
|
| 99 |
|
| 100 |
+
# Step 7: Rank top-N
|
| 101 |
+
top_k = args.top_k
|
| 102 |
|
| 103 |
+
if args.top_k > len(job_df['title'].unique()):
|
| 104 |
+
print(f"β οΈ Requested top_k={args.top_k} exceeds unique job titles={len(job_df['title'].unique())}. Reducing top_k.")
|
| 105 |
+
top_k = len(job_df['title'].unique())
|
| 106 |
+
|
| 107 |
+
elif args.top_k is None:
|
| 108 |
+
top_k = len(job_df['title'].unique())
|
| 109 |
+
print(f"\nβΉοΈ Showing all {top_k} job titles.\n")
|
| 110 |
+
|
| 111 |
+
matches = top_n_bert_matches(I, D, job_df, top_n=top_k)
|
| 112 |
+
|
| 113 |
+
print(f"\nπ― Top {top_k} Job Matches for the Resume (BERT):")
|
| 114 |
for idx, score in matches:
|
| 115 |
print(f"πΉ {job_df.iloc[idx]['title']} (score: {score:0.4f})")
|
| 116 |
|
|
|
|
| 117 |
if args.debug:
|
| 118 |
print("\n================ DEBUG MODE ================")
|
| 119 |
+
print(f"\n--- [DEBUG - BERT/FAISS] Raw Similarity Scores (top {top_k}) ---")
|
| 120 |
for idx, score in matches:
|
| 121 |
+
print(f"[{idx}] {job_df.iloc[idx]['title']} β {score:0.6f}")
|
| 122 |
print("==============================================")
|
| 123 |
|
| 124 |
|
| 125 |
+
# ------------------------- MAIN -------------------------
|
| 126 |
def main(args):
|
| 127 |
try:
|
|
|
|
| 128 |
if not os.path.exists(args.resume_path):
|
| 129 |
+
raise FileNotFoundError(f"β οΈ Resume file not found at: {args.resume_path}")
|
| 130 |
+
|
| 131 |
raw_resume = extract_text_from_file(args.resume_path)
|
| 132 |
+
print(f"\nπ Resume: {args.resume_path}")
|
| 133 |
|
| 134 |
+
# Pipeline selector
|
| 135 |
+
print(f"βοΈ Using model: {args.model.upper()}")
|
| 136 |
if args.model == "bert":
|
| 137 |
run_bert_pipeline(args, raw_resume)
|
| 138 |
else:
|
|
|
|
| 144 |
|
| 145 |
if __name__ == "__main__":
|
| 146 |
parser = argparse.ArgumentParser(description="Match a resume to top relevant job titles")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
# Shared args
|
| 149 |
+
parser.add_argument("--resume_path", type=str, required=True, help="Path to resume file")
|
| 150 |
+
parser.add_argument("--model", type=str, choices=["tfidf", "bert"], default="tfidf")
|
| 151 |
+
parser.add_argument("--top_k", type=int, default=None,
|
| 152 |
+
help="Number of top matches to return if not specified, returns all")
|
| 153 |
+
parser.add_argument("--debug", action="store_true",
|
| 154 |
+
help="print raw similarity scores for both and cleaned resume for tfidf pipeline")
|
| 155 |
+
|
| 156 |
+
# TF-IDF args
|
| 157 |
+
parser.add_argument("--local_vectorizer_path", type=str, default=None,
|
| 158 |
+
help="Local TF-IDF vectorizer .pkl file")
|
| 159 |
+
parser.add_argument("--local_matrix_path", type=str, default=None,
|
| 160 |
+
help="Local TF-IDF job matrix .npz file")
|
| 161 |
+
parser.add_argument("--tfidf_repo_id", type=str, default="Om-Shandilya/resume-matcher-tfidf",
|
| 162 |
+
help="Hub repo id for HuggingFace model")
|
| 163 |
+
parser.add_argument("--vectorizer_filename", type=str, default="applicant/job_vectorizer.pkl",
|
| 164 |
+
help="Filename of vectorizer in the HF repo")
|
| 165 |
+
parser.add_argument("--matrix_filename", type=str, default="applicant/job_matrix.npz",
|
| 166 |
+
help="Filename of matrix in the HF repo")
|
| 167 |
+
|
| 168 |
+
# BERT args
|
| 169 |
+
parser.add_argument("--local_bert_path", type=str, default=None,
|
| 170 |
+
help="Local fine-tuned ST model path")
|
| 171 |
+
parser.add_argument("--local_index_path", type=str, default=None,
|
| 172 |
+
help="Local FAISS index file path")
|
| 173 |
+
parser.add_argument("--bert_repo_id", type=str, default="Om-Shandilya/resume-matcher-bert",
|
| 174 |
+
help="fine-tuned ST model's HF repo id")
|
| 175 |
+
parser.add_argument("--index_filename", type=str, default="applicant/jobs.faiss",
|
| 176 |
+
help="Filename of FAISS index in the HF repo")
|
| 177 |
|
| 178 |
args = parser.parse_args()
|
| 179 |
main(args)
|
|
|
pipelines/recruiter_pipeline.py
CHANGED
|
@@ -1,139 +1,167 @@
|
|
| 1 |
import argparse
|
| 2 |
import os
|
| 3 |
-
import
|
| 4 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 5 |
-
import
|
| 6 |
-
from
|
| 7 |
from src.utils.bulk_loading import bulk_load_raw_resume_files
|
| 8 |
from src.utils.file_reader import extract_text_from_file
|
| 9 |
-
from src.processing.text_cleaning import clean_text
|
| 10 |
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# Step 1: Load vectorizer
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
# Step 2:
|
| 20 |
cleaned_job_text = clean_text(raw_job_text)
|
| 21 |
job_vector = vectorizer.transform([cleaned_job_text])
|
| 22 |
|
| 23 |
-
# Step 3:
|
| 24 |
cleaned_resumes = {fname: clean_text(txt) for fname, txt in raw_resume_texts.items()}
|
| 25 |
resume_matrix = vectorizer.transform(cleaned_resumes.values())
|
| 26 |
|
| 27 |
# Step 4: Compute similarity
|
| 28 |
sims = cosine_similarity(job_vector, resume_matrix)[0]
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
if args.debug:
|
| 31 |
print("\n================ DEBUG MODE ================")
|
| 32 |
-
print("\n[DEBUG - TFIDF] Cleaned
|
| 33 |
-
print(
|
| 34 |
-
print("[DEBUG - TFIDF] First 3 cleaned resumes:")
|
| 35 |
for i, (fname, txt) in enumerate(cleaned_resumes.items()):
|
| 36 |
if i >= 3: break
|
| 37 |
print(f"{fname}: {txt[:300]}...\n")
|
| 38 |
-
print("[DEBUG - TFIDF] Raw
|
|
|
|
|
|
|
| 39 |
print("==============================================")
|
| 40 |
|
| 41 |
-
# Step 5: Rank resumes
|
| 42 |
-
ranked = sorted(zip(cleaned_resumes.keys(), sims), key=lambda x: x[1], reverse=True)
|
| 43 |
-
return ranked
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
raise FileNotFoundError(f"β οΈ BERT model not found: {args.bert_model_path}")
|
| 50 |
|
| 51 |
-
# Step
|
| 52 |
-
|
|
|
|
| 53 |
|
| 54 |
-
# Step
|
| 55 |
-
|
| 56 |
|
| 57 |
-
# Step
|
| 58 |
-
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
|
| 66 |
if args.debug:
|
| 67 |
print("\n================ DEBUG MODE ================")
|
| 68 |
-
print("\n[DEBUG - BERT
|
| 69 |
-
print(
|
| 70 |
-
|
| 71 |
-
for i, (fname, txt) in enumerate(raw_resume_texts.items()):
|
| 72 |
if i >= 3: break
|
| 73 |
print(f"{fname}: {txt[:300]}...\n")
|
| 74 |
-
print(f"[DEBUG - BERT
|
|
|
|
|
|
|
| 75 |
print("==============================================")
|
| 76 |
|
| 77 |
-
# Step 5: Rank resumes
|
| 78 |
-
ranked = [(list(raw_resume_texts.keys())[i], float(scores[0][j]))
|
| 79 |
-
for j, i in enumerate(indices[0])]
|
| 80 |
-
return ranked
|
| 81 |
-
|
| 82 |
|
|
|
|
| 83 |
def main(args):
|
| 84 |
try:
|
| 85 |
-
# Load
|
|
|
|
|
|
|
| 86 |
raw_job_text = extract_text_from_file(args.job_desc_path)
|
|
|
|
|
|
|
|
|
|
| 87 |
raw_resume_texts = bulk_load_raw_resume_files(args.resume_dir)
|
| 88 |
|
| 89 |
if not raw_resume_texts:
|
| 90 |
raise ValueError("β οΈ No valid resumes found in the given directory.")
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
top_k = min(args.top_k, available_resumes)
|
| 95 |
-
|
| 96 |
-
if args.top_k > available_resumes:
|
| 97 |
-
print(f"\nβ οΈ Only {available_resumes} resumes are available. "
|
| 98 |
-
f"Showing top {available_resumes} matches instead of {args.top_k}.\n")
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
ranked = rank_with_bert(args, raw_job_text, raw_resume_texts)
|
| 105 |
else:
|
| 106 |
-
|
| 107 |
|
| 108 |
-
# Display ranked resumes
|
| 109 |
-
print(f"\nπ― Top {top_k} Candidate Matches for the Job ({args.model.upper()}):")
|
| 110 |
-
for i, (fname, score) in enumerate(ranked[:top_k], 1):
|
| 111 |
-
print(f"{i}. {fname} β score: {score:.4f}")
|
| 112 |
-
|
| 113 |
except Exception as e:
|
| 114 |
print(f"β Error: {str(e)}")
|
| 115 |
|
| 116 |
|
| 117 |
if __name__ == "__main__":
|
| 118 |
-
parser = argparse.ArgumentParser(description="Recruiter Pipeline: Rank
|
| 119 |
-
|
| 120 |
-
# Shared
|
| 121 |
-
parser.add_argument(
|
| 122 |
-
parser.add_argument(
|
| 123 |
-
parser.add_argument(
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
parser.add_argument(
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
help="
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
parser.add_argument(
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
args = parser.parse_args()
|
| 139 |
main(args)
|
|
|
|
| 1 |
import argparse
|
| 2 |
import os
|
| 3 |
+
import numpy as np
|
| 4 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 5 |
+
from src.feature_engg.tfidf_vectorizing_data import load_tfidf_vectorizer
|
| 6 |
+
from src.feature_engg.bert_embedding_data import get_bert_model
|
| 7 |
from src.utils.bulk_loading import bulk_load_raw_resume_files
|
| 8 |
from src.utils.file_reader import extract_text_from_file
|
| 9 |
+
from src.processing.text_cleaning import clean_text, clean_text_for_bert
|
| 10 |
|
| 11 |
|
| 12 |
+
# ------------------------- TF-IDF PIPELINE -------------------------
|
| 13 |
+
def run_tfidf_pipeline(args, raw_job_text, raw_resume_texts):
|
| 14 |
+
# Step 1: Load vectorizer (local or HF Hub)
|
| 15 |
+
vectorizer = load_tfidf_vectorizer(
|
| 16 |
+
local_vectorizer_path=args.local_vectorizer_path,
|
| 17 |
+
repo_id=args.tfidf_repo_id,
|
| 18 |
+
filename=args.vectorizer_filename
|
| 19 |
+
)
|
| 20 |
|
| 21 |
+
# Step 2: Clean job description
|
| 22 |
cleaned_job_text = clean_text(raw_job_text)
|
| 23 |
job_vector = vectorizer.transform([cleaned_job_text])
|
| 24 |
|
| 25 |
+
# Step 3: Clean and vectorize resumes
|
| 26 |
cleaned_resumes = {fname: clean_text(txt) for fname, txt in raw_resume_texts.items()}
|
| 27 |
resume_matrix = vectorizer.transform(cleaned_resumes.values())
|
| 28 |
|
| 29 |
# Step 4: Compute similarity
|
| 30 |
sims = cosine_similarity(job_vector, resume_matrix)[0]
|
| 31 |
|
| 32 |
+
# Step 5: Rank resumes
|
| 33 |
+
ranked = sorted(zip(cleaned_resumes.keys(), sims), key=lambda x: x[1], reverse=True)
|
| 34 |
+
|
| 35 |
+
# Step 6: Top-K handling
|
| 36 |
+
top_k = args.top_k
|
| 37 |
+
available_resumes = len(ranked)
|
| 38 |
+
|
| 39 |
+
if args.top_k is None:
|
| 40 |
+
top_k = available_resumes
|
| 41 |
+
print(f"\nβΉοΈ Showing all {available_resumes} resumes.\n")
|
| 42 |
+
elif args.top_k > available_resumes:
|
| 43 |
+
top_k = available_resumes
|
| 44 |
+
print(f"\nβ οΈ Requested top_k={args.top_k} exceeds available resumes={available_resumes}. Reducing top_k.\n")
|
| 45 |
+
|
| 46 |
+
print(f"\nπ― Top {top_k} Candidate Matches for the Job (TF-IDF):")
|
| 47 |
+
for i, (fname, score) in enumerate(ranked[:top_k], 1):
|
| 48 |
+
print(f"{i}. {fname} β score: {score:.4f}")
|
| 49 |
+
|
| 50 |
if args.debug:
|
| 51 |
print("\n================ DEBUG MODE ================")
|
| 52 |
+
print("\nπ--- [DEBUG - TFIDF] Cleaned Job Description Preview:\n", cleaned_job_text[:1000], "---")
|
| 53 |
+
print("\n--- [DEBUG - TFIDF] First 3 Cleaned Resumes ---")
|
|
|
|
| 54 |
for i, (fname, txt) in enumerate(cleaned_resumes.items()):
|
| 55 |
if i >= 3: break
|
| 56 |
print(f"{fname}: {txt[:300]}...\n")
|
| 57 |
+
print(f"\n--- [DEBUG - TFIDF] Raw Similarity Scores (top {top_k}) ---")
|
| 58 |
+
for fname, score in ranked[:top_k]:
|
| 59 |
+
print(f"{fname} β {score:0.6f}")
|
| 60 |
print("==============================================")
|
| 61 |
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
# ------------------------- BERT PIPELINE -------------------------
|
| 64 |
+
def run_bert_pipeline(args, raw_job_text, raw_resume_texts):
|
| 65 |
+
# Step 1: Load fine-tuned ST model (local or HF Hub)
|
| 66 |
+
model = get_bert_model(args.local_bert_path or args.bert_repo_id)
|
| 67 |
+
|
| 68 |
+
# Step 2: Clean job description
|
| 69 |
+
cleaned_job_text = clean_text_for_bert(raw_job_text)
|
| 70 |
+
job_embedding = model.encode([cleaned_job_text], normalize_embeddings=True)
|
| 71 |
|
| 72 |
+
# Step 3: Encode resumes
|
| 73 |
+
cleaned_resumes = {fname: clean_text_for_bert(txt) for fname, txt in raw_resume_texts.items()}
|
| 74 |
+
resume_embeddings = model.encode(list(cleaned_resumes.values()), normalize_embeddings=True)
|
|
|
|
| 75 |
|
| 76 |
+
# Step 4: Compute cosine similarity manually
|
| 77 |
+
# Using dot product as embeddings are normalized and not FAISS since we have small data here.
|
| 78 |
+
sims = np.dot(resume_embeddings, job_embedding.T).flatten()
|
| 79 |
|
| 80 |
+
# Step 5: Rank resumes
|
| 81 |
+
ranked = sorted(zip(cleaned_resumes.keys(), sims), key=lambda x: x[1], reverse=True)
|
| 82 |
|
| 83 |
+
# Step 6: Top-K handling
|
| 84 |
+
top_k = args.top_k
|
| 85 |
+
available_resumes = len(ranked)
|
| 86 |
|
| 87 |
+
if args.top_k is None:
|
| 88 |
+
top_k = available_resumes
|
| 89 |
+
print(f"\nβΉοΈ Showing all {available_resumes} resumes.\n")
|
| 90 |
+
elif args.top_k > available_resumes:
|
| 91 |
+
top_k = available_resumes
|
| 92 |
+
print(f"\nβ οΈ Requested top_k={args.top_k} exceeds available resumes={available_resumes}. Reducing top_k.\n")
|
| 93 |
|
| 94 |
+
print(f"\nπ― Top {top_k} Candidate Matches for the Job (BERT):")
|
| 95 |
+
for i, (fname, score) in enumerate(ranked[:top_k], 1):
|
| 96 |
+
print(f"{i}. {fname} β score: {score:.4f}")
|
| 97 |
|
| 98 |
if args.debug:
|
| 99 |
print("\n================ DEBUG MODE ================")
|
| 100 |
+
print("\nπ--- [DEBUG - BERT] Cleaned Job Description Preview:\n", cleaned_job_text[:1000], "---")
|
| 101 |
+
print("\n--- [DEBUG - BERT] First 3 Cleaned Resumes ---")
|
| 102 |
+
for i, (fname, txt) in enumerate(cleaned_resumes.items()):
|
|
|
|
| 103 |
if i >= 3: break
|
| 104 |
print(f"{fname}: {txt[:300]}...\n")
|
| 105 |
+
print(f"\n--- [DEBUG - BERT] Raw Similarity Scores (top {top_k}) ---")
|
| 106 |
+
for fname, score in ranked[:top_k]:
|
| 107 |
+
print(f"{fname} β {score:0.6f}")
|
| 108 |
print("==============================================")
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
# ------------------------- MAIN -------------------------
|
| 112 |
def main(args):
|
| 113 |
try:
|
| 114 |
+
# Load job description and resumes
|
| 115 |
+
if not os.path.exists(args.job_desc_path):
|
| 116 |
+
raise FileNotFoundError(f"β οΈ Job description not found: {args.job_desc_path}")
|
| 117 |
raw_job_text = extract_text_from_file(args.job_desc_path)
|
| 118 |
+
|
| 119 |
+
if not os.path.exists(args.resume_dir):
|
| 120 |
+
raise FileNotFoundError(f"β οΈ Resume directory not found: {args.resume_dir}")
|
| 121 |
raw_resume_texts = bulk_load_raw_resume_files(args.resume_dir)
|
| 122 |
|
| 123 |
if not raw_resume_texts:
|
| 124 |
raise ValueError("β οΈ No valid resumes found in the given directory.")
|
| 125 |
|
| 126 |
+
print(f"\nπ Job Description: {args.job_desc_path}")
|
| 127 |
+
print(f"π Loaded {len(raw_resume_texts)} resumes from {args.resume_dir}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
+
# Pipeline selector
|
| 130 |
+
print(f"βοΈ Using model: {args.model.upper()}")
|
| 131 |
+
if args.model == "bert":
|
| 132 |
+
run_bert_pipeline(args, raw_job_text, raw_resume_texts)
|
|
|
|
| 133 |
else:
|
| 134 |
+
run_tfidf_pipeline(args, raw_job_text, raw_resume_texts)
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
except Exception as e:
|
| 137 |
print(f"β Error: {str(e)}")
|
| 138 |
|
| 139 |
|
| 140 |
if __name__ == "__main__":
|
| 141 |
+
parser = argparse.ArgumentParser(description="Recruiter Pipeline: Rank resumes for a given job description")
|
| 142 |
+
|
| 143 |
+
# Shared args
|
| 144 |
+
parser.add_argument("--job_desc_path", type=str, required=True, help="Path to job description file")
|
| 145 |
+
parser.add_argument("--resume_dir", type=str, required=True, help="Directory containing applicant resumes")
|
| 146 |
+
parser.add_argument("--model", type=str, choices=["tfidf", "bert"], default="tfidf")
|
| 147 |
+
parser.add_argument("--top_k", type=int, default=None,
|
| 148 |
+
help="Number of top matches to return if not specified, returns all")
|
| 149 |
+
parser.add_argument("--debug", action="store_true",
|
| 150 |
+
help="print raw similarity scores and cleaned texts for debugging")
|
| 151 |
+
|
| 152 |
+
# TF-IDF args
|
| 153 |
+
parser.add_argument("--local_vectorizer_path", type=str, default=None,
|
| 154 |
+
help="Local TF-IDF vectorizer .pkl file")
|
| 155 |
+
parser.add_argument("--tfidf_repo_id", type=str, default="Om-Shandilya/resume-matcher-tfidf",
|
| 156 |
+
help="Hub repo id for HuggingFace TF-IDF model")
|
| 157 |
+
parser.add_argument("--vectorizer_filename", type=str, default="recruiter/combined_vectorizer.pkl",
|
| 158 |
+
help="Filename of vectorizer in the HF repo")
|
| 159 |
+
|
| 160 |
+
# BERT args
|
| 161 |
+
parser.add_argument("--local_bert_path", type=str, default=None,
|
| 162 |
+
help="Local fine-tuned ST model path")
|
| 163 |
+
parser.add_argument("--bert_repo_id", type=str, default="Om-Shandilya/resume-matcher-bert",
|
| 164 |
+
help="fine-tuned ST model's HF repo id")
|
| 165 |
|
| 166 |
args = parser.parse_args()
|
| 167 |
main(args)
|
src/feature_engg/bert_embedding_data.py
CHANGED
|
@@ -2,9 +2,13 @@ import os
|
|
| 2 |
import numpy as np
|
| 3 |
import pandas as pd
|
| 4 |
import torch
|
|
|
|
|
|
|
|
|
|
| 5 |
from typing import Optional
|
| 6 |
from sentence_transformers import SentenceTransformer, models
|
| 7 |
-
import
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def get_bert_model(model_name: str = "all-MiniLM-L6-v2",
|
|
@@ -53,7 +57,7 @@ def save_bert_model(vectorizer: SentenceTransformer,
|
|
| 53 |
|
| 54 |
def bert_embed_text(df: pd.DataFrame,
|
| 55 |
text_column: str,
|
| 56 |
-
label: str,
|
| 57 |
model: Optional[SentenceTransformer] = None,
|
| 58 |
save_path: Optional[str] = None,
|
| 59 |
save_model_file: bool = False):
|
|
@@ -89,26 +93,41 @@ def bert_embed_text(df: pd.DataFrame,
|
|
| 89 |
)
|
| 90 |
|
| 91 |
if save_path and label:
|
| 92 |
-
save_bert_embeddings(embeddings, os.path.join(save_path, f"{label}
|
| 93 |
if save_model_file:
|
| 94 |
-
save_bert_model(model,
|
| 95 |
|
| 96 |
return embeddings, model
|
| 97 |
|
| 98 |
|
| 99 |
-
def
|
| 100 |
-
"""
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
def convert_hf_model_to_st(hf_model_path: str,
|
|
|
|
| 2 |
import numpy as np
|
| 3 |
import pandas as pd
|
| 4 |
import torch
|
| 5 |
+
import faiss
|
| 6 |
+
import torch
|
| 7 |
+
from faiss import read_index
|
| 8 |
from typing import Optional
|
| 9 |
from sentence_transformers import SentenceTransformer, models
|
| 10 |
+
from transformers import AutoTokenizer, AutoModel
|
| 11 |
+
from huggingface_hub import hf_hub_download
|
| 12 |
|
| 13 |
|
| 14 |
def get_bert_model(model_name: str = "all-MiniLM-L6-v2",
|
|
|
|
| 57 |
|
| 58 |
def bert_embed_text(df: pd.DataFrame,
|
| 59 |
text_column: str,
|
| 60 |
+
label: str = 'jobs', # default is 'jobs' as most common use-case in pipeline.
|
| 61 |
model: Optional[SentenceTransformer] = None,
|
| 62 |
save_path: Optional[str] = None,
|
| 63 |
save_model_file: bool = False):
|
|
|
|
| 93 |
)
|
| 94 |
|
| 95 |
if save_path and label:
|
| 96 |
+
save_bert_embeddings(embeddings, os.path.join(save_path, f"{label}.faiss"))
|
| 97 |
if save_model_file:
|
| 98 |
+
save_bert_model(model, save_path)
|
| 99 |
|
| 100 |
return embeddings, model
|
| 101 |
|
| 102 |
|
| 103 |
+
def load_faiss_index(local_index_path: str, repo_id: str, filename: str):
|
| 104 |
+
"""Load FAISS index, preferring local then HF Hub."""
|
| 105 |
+
if local_index_path and os.path.exists(local_index_path):
|
| 106 |
+
print(f"π Loading local FAISS index from {local_index_path}")
|
| 107 |
+
return read_index(local_index_path)
|
| 108 |
+
else:
|
| 109 |
+
print(f"π Downloading FAISS index from Hugging Face Hub ({repo_id})")
|
| 110 |
+
faiss_path = hf_hub_download(repo_id=repo_id, filename=filename)
|
| 111 |
+
return read_index(faiss_path)
|
| 112 |
+
|
| 113 |
+
def load_bert_model(local_model_path: str, repo_id: str):
|
| 114 |
+
"""Load BERT model, preferring local then HF Hub."""
|
| 115 |
+
if local_model_path and os.path.exists(local_model_path):
|
| 116 |
+
print(f"π Loading local BERT model from {local_model_path}")
|
| 117 |
+
tokenizer = AutoTokenizer.from_pretrained(local_model_path)
|
| 118 |
+
model = AutoModel.from_pretrained(local_model_path)
|
| 119 |
+
else:
|
| 120 |
+
print(f"π Downloading BERT model from Hugging Face Hub ({repo_id})")
|
| 121 |
+
tokenizer = AutoTokenizer.from_pretrained(repo_id)
|
| 122 |
+
model = AutoModel.from_pretrained(repo_id)
|
| 123 |
+
return tokenizer, model
|
| 124 |
+
|
| 125 |
+
def mean_pooling(model_output, attention_mask):
|
| 126 |
+
"""Mean pooling for sentence embeddings."""
|
| 127 |
+
token_embeddings = model_output[0] # First element is [batch, seq_len, hidden_dim]
|
| 128 |
+
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
|
| 129 |
+
return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(
|
| 130 |
+
input_mask_expanded.sum(1), min=1e-9)
|
| 131 |
|
| 132 |
|
| 133 |
def convert_hf_model_to_st(hf_model_path: str,
|
src/feature_engg/tfidf_vectorizing_data.py
CHANGED
|
@@ -4,6 +4,7 @@ import joblib
|
|
| 4 |
import os
|
| 5 |
from scipy.sparse import csr_matrix, save_npz, load_npz
|
| 6 |
from typing import Optional, Tuple
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def get_tfidf_vectorizer(max_features: int = 5000,
|
|
@@ -108,19 +109,23 @@ def tfidf_vectorize_text(df: pd.DataFrame,
|
|
| 108 |
return X, vectorizer
|
| 109 |
|
| 110 |
|
| 111 |
-
def
|
| 112 |
-
"""
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
| 118 |
|
| 119 |
|
| 120 |
-
def
|
| 121 |
-
"""
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
| 4 |
import os
|
| 5 |
from scipy.sparse import csr_matrix, save_npz, load_npz
|
| 6 |
from typing import Optional, Tuple
|
| 7 |
+
from huggingface_hub import hf_hub_download
|
| 8 |
|
| 9 |
|
| 10 |
def get_tfidf_vectorizer(max_features: int = 5000,
|
|
|
|
| 109 |
return X, vectorizer
|
| 110 |
|
| 111 |
|
| 112 |
+
def load_tfidf_vectorizer(local_vectorizer_path: str, repo_id: str, filename: str):
|
| 113 |
+
"""Load TF-IDF vectorizer, preferring local then HF Hub."""
|
| 114 |
+
if local_vectorizer_path and os.path.exists(local_vectorizer_path):
|
| 115 |
+
print(f"π Loading local TF-IDF vectorizer from {local_vectorizer_path}")
|
| 116 |
+
return joblib.load(local_vectorizer_path)
|
| 117 |
+
else:
|
| 118 |
+
print(f"π Downloading TF-IDF vectorizer from Hugging Face Hub ({repo_id})")
|
| 119 |
+
vec_path = hf_hub_download(repo_id=repo_id, filename=filename)
|
| 120 |
+
return joblib.load(vec_path)
|
| 121 |
|
| 122 |
|
| 123 |
+
def load_tfidf_matrix(local_matrix_path: str, repo_id: str, filename: str):
|
| 124 |
+
"""Load TF-IDF matrix, preferring local then HF Hub."""
|
| 125 |
+
if local_matrix_path and os.path.exists(local_matrix_path):
|
| 126 |
+
print(f"π Loading local TF-IDF matrix from {local_matrix_path}")
|
| 127 |
+
return load_npz(local_matrix_path)
|
| 128 |
+
else:
|
| 129 |
+
print(f"π Downloading TF-IDF matrix from Hugging Face Hub ({repo_id})")
|
| 130 |
+
mat_path = hf_hub_download(repo_id=repo_id, filename=filename)
|
| 131 |
+
return load_npz(mat_path)
|
src/matching/matching_engine.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import numpy as np
|
| 2 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 3 |
-
from src.feature_engg.tfidf_vectorizing_data import
|
| 4 |
|
| 5 |
def compute_similarity_matrix(X_resumes, X_jobs ):
|
| 6 |
"""
|
|
@@ -70,8 +70,8 @@ if __name__ == "__main__":
|
|
| 70 |
job_vec_path = "models/dev_tfidf/jobs_tfidf_matrix.npz"
|
| 71 |
|
| 72 |
# Load sparse TF-IDF matrices
|
| 73 |
-
X_resumes =
|
| 74 |
-
X_jobs =
|
| 75 |
|
| 76 |
print(f"β
Loaded resumes vector shape: {X_resumes.shape}")
|
| 77 |
print(f"β
Loaded job descriptions vector shape: {X_jobs.shape}")
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
from sklearn.metrics.pairwise import cosine_similarity
|
| 3 |
+
from src.feature_engg.tfidf_vectorizing_data import load_tfidf_matrix
|
| 4 |
|
| 5 |
def compute_similarity_matrix(X_resumes, X_jobs ):
|
| 6 |
"""
|
|
|
|
| 70 |
job_vec_path = "models/dev_tfidf/jobs_tfidf_matrix.npz"
|
| 71 |
|
| 72 |
# Load sparse TF-IDF matrices
|
| 73 |
+
X_resumes = load_tfidf_matrix(resume_vec_path)
|
| 74 |
+
X_jobs = load_tfidf_matrix(job_vec_path)
|
| 75 |
|
| 76 |
print(f"β
Loaded resumes vector shape: {X_resumes.shape}")
|
| 77 |
print(f"β
Loaded job descriptions vector shape: {X_jobs.shape}")
|
src/processing/text_cleaning.py
CHANGED
|
@@ -157,4 +157,24 @@ def clean_column(
|
|
| 157 |
)
|
| 158 |
)
|
| 159 |
print(f"β
Cleaned column '{column_name}' and saved cleaned data to '{new_column_name}' column.")
|
| 160 |
-
return df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
)
|
| 159 |
print(f"β
Cleaned column '{column_name}' and saved cleaned data to '{new_column_name}' column.")
|
| 160 |
+
return df
|
| 161 |
+
|
| 162 |
+
def clean_text_for_bert(text: str) -> str:
|
| 163 |
+
"""
|
| 164 |
+
Minimal cleaning for BERT input.
|
| 165 |
+
Keeps stopwords, punctuation, and numbers because they carry meaning.
|
| 166 |
+
Only removes obvious noise.
|
| 167 |
+
"""
|
| 168 |
+
if pd.isnull(text):
|
| 169 |
+
return ""
|
| 170 |
+
|
| 171 |
+
# Normalize unicode to standard form
|
| 172 |
+
text = unicodedata.normalize("NFKC", text)
|
| 173 |
+
|
| 174 |
+
# Remove HTML tags
|
| 175 |
+
text = re.sub(r"<.*?>", " ", text)
|
| 176 |
+
|
| 177 |
+
# Replace multiple spaces/newlines with a single space
|
| 178 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 179 |
+
|
| 180 |
+
return text
|