Système automatisé de présélection de CVs basé sur l'IA.
Un CV (fichier .txt) est déposé via une interface web, analysé par un LLM (Groq), évalué par un modèle de Machine Learning, et une décision Sélectionné / Refusé est retournée en quelques secondes — avec ou sans explication détaillée selon le mode choisi.
Frontend (Vercel — React)
│
│ POST multipart/form-data (.txt)
▼
n8n (webhook orchestrateur)
│
├── POST /process-cv → Modèle standard (décision seule)
└── POST /process-cv-fair → Modèle équitable (décision + explications)
▼
FastAPI (DigitalOcean — Docker)
│
├── preprocessor.py → extraction regex (âge, adresse, skills, langues...)
├── analyzer.py → appel LLM Groq → JSON structuré (formation, expériences)
├── json2csv.py → transformation du JSON en vecteur numérique
├── model_strict → prédiction ML → "✅ Sélectionné" ou "❌ Refusé"
└── model_FAIR → prédiction ML équitable + explications log-odds
│
│ JSON de résultat
▼
Frontend (affichage de la décision et des explications)
CVision/
├── .python-version # Version Python fixée à 3.12
├── .gitignore # Exclut .env, données brutes, modèles, venv
├── README.md # Cette documentation
│
├── backend/
│ ├── api.py # FastAPI — POST /process-cv et POST /process-cv-fair
│ ├── main.py # Script batch : traite tous les CVs bruts → cv_dataset.csv
│ ├── requirements.txt # Dépendances Python
│ ├── .dockerignore # Exclut notebooks, données brutes, tests du build Docker
│ │
│ ├── config/
│ │ ├── prompt_example.txt # Exemple de prompt LLM (à copier en prompt.txt)
│ │ └── prompt.txt # Prompt réel envoyé au LLM (gitignore)
│ │
│ ├── core/ # Modules métier
│ │ ├── __init__.py
│ │ ├── loader.py # Chargement et validation des fichiers .txt
│ │ ├── preprocessor.py # Pré-traitement regex du texte brut du CV
│ │ ├── analyzer.py # Extraction JSON via LLM (Groq)
│ │ ├── features.py # Conversion JSON → features numériques (legacy)
│ │ └── json2csv.py # Compilation des JSON extraits → DataFrame CSV
│ │
│ ├── data/
│ │ ├── raw/ # CVs bruts au format .txt (gitignore)
│ │ ├── extracted/ # CVs analysés au format .json (gitignore)
│ │ ├── quarantaine/ # CVs en échec de traitement (gitignore)
│ │ ├── cv_dataset.csv # Dataset final (features + labels)
│ │ └── student_labels.csv # Labels manuels (passed_next_stage)
│ │
│ ├── docs/ # Documentation technique
│ │ ├── classification_cv_doc.md # Méthodologie du modèle de classification
│ │ ├── classification_cv_rapport.docx # Rapport au format Word
│ │ ├── ethique_synthese.md # Synthèse éthique IA (AI4People + AI Act)
│ │ ├── fairness_audit.md # Méthodologie de l'audit d'équité
│ │ └── consignes_WP2.md # Cahier des charges Lot 2
│ │
│ ├── models/
│ │ ├── model_classification_cv_strict.joblib # Modèle standard (gitignore)
│ │ └── model_classification_cv_FAIR.joblib # Modèle équitable post-audit (gitignore)
│ │
│ ├── notebooks/
│ │ ├── classification_cv_FINAL_500_a.ipynb # Modèle final (LR L1, 500 CVs, TF-IDF)
│ │ └── fairness_audit_comp.ipynb # Audit d'équité complet + modèle FAIR
│ │
│ └── tests/
│ ├── test_analyser.py # Tests unitaires : Groq, retry, parsing JSON
│ ├── test_features.py # Tests unitaires : cv_to_features
│ ├── test_geo.py # Tests : géocodage et calcul de distance
│ ├── test_json2csv.py # Tests : compilation JSON → CSV
│ ├── test_loader.py # Tests : validation des fichiers .txt
│ └── test_prepocessor.py # Tests : extraction regex du texte
│
├── docs/
│ └── frontend_example.png # Capture d'écran de l'interface
│
├── frontend/ # Interface React + Vite (déployée sur Vercel)
│ ├── src/
│ │ ├── App.jsx # Application principale (2 onglets : Standard & FAIR)
│ │ └── main.jsx # Point d'entrée React
│ ├── package.json # Dépendances (React 19, Vite 8)
│ └── index.html
│
└── infra/
├── Dockerfile # Image Python 3.12-slim, port 8000
└── docker-compose.yml # Service API, volumes data/models, network
- Python 3.12
- Une clé API Groq (gratuite)
- Docker (pour le déploiement)
git clone https://github.com/CVision-dlz/CVision.git
cd CVisionpython -m venv .venv
# Windows
.venv\Scripts\activate
# Linux / macOS
source .venv/bin/activatepip install -r backend/requirements.txtCrée un fichier backend/.env :
GROQ_API_KEY=ta_clé_groq_ici
GROQ_MODEL=llama-3.3-70b-versatile
GROQ_TEMPERATURE=0.0Crée ensuite le fichier prompt à partir de l'exemple :
cp backend/config/prompt_example.txt backend/config/prompt.txt
# Édite backend/config/prompt.txt avec ton prompt RHcd backend
uvicorn api:app --reloadL'API est accessible sur http://localhost:8000.
Analyse un CV et retourne une décision de présélection.
curl -X POST "http://localhost:8000/process-cv" \
-F "file=@cv.txt"Réponse :
{
"target_role": "Senior Data Analyst",
"age": 35,
"distance_ville_haute_km": 12.4,
"education": {
"degree": "Master of Science",
"field": "Data Science",
"school": "UCLouvain",
"education_score": 4
},
"experiences": [...],
"total_experience_years": 3.2,
"skills": ["Python", "SQL", "Tableau"],
"languages": [{"language": "French", "level": "C2", "score": 6}],
"certifications": [{"name": "AWS Data Analytics", "year": 2022}],
"decision": "✅ Sélectionné",
"probability": 0.742,
"threshold_used": 0.635
}Même analyse mais avec le modèle FAIR (sans features discriminatoires) et les explications détaillées de la décision.
curl -X POST "http://localhost:8000/process-cv-fair" \
-F "file=@cv.txt"Réponse (champs supplémentaires) :
{
"decision": "❌ Refusé",
"probability": 0.231,
"threshold_used": 0.635,
"log_odds": -1.204,
"explanations": [
{
"feature": "education_score",
"contribution": -0.712,
"direction": "défavorable"
},
{
"feature": "total_experience_years",
"contribution": 0.314,
"direction": "favorable"
}
]
}Le champ explanations contient le top 10 des features triées par contribution absolue, avec leur direction (favorable / défavorable). Ces contributions sont calculées via la décomposition log-odds (coef_i × x_i).
L'interface React propose deux onglets :
- Upload par glisser-déposer ou clic
- Affiche : décision, âge, expérience, poste visé, diplôme, compétences, langues
- Utilise le modèle
model_classification_cv_strict.joblib
- Même interface d'upload
- Affiche en plus :
- Résumé textuel auto-généré — "Ce candidat est refusé. Principaux freins : Ratio de gaps d'emploi et Prestige de l'école."
- Jauge de seuil — visualise où le candidat se situe par rapport au seuil de décision
- Score log-odds — score brut du modèle
- Barres de contribution catégorisées — contributions groupées par Formation / Expérience / Compétences / Langues
- Utilise le modèle
model_classification_cv_FAIR.joblib
Le script main.py permet de traiter un lot de CVs bruts pour constituer le dataset d'entraînement.
cd backend
python main.pyCe qu'il fait, étape par étape :
- Charge tous les fichiers
.txtdepuisdata/raw/ - Pour chaque CV :
pre_process_cv()→ extraction regex (âge, adresse, skills, langues, certifications)clean_cv_text_for_llm()→ filtre le texte pour n'envoyer que l'essentiel au LLMextract_cv()→ appel Groq → JSON structuré (formation, expériences)compute_experience_metrics()→ calcul durée totale + gaps- Sauvegarde le JSON final dans
data/extracted/ - Si erreur → CV déplacé dans
data/quarantaine/
json2csv()→ compile tous les JSON en un seul DataFrame- Fusion avec
student_labels.csv(labels manuelspassed_next_stage) - Sauvegarde du dataset final dans
data/cv_dataset.csv
Point de reprise : la variable
RESUME_FROMdansmain.pypermet de reprendre le traitement à partir d'un CV précis sans tout retraiter depuis le début.
| Fonction | Rôle |
|---|---|
load_cv(filepath) |
Charge un fichier .txt, vérifie qu'il existe, qu'il est bien au format .txt et non vide |
load_cvs_from_folder(folder_path) |
Charge tous les .txt d'un dossier, retourne un dict {nom_fichier: contenu} |
Extraction par regex des informations directement lisibles dans le texte brut du CV.
| Fonction | Rôle |
|---|---|
pre_process_cv(cv_text) |
Point d'entrée principal — retourne un dict avec âge, distance, skills, langues, certifications |
compute_age(dob) |
Calcule l'âge exact à partir de la date de naissance |
compute_distance_km(address) |
Géocode l'adresse et calcule la distance en km jusqu'à Luxembourg |
extract_skills(section_text) |
Extrait la liste des compétences depuis la section Skills |
extract_languages(section_text) |
Extrait les langues et leur niveau CECRL (A1→C2), convertit en score 1-6 |
extract_certifications(section_text) |
Extrait les certifications et leur année |
score_education(degree) |
Attribue un score 1-5 au diplôme (PhD=5, Master=4, Bachelor=3, BTS=2, Bac=1) |
clean_cv_text_for_llm(cv_text) |
Filtre le texte pour ne garder que Education et Experience |
compute_experience_metrics(experiences) |
Calcule la durée totale d'expérience et identifie les gaps |
Score de niveau de langue (CECRL) :
| Niveau | Score |
|---|---|
| A1 | 1 |
| A2 | 2 |
| B1 | 3 |
| B2 | 4 |
| C1 | 5 |
| C2 | 6 |
Extraction par LLM (Groq) des informations complexes nécessitant de la compréhension.
| Fonction | Rôle |
|---|---|
extract_cv(cv_text) |
Envoie le texte au LLM Groq et retourne un dict JSON structuré |
_get_groq_config() |
Lit GROQ_API_KEY, GROQ_MODEL, GROQ_TEMPERATURE depuis les variables d'environnement |
Mécanisme de retry :
MAX_RETRIES = 2— réessaie en cas de JSON invalide ou d'erreur réseauRETRY_DELAY_SEC = 3— délai entre chaque tentativeSKIP_ON_FAILURE = False— en cas d'échec, propage l'exception (CV mis en quarantaine)
Variables d'environnement :
| Variable | Défaut | Description |
|---|---|---|
GROQ_API_KEY |
— | Obligatoire. Clé API Groq |
GROQ_MODEL |
llama-3.3-70b-versatile |
Modèle Groq à utiliser |
GROQ_TEMPERATURE |
0.0 |
Température du LLM (0 = déterministe) |
Compile tous les fichiers JSON de data/extracted/ en un seul DataFrame pandas prêt pour le ML.
Colonnes de langues gérées : lang_fr, lang_en, lang_de, lang_es, lang_it, lang_other_score_sum
Convertit un CV JSON en vecteur numérique simplifié (utilisé en legacy — l'API utilise directement json2csv.py + feature engineering inline).
API FastAPI exposant deux endpoints.
POST /process-cv — Modèle standard
- Charge
model_classification_cv_strict.joblibau démarrage - Retourne : décision, probabilité, seuil, features calculées
POST /process-cv-fair — Modèle équitable
- Charge
model_classification_cv_FAIR.joblibau démarrage - Retourne en plus :
log_odds,explanations(top 10 contributions log-odds par feature) - Les contributions sont calculées via
coef_i × x_isur les features transformées
CORS : allow_origins=["*"] — accepte toutes les origines
- 500 CVs, 21 colonnes
- Variable cible :
passed_next_stage(0 = Refusé, 1 = Sélectionné) - Déséquilibre : ~80% refusés / ~20% sélectionnés → traité par
class_weight='balanced'
- Algorithme : Logistic Regression L1 (Lasso) avec
LogisticRegressionCV - Features : âge, distance, expérience, gaps, scores de langues, TF-IDF skills/certifications, score école
- Feature engineering :
avg_gap_duration,gap_ratio,skills_count,certif_count,has_certif - Pipeline :
ColumnTransformer(StandardScaler + OneHotEncoder + TfidfVectorizer) → LogisticRegression - Seuil : optimisé sur métrique F0.5 (favorise la précision sur le rappel)
- Validation : cross-validation stratifiée 5-folds
| Métrique | Valeur |
|---|---|
| ROC-AUC | 0.706 |
| Précision (Sélectionné) | 0.41 |
| Rappel (Sélectionné) | 0.70 |
| Accuracy | 74% |
Audit complet réalisé avant déploiement, conformément à l'AI Act (Annexe III — systèmes à haut risque).
| Attribut | Risque identifié | Base légale |
|---|---|---|
age |
Discrimination directe par âge | Directive 2000/78/CE |
distance_ville_haute_km |
Proxy géographique / d'origine | RGPD Art. 9 |
lang_de, lang_es, lang_it |
Proxy de nationalité (coefficients L1 actifs) | Directive 2000/43/CE |
education_degree |
Discrimination socioéconomique indirecte | AI Act Annexe III |
education_score |
Biais de prestige des institutions | AI Act Art. 10 |
| Métrique | Seuil d'alerte | Description |
|---|---|---|
| Equal Opportunity Difference (EOD) | > 0.10 | Écart de TPR entre groupes — métrique principale |
| Demographic Parity Difference (DP) | > 0.10 | Écart brut de taux de sélection |
| Disparate Impact Ratio (DI) | < 0.80 | Règle des 80% (jurisprudence EU) |
L'EOD est retenue comme métrique principale car les base rates diffèrent légitimement entre groupes (les Juniors ont 10,5% de taux réel de qualification vs 29,1% pour les Seniors).
Features supprimées : age, distance_ville_haute_km, lang_de, lang_es, lang_it, lang_other_score_sum
| Métrique | Modèle standard | Modèle FAIR |
|---|---|---|
| ROC-AUC | 0.706 | 0.687 |
| Accuracy | 74% | 75% |
| Rappel (Sélectionné) | 0.70 | 0.55 |
Le trade-off −0.02 ROC-AUC est accepté en échange de la suppression des proxies discriminatoires.
Chaque décision du modèle FAIR est accompagnée des contributions log-odds par feature, conformément à l'Art. 14 AI Act (supervision humaine). Le recruteur peut voir exactement quels critères ont influencé la décision.
cd backend
python -m pytest tests/| Fichier | Ce qui est testé |
|---|---|
test_analyser.py |
Appel Groq, retry sur JSON invalide, retry sur erreur réseau, strip des balises markdown |
test_features.py |
Conversion JSON → features numériques |
test_geo.py |
Géocodage et calcul de distance |
test_json2csv.py |
Compilation JSON → DataFrame |
test_loader.py |
Validation des fichiers : inexistant, mauvais format, vide |
test_prepocessor.py |
Extraction regex : âge, langues, certifications, gaps |
Name: Prénom Nom
Gender: Male/Female
Date of Birth: YYYY-MM-DD
Address: Rue, Ville, Pays
Email: email@exemple.com
Phone: +32 ...
Target Role: Data Scientist
Professional Summary:
...
Education:
...
Experience:
...
Skills:
Python, SQL, Docker, ...
Languages:
French — C2
English — B2
Certifications:
AWS Certified Developer — 2023
cd infra
docker compose up --build -dL'API sera disponible sur le port 8000.
Volumes montés :
backend/data→/app/databackend/models→/app/models
Les modèles
.joblibsont dans.gitignoreet doivent être copiés manuellement sur le serveur viascp.
scp backend/models/model_classification_cv_FAIR.joblib user@server:~/CVision/backend/models/
scp backend/models/model_classification_cv_strict.joblib user@server:~/CVision/backend/models/Projet réalisé dans le cadre du cours : Projet Global en Intelligence Artificielle — HELMo Bloc 2 Q2
