Spaces:
Sleeping
Sleeping
Matis Codjia
commited on
Commit
·
1d8c2e0
1
Parent(s):
e4d3f10
Scoring app
Browse files- CHANGELOG.md +185 -0
- CONFIGURATION.md +343 -0
- Dockerfile +20 -6
- QUICKSTART.md +265 -0
- README.md +139 -10
- app.py +415 -0
- backend/__init__.py +3 -0
- backend/annotator_config.py +279 -0
- backend/auth.py +148 -0
- backend/data_loader.py +31 -0
- backend/export.py +65 -0
- backend/hf_storage.py +237 -0
- backend/persistence.py +157 -0
- backend/statistics.py +53 -0
- frontend/__init__.py +3 -0
- frontend/components.py +189 -0
- frontend/help_page.py +308 -0
- frontend/styles.py +132 -0
- requirements.txt +5 -3
CHANGELOG.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Changelog - Version Production
|
| 2 |
+
|
| 3 |
+
## Version 2.0.0 - Production Ready (2025-01-27)
|
| 4 |
+
|
| 5 |
+
### 🎉 Nouvelles fonctionnalités majeures
|
| 6 |
+
|
| 7 |
+
#### 🔐 Sécurité et Authentification
|
| 8 |
+
- **Authentification par mot de passe** : Accès sécurisé via `APP_PASSWORD`
|
| 9 |
+
- **Système de dispatch multi-annotateurs** : Chaque annotateur ne voit que sa portion
|
| 10 |
+
- **Isolation des données** : Traçabilité complète par annotateur
|
| 11 |
+
- Module : `backend/auth.py`
|
| 12 |
+
|
| 13 |
+
#### 👥 Gestion des Annotateurs
|
| 14 |
+
- **Configuration flexible** : Fichier JSON ou secrets HF
|
| 15 |
+
- **Assignment automatique** : Filtrage par `start_idx` / `end_idx`
|
| 16 |
+
- **Interface de sélection** : Choix visuel de l'identifiant
|
| 17 |
+
- **Validation** : Vérification de la cohérence de la config
|
| 18 |
+
- Module : `backend/annotator_config.py`
|
| 19 |
+
|
| 20 |
+
#### ☁️ Persistance Cloud
|
| 21 |
+
- **Sauvegarde HuggingFace** : Stockage permanent sur dataset privé
|
| 22 |
+
- **Auto-restore** : Chargement automatique au démarrage
|
| 23 |
+
- **Format horodaté** : Fichiers JSON avec timestamp
|
| 24 |
+
- **Bouton explicite** : Sauvegarde manuelle à tout moment
|
| 25 |
+
- Module : `backend/hf_storage.py`
|
| 26 |
+
|
| 27 |
+
### 🔧 Modifications
|
| 28 |
+
|
| 29 |
+
#### Interface Utilisateur
|
| 30 |
+
- Affichage de l'identité annotateur dans la sidebar
|
| 31 |
+
- Indicateur de portion assignée (start-end)
|
| 32 |
+
- Badge de statut sauvegarde cloud
|
| 33 |
+
- Bouton "☁️ Sauvegarder sur HF" proéminent
|
| 34 |
+
- Nom de fichier export personnalisé par annotateur
|
| 35 |
+
|
| 36 |
+
#### Backend
|
| 37 |
+
- Filtrage automatique du dataset au chargement
|
| 38 |
+
- Stockage du dataset complet + portion filtrée
|
| 39 |
+
- Métadonnées enrichies (annotator_id, filtered, etc.)
|
| 40 |
+
- Support secrets HF + variables d'environnement
|
| 41 |
+
|
| 42 |
+
#### Configuration
|
| 43 |
+
- Nouveau fichier `data/annotators.json`
|
| 44 |
+
- Exemple fourni : `data/annotators.json.example`
|
| 45 |
+
- Secrets HF : `APP_PASSWORD`, `HF_TOKEN`, `HF_DATASET_REPO`
|
| 46 |
+
- Fallback sur config par défaut si non configuré
|
| 47 |
+
|
| 48 |
+
### 📚 Documentation
|
| 49 |
+
|
| 50 |
+
#### Nouveaux fichiers
|
| 51 |
+
- **QUICKSTART.md** : Guide de démarrage rapide (15 min)
|
| 52 |
+
- **CONFIGURATION.md** : Documentation complète de configuration
|
| 53 |
+
- **CHANGELOG.md** : Ce fichier
|
| 54 |
+
|
| 55 |
+
#### Mises à jour
|
| 56 |
+
- **README.md** : Vue d'ensemble production, architecture, sécurité
|
| 57 |
+
- **requirements.txt** : Ajout de `huggingface_hub>=0.20.0`
|
| 58 |
+
- **.gitignore** : Ignorer secrets et données sensibles
|
| 59 |
+
|
| 60 |
+
### 🏗️ Architecture
|
| 61 |
+
|
| 62 |
+
#### Nouveaux modules
|
| 63 |
+
```
|
| 64 |
+
backend/
|
| 65 |
+
├── auth.py # Authentification
|
| 66 |
+
├── annotator_config.py # Configuration annotateurs
|
| 67 |
+
└── hf_storage.py # Sauvegarde HuggingFace
|
| 68 |
+
|
| 69 |
+
data/
|
| 70 |
+
└── annotators.json.example # Template configuration
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
#### Flux d'exécution
|
| 74 |
+
1. **Authentification** : Vérification mot de passe
|
| 75 |
+
2. **Sélection** : Choix de l'annotateur
|
| 76 |
+
3. **Filtrage** : Dataset filtré automatiquement
|
| 77 |
+
4. **Annotation** : Interface scoring
|
| 78 |
+
5. **Persistance** : Sauvegarde locale + HF cloud
|
| 79 |
+
|
| 80 |
+
### 🔒 Sécurité
|
| 81 |
+
|
| 82 |
+
#### Protections ajoutées
|
| 83 |
+
- ✅ Mot de passe obligatoire pour accès
|
| 84 |
+
- ✅ Tokens jamais en clair dans le code
|
| 85 |
+
- ✅ Dataset de stockage privé recommandé
|
| 86 |
+
- ✅ Isolation des annotateurs
|
| 87 |
+
- ✅ Traçabilité complète (qui a annoté quoi)
|
| 88 |
+
|
| 89 |
+
#### Bonnes pratiques
|
| 90 |
+
- Secrets via HF Spaces (pas de commit)
|
| 91 |
+
- Validation des configurations
|
| 92 |
+
- Messages d'erreur explicites
|
| 93 |
+
- Mode développement avec fallbacks
|
| 94 |
+
|
| 95 |
+
### 📊 Intégration FFGen
|
| 96 |
+
|
| 97 |
+
#### Workflow complet
|
| 98 |
+
1. **FFGen** : `create_annotation_study.py` → Créer subsets + gold
|
| 99 |
+
2. **App** : Distribuer et collecter annotations
|
| 100 |
+
3. **FFGen** : `analyze_agreement.py` → Calculer accord
|
| 101 |
+
4. **FFGen** : `merge_scores.py` → Dataset final
|
| 102 |
+
|
| 103 |
+
#### Compatibilité
|
| 104 |
+
- Format identique aux scripts FFGen
|
| 105 |
+
- Support des gold items pour IAA
|
| 106 |
+
- Export JSONL directement utilisable
|
| 107 |
+
- Métadonnées compatibles
|
| 108 |
+
|
| 109 |
+
### 🐛 Corrections
|
| 110 |
+
|
| 111 |
+
- Fix : Sauvegarde perdue après redémarrage → Sauvegarde HF
|
| 112 |
+
- Fix : Tous les annotateurs voient tout → Filtrage par portion
|
| 113 |
+
- Fix : Pas de traçabilité → annotator_id dans toutes les sauvegardes
|
| 114 |
+
- Fix : Accès public non sécurisé → Authentification obligatoire
|
| 115 |
+
|
| 116 |
+
### ⚠️ Breaking Changes
|
| 117 |
+
|
| 118 |
+
#### Configuration requise
|
| 119 |
+
- **Avant** : Aucune config nécessaire
|
| 120 |
+
- **Après** : Secrets HF obligatoires en production
|
| 121 |
+
|
| 122 |
+
#### Workflow
|
| 123 |
+
- **Avant** : Un seul utilisateur, dataset complet
|
| 124 |
+
- **Après** : Multi-utilisateurs, portions filtrées
|
| 125 |
+
|
| 126 |
+
#### Migration depuis v1.0
|
| 127 |
+
1. Configurer les secrets HF (voir QUICKSTART.md)
|
| 128 |
+
2. Créer `data/annotators.json`
|
| 129 |
+
3. Pousser la nouvelle version
|
| 130 |
+
4. Informer les annotateurs du nouveau workflow
|
| 131 |
+
|
| 132 |
+
### 📈 Métriques
|
| 133 |
+
|
| 134 |
+
#### Fichiers modifiés
|
| 135 |
+
- 4 fichiers modifiés
|
| 136 |
+
- 3 nouveaux modules backend
|
| 137 |
+
- 3 nouveaux fichiers documentation
|
| 138 |
+
- 1 fichier exemple configuration
|
| 139 |
+
|
| 140 |
+
#### Lignes de code
|
| 141 |
+
- ~600 lignes ajoutées (backend)
|
| 142 |
+
- ~200 lignes ajoutées (app.py)
|
| 143 |
+
- ~800 lignes documentation
|
| 144 |
+
|
| 145 |
+
### 🚀 Déploiement
|
| 146 |
+
|
| 147 |
+
#### Prérequis
|
| 148 |
+
1. Créer dataset HF privé pour stockage
|
| 149 |
+
2. Créer token HF avec droits write
|
| 150 |
+
3. Configurer secrets dans HF Space
|
| 151 |
+
4. Créer configuration annotateurs
|
| 152 |
+
|
| 153 |
+
#### Commandes
|
| 154 |
+
```bash
|
| 155 |
+
git add .
|
| 156 |
+
git commit -m "Add authentication, dispatch and HF persistence"
|
| 157 |
+
git push origin main
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
Le Space rebuild automatiquement (3-5 min).
|
| 161 |
+
|
| 162 |
+
### 📖 Ressources
|
| 163 |
+
|
| 164 |
+
- **Guide rapide** : [QUICKSTART.md](QUICKSTART.md) - 15 minutes
|
| 165 |
+
- **Documentation complète** : [CONFIGURATION.md](CONFIGURATION.md)
|
| 166 |
+
- **Exemple config** : `data/annotators.json.example`
|
| 167 |
+
|
| 168 |
+
### 🙏 Remerciements
|
| 169 |
+
|
| 170 |
+
Cette version implémente les recommandations de sécurité et persistance pour un déploiement en production sur HuggingFace Spaces avec hébergement gratuit.
|
| 171 |
+
|
| 172 |
+
Inspiré par :
|
| 173 |
+
- Le système de dispatch de FFGen (`create_annotation_study.py`)
|
| 174 |
+
- Les bonnes pratiques HuggingFace Spaces
|
| 175 |
+
- Les besoins réels d'études d'annotation multi-annotateurs
|
| 176 |
+
|
| 177 |
+
---
|
| 178 |
+
|
| 179 |
+
## Versions précédentes
|
| 180 |
+
|
| 181 |
+
### Version 1.0.0 - Version initiale
|
| 182 |
+
- Interface de scoring basique
|
| 183 |
+
- Sauvegarde locale uniquement
|
| 184 |
+
- Un seul utilisateur
|
| 185 |
+
- Pas d'authentification
|
CONFIGURATION.md
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Configuration de l'Application d'Annotation
|
| 2 |
+
|
| 3 |
+
Cette documentation explique comment configurer l'application pour un déploiement en production sur Hugging Face Spaces avec plusieurs annotateurs.
|
| 4 |
+
|
| 5 |
+
## Table des matières
|
| 6 |
+
|
| 7 |
+
1. [Vue d'ensemble](#vue-densemble)
|
| 8 |
+
2. [Étape 1 : Créer le Dataset de stockage](#étape-1--créer-le-dataset-de-stockage)
|
| 9 |
+
3. [Étape 2 : Configurer les Secrets HF](#étape-2--configurer-les-secrets-hf)
|
| 10 |
+
4. [Étape 3 : Configurer les Annotateurs](#étape-3--configurer-les-annotateurs)
|
| 11 |
+
5. [Étape 4 : Pousser sur HF Spaces](#étape-4--pousser-sur-hf-spaces)
|
| 12 |
+
6. [Utilisation pour les Annotateurs](#utilisation-pour-les-annotateurs)
|
| 13 |
+
7. [Récupération des Annotations](#récupération-des-annotations)
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Vue d'ensemble
|
| 18 |
+
|
| 19 |
+
L'application implémente 3 systèmes de sécurité et persistance :
|
| 20 |
+
|
| 21 |
+
1. **Authentification** : Mot de passe unique pour accéder à l'app
|
| 22 |
+
2. **Dispatch** : Chaque annotateur voit uniquement sa portion du dataset
|
| 23 |
+
3. **Persistance** : Sauvegarde automatique sur un Dataset HuggingFace privé
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## Étape 1 : Créer le Dataset de stockage
|
| 28 |
+
|
| 29 |
+
1. Allez sur https://huggingface.co/new-dataset
|
| 30 |
+
|
| 31 |
+
2. Créez un nouveau dataset **PRIVÉ** :
|
| 32 |
+
- Nom : `ffgen-annotations-storage` (ou autre)
|
| 33 |
+
- Type : Dataset
|
| 34 |
+
- Visibilité : **Private** ⚠️ Très important !
|
| 35 |
+
|
| 36 |
+
3. Laissez-le vide, l'app créera automatiquement la structure
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## Étape 2 : Configurer les Secrets HF
|
| 41 |
+
|
| 42 |
+
### 2.1 Créer un Token HuggingFace
|
| 43 |
+
|
| 44 |
+
1. Allez dans vos settings HF : https://huggingface.co/settings/tokens
|
| 45 |
+
2. Créez un nouveau token avec les permissions **write**
|
| 46 |
+
3. Copiez le token (format : `hf_xxxxxxxxxxxx`)
|
| 47 |
+
|
| 48 |
+
### 2.2 Ajouter les Secrets dans le Space
|
| 49 |
+
|
| 50 |
+
1. Allez dans votre Space : `https://huggingface.co/spaces/VOTRE-USERNAME/feedbacks-scoring-app`
|
| 51 |
+
2. Cliquez sur **Settings**
|
| 52 |
+
3. Scrollez vers **Variables and secrets**
|
| 53 |
+
4. Ajoutez les secrets suivants :
|
| 54 |
+
|
| 55 |
+
| Nom | Valeur | Description |
|
| 56 |
+
|-----|--------|-------------|
|
| 57 |
+
| `APP_PASSWORD` | `VotreMotDePasse2025!` | Mot de passe pour accéder à l'app |
|
| 58 |
+
| `HF_TOKEN` | `hf_xxxxxxxxxxxx` | Token avec droits d'écriture |
|
| 59 |
+
| `HF_DATASET_REPO` | `VOTRE-USERNAME/ffgen-annotations-storage` | Nom du dataset de stockage |
|
| 60 |
+
|
| 61 |
+
**Important** :
|
| 62 |
+
- Le token doit avoir les droits d'écriture
|
| 63 |
+
- Le dataset doit être privé pour protéger les annotations
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## Étape 3 : Configurer les Annotateurs
|
| 68 |
+
|
| 69 |
+
### Option A : Configuration automatique (recommandé)
|
| 70 |
+
|
| 71 |
+
Créez un fichier `annotators.json` dans le dossier `data/` :
|
| 72 |
+
|
| 73 |
+
```json
|
| 74 |
+
{
|
| 75 |
+
"annotator_1": {
|
| 76 |
+
"name": "Alice Dupont",
|
| 77 |
+
"start_idx": 0,
|
| 78 |
+
"end_idx": 100,
|
| 79 |
+
"description": "Première portion du dataset"
|
| 80 |
+
},
|
| 81 |
+
"annotator_2": {
|
| 82 |
+
"name": "Bob Martin",
|
| 83 |
+
"start_idx": 100,
|
| 84 |
+
"end_idx": 200,
|
| 85 |
+
"description": "Deuxième portion"
|
| 86 |
+
},
|
| 87 |
+
"annotator_3": {
|
| 88 |
+
"name": "Charlie Durand",
|
| 89 |
+
"start_idx": 200,
|
| 90 |
+
"end_idx": 300,
|
| 91 |
+
"description": "Troisième portion"
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Option B : Configuration via Secret HF
|
| 97 |
+
|
| 98 |
+
Ajoutez un secret `ANNOTATOR_CONFIG` avec la configuration JSON en une ligne :
|
| 99 |
+
|
| 100 |
+
```json
|
| 101 |
+
{"annotator_1":{"name":"Alice","start_idx":0,"end_idx":100},"annotator_2":{"name":"Bob","start_idx":100,"end_idx":200}}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Utilisation du script de génération FFGen
|
| 105 |
+
|
| 106 |
+
Si vous avez déjà utilisé le script `create_annotation_study.py` de FFGen :
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# Depuis FFGen/3_data_processing/
|
| 110 |
+
python create_annotation_study.py \
|
| 111 |
+
matis35/code-feedback-infonce \
|
| 112 |
+
--num-samples 300 \
|
| 113 |
+
--num-subsets 3 \
|
| 114 |
+
--output-dir annotation_study
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Cela crée 3 subsets de ~100 items chacun + gold standard.
|
| 118 |
+
|
| 119 |
+
Pour l'app, configurez les annotateurs correspondants :
|
| 120 |
+
- annotator_1 : subset_01 (indices 0-100)
|
| 121 |
+
- annotator_2 : subset_02 (indices 100-200)
|
| 122 |
+
- annotator_3 : subset_03 (indices 200-300)
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## Étape 4 : Pousser sur HF Spaces
|
| 127 |
+
|
| 128 |
+
1. Commitez tous les changements :
|
| 129 |
+
```bash
|
| 130 |
+
git add .
|
| 131 |
+
git commit -m "Add authentication, dispatch and HF persistence"
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
2. Poussez vers HF :
|
| 135 |
+
```bash
|
| 136 |
+
git push origin main
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
3. Le Space va rebuilder automatiquement (prend 3-5 minutes)
|
| 140 |
+
|
| 141 |
+
4. Une fois prêt, vérifiez que l'app démarre correctement
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
## Utilisation pour les Annotateurs
|
| 146 |
+
|
| 147 |
+
### Workflow complet
|
| 148 |
+
|
| 149 |
+
1. **Accès à l'application**
|
| 150 |
+
- URL : `https://huggingface.co/spaces/VOTRE-USERNAME/feedbacks-scoring-app`
|
| 151 |
+
- Entrer le mot de passe fourni
|
| 152 |
+
|
| 153 |
+
2. **Sélection de l'identité**
|
| 154 |
+
- Choisir son identifiant dans la liste
|
| 155 |
+
- Le système affiche automatiquement la portion assignée
|
| 156 |
+
|
| 157 |
+
3. **Chargement du dataset**
|
| 158 |
+
- Option 1 : Upload d'un fichier JSONL
|
| 159 |
+
- Option 2 : Chargement depuis HF Hub
|
| 160 |
+
- Le dataset est automatiquement filtré selon la portion
|
| 161 |
+
|
| 162 |
+
4. **Annotation**
|
| 163 |
+
- Scorer les feedbacks (1-5)
|
| 164 |
+
- Ajouter des commentaires optionnels
|
| 165 |
+
- La progression est sauvegardée automatiquement
|
| 166 |
+
|
| 167 |
+
5. **Sauvegarde**
|
| 168 |
+
- **Importante** : Cliquer sur "☁️ Sauvegarder sur HF" régulièrement
|
| 169 |
+
- Cette sauvegarde est permanente (survit aux redémarrages)
|
| 170 |
+
- La sauvegarde locale est perdue tous les 48h
|
| 171 |
+
|
| 172 |
+
6. **Export (optionnel)**
|
| 173 |
+
- Télécharger le JSONL en local pour backup
|
| 174 |
+
|
| 175 |
+
### Instructions à donner aux annotateurs
|
| 176 |
+
|
| 177 |
+
> **Bienvenue sur l'outil d'annotation FFGen !**
|
| 178 |
+
>
|
| 179 |
+
> 1. Mot de passe : `[VOTRE_MOT_DE_PASSE]`
|
| 180 |
+
> 2. Choisissez votre identifiant dans la liste
|
| 181 |
+
> 3. Chargez le dataset (demandez le lien HF ou le fichier)
|
| 182 |
+
> 4. Annotez les feedbacks selon les critères fournis
|
| 183 |
+
> 5. **Important** : Sauvegardez sur HF toutes les 30-60 minutes
|
| 184 |
+
> 6. Vous pouvez fermer et reprendre plus tard
|
| 185 |
+
>
|
| 186 |
+
> Questions ? Contactez [VOTRE_EMAIL]
|
| 187 |
+
|
| 188 |
+
---
|
| 189 |
+
|
| 190 |
+
## Récupération des Annotations
|
| 191 |
+
|
| 192 |
+
### Méthode 1 : Via HuggingFace Hub (recommandé)
|
| 193 |
+
|
| 194 |
+
1. Allez sur votre dataset de stockage :
|
| 195 |
+
`https://huggingface.co/datasets/VOTRE-USERNAME/ffgen-annotations-storage`
|
| 196 |
+
|
| 197 |
+
2. Dans le dossier `annotations/`, vous trouverez tous les fichiers :
|
| 198 |
+
- `annotation_annotator_1_20250127_143022.json`
|
| 199 |
+
- `annotation_annotator_2_20250127_151533.json`
|
| 200 |
+
- etc.
|
| 201 |
+
|
| 202 |
+
3. Téléchargez-les ou utilisez la CLI :
|
| 203 |
+
```bash
|
| 204 |
+
huggingface-cli download \
|
| 205 |
+
VOTRE-USERNAME/ffgen-annotations-storage \
|
| 206 |
+
--repo-type dataset \
|
| 207 |
+
--local-dir ./annotations
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
### Méthode 2 : Via l'API Python
|
| 211 |
+
|
| 212 |
+
```python
|
| 213 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 214 |
+
import json
|
| 215 |
+
|
| 216 |
+
api = HfApi(token="hf_xxxxxxxxxxxx")
|
| 217 |
+
|
| 218 |
+
# Lister tous les fichiers
|
| 219 |
+
files = api.list_repo_files(
|
| 220 |
+
repo_id="VOTRE-USERNAME/ffgen-annotations-storage",
|
| 221 |
+
repo_type="dataset"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
# Télécharger et charger chaque annotation
|
| 225 |
+
for file in files:
|
| 226 |
+
if file.startswith("annotations/"):
|
| 227 |
+
local_path = hf_hub_download(
|
| 228 |
+
repo_id="VOTRE-USERNAME/ffgen-annotations-storage",
|
| 229 |
+
filename=file,
|
| 230 |
+
repo_type="dataset",
|
| 231 |
+
token="hf_xxxxxxxxxxxx"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
with open(local_path, 'r') as f:
|
| 235 |
+
data = json.load(f)
|
| 236 |
+
print(f"Annotator: {data['annotator_name']}")
|
| 237 |
+
print(f"Scores: {len(data['scores'])}")
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
### Méthode 3 : Utiliser le script de merge FFGen
|
| 241 |
+
|
| 242 |
+
Si vous avez des gold items pour calculer l'accord inter-annotateurs :
|
| 243 |
+
|
| 244 |
+
```bash
|
| 245 |
+
cd FFGen/3_data_processing
|
| 246 |
+
|
| 247 |
+
# Analyser l'accord
|
| 248 |
+
python analyze_agreement.py \
|
| 249 |
+
../../annotations/annotation_*.json \
|
| 250 |
+
--gold-file annotation_study/matis35_code-feedback-infonce_gold_standard.json
|
| 251 |
+
|
| 252 |
+
# Merger les scores
|
| 253 |
+
python merge_scores.py \
|
| 254 |
+
../../annotations/annotation_*.json \
|
| 255 |
+
-o final_annotations.jsonl
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
---
|
| 259 |
+
|
| 260 |
+
## Dépannage
|
| 261 |
+
|
| 262 |
+
### Le Space ne démarre pas
|
| 263 |
+
- Vérifiez que tous les secrets sont bien configurés
|
| 264 |
+
- Regardez les logs du Space (onglet Logs)
|
| 265 |
+
- Vérifiez que le `HF_TOKEN` a les droits d'écriture
|
| 266 |
+
|
| 267 |
+
### L'authentification ne marche pas
|
| 268 |
+
- Vérifiez que `APP_PASSWORD` est bien dans les secrets
|
| 269 |
+
- Pas d'espaces avant/après le mot de passe
|
| 270 |
+
|
| 271 |
+
### La sauvegarde HF échoue
|
| 272 |
+
- Vérifiez que `HF_TOKEN` a les droits d'écriture
|
| 273 |
+
- Vérifiez que `HF_DATASET_REPO` existe et est privé
|
| 274 |
+
- Regardez les messages d'erreur dans l'app
|
| 275 |
+
|
| 276 |
+
### Un annotateur voit le dataset complet
|
| 277 |
+
- Vérifiez la configuration dans `annotators.json`
|
| 278 |
+
- Vérifiez que les indices start_idx/end_idx sont corrects
|
| 279 |
+
- Rechargez le dataset après modification de la config
|
| 280 |
+
|
| 281 |
+
### Les annotations sont perdues
|
| 282 |
+
- Rappelez aux annotateurs de sauvegarder sur HF
|
| 283 |
+
- La sauvegarde locale est réinitialisée tous les 48h
|
| 284 |
+
- Les sauvegardes HF sont permanentes
|
| 285 |
+
|
| 286 |
+
---
|
| 287 |
+
|
| 288 |
+
## Architecture Technique
|
| 289 |
+
|
| 290 |
+
```
|
| 291 |
+
┌─────────────────────────────────────────────────────────┐
|
| 292 |
+
│ HF Space (Public) │
|
| 293 |
+
│ ┌────────────────────────────────────────────────────┐ │
|
| 294 |
+
│ │ 1. Authentification (APP_PASSWORD) │ │
|
| 295 |
+
│ │ ↓ │ │
|
| 296 |
+
│ │ 2. Sélection Annotateur │ │
|
| 297 |
+
│ │ ↓ │ │
|
| 298 |
+
│ │ 3. Filtrage Dataset (start_idx, end_idx) │ │
|
| 299 |
+
│ │ ↓ │ │
|
| 300 |
+
│ │ 4. Interface d'Annotation │ │
|
| 301 |
+
│ │ ↓ │ │
|
| 302 |
+
│ │ 5. Sauvegarde Cloud (HF_TOKEN) │ │
|
| 303 |
+
│ └────────────────────────────────────────────────────┘ │
|
| 304 |
+
└────────────────────┬────────────────────────────────────┘
|
| 305 |
+
│
|
| 306 |
+
↓
|
| 307 |
+
┌─────────────────────────────────────────────────────────┐
|
| 308 |
+
│ HF Dataset (Private): annotations-storage │
|
| 309 |
+
│ ┌────────────────────────────────────────────────────┐ │
|
| 310 |
+
│ │ annotations/ │ │
|
| 311 |
+
│ │ ├── annotation_annotator_1_timestamp.json │ │
|
| 312 |
+
│ │ ├── annotation_annotator_2_timestamp.json │ │
|
| 313 |
+
│ │ └── annotation_annotator_3_timestamp.json │ │
|
| 314 |
+
│ └────────────────────────────────────────────────────┘ │
|
| 315 |
+
└─────────────────────────────────────────────────────────┘
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
---
|
| 319 |
+
|
| 320 |
+
## Sécurité et Bonnes Pratiques
|
| 321 |
+
|
| 322 |
+
✅ **À faire** :
|
| 323 |
+
- Utiliser des mots de passe forts (12+ caractères)
|
| 324 |
+
- Garder le dataset de stockage **PRIVÉ**
|
| 325 |
+
- Révoquer les tokens après l'étude
|
| 326 |
+
- Sauvegarder régulièrement sur HF
|
| 327 |
+
- Tester avec un annotateur avant le déploiement complet
|
| 328 |
+
|
| 329 |
+
❌ **À éviter** :
|
| 330 |
+
- Partager le token HF publiquement
|
| 331 |
+
- Utiliser le même mot de passe que vos autres comptes
|
| 332 |
+
- Rendre le dataset de stockage public
|
| 333 |
+
- Compter uniquement sur la sauvegarde locale
|
| 334 |
+
- Modifier la config en cours d'annotation
|
| 335 |
+
|
| 336 |
+
---
|
| 337 |
+
|
| 338 |
+
## Support
|
| 339 |
+
|
| 340 |
+
Pour toute question ou problème :
|
| 341 |
+
- Issues GitHub : [LIEN_REPO]
|
| 342 |
+
- Email : [VOTRE_EMAIL]
|
| 343 |
+
- Documentation FFGen : `FFGen/README.md`
|
Dockerfile
CHANGED
|
@@ -1,20 +1,34 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
| 5 |
RUN apt-get update && apt-get install -y \
|
| 6 |
build-essential \
|
| 7 |
curl \
|
| 8 |
git \
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
|
|
|
| 11 |
COPY requirements.txt ./
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
|
|
|
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Install system dependencies
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
build-essential \
|
| 8 |
curl \
|
| 9 |
git \
|
| 10 |
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
+
# Copy requirements and install dependencies
|
| 13 |
COPY requirements.txt ./
|
| 14 |
+
RUN pip3 install --no-cache-dir -r requirements.txt
|
| 15 |
|
| 16 |
+
# Copy application files
|
| 17 |
+
COPY app.py ./
|
| 18 |
+
COPY backend/ ./backend/
|
| 19 |
+
COPY frontend/ ./frontend/
|
| 20 |
|
| 21 |
+
# Create data directory
|
| 22 |
+
RUN mkdir -p /app/data
|
| 23 |
|
| 24 |
+
# Create .streamlit directory and empty secrets file to avoid warnings
|
| 25 |
+
RUN mkdir -p /app/.streamlit && touch /app/.streamlit/secrets.toml
|
| 26 |
|
| 27 |
+
# Expose Streamlit port
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Health check for Hugging Face Spaces
|
| 31 |
+
HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health
|
| 32 |
+
|
| 33 |
+
# Run the application
|
| 34 |
+
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.enableCORS=false", "--server.enableXsrfProtection=false"]
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Guide de Démarrage Rapide
|
| 2 |
+
|
| 3 |
+
Configuration en 5 minutes pour déployer l'application d'annotation sécurisée.
|
| 4 |
+
|
| 5 |
+
## Prérequis
|
| 6 |
+
|
| 7 |
+
- Un compte HuggingFace
|
| 8 |
+
- Accès à votre Space `matis35/feedbacks-scoring-app`
|
| 9 |
+
- Le dataset à annoter (format JSONL ou sur HF Hub)
|
| 10 |
+
|
| 11 |
+
## Étape 1 : Créer le Dataset de Stockage (2 min)
|
| 12 |
+
|
| 13 |
+
1. Allez sur https://huggingface.co/new-dataset
|
| 14 |
+
2. Nom : `ffgen-annotations-storage`
|
| 15 |
+
3. Visibilité : **Private** (très important!)
|
| 16 |
+
4. Cliquez sur "Create dataset"
|
| 17 |
+
5. Laissez-le vide, ne chargez rien
|
| 18 |
+
|
| 19 |
+
✅ Vous avez maintenant : `matis35/ffgen-annotations-storage`
|
| 20 |
+
|
| 21 |
+
## Étape 2 : Créer un Token HF (1 min)
|
| 22 |
+
|
| 23 |
+
1. Allez sur https://huggingface.co/settings/tokens
|
| 24 |
+
2. Cliquez "New token"
|
| 25 |
+
3. Nom : `annotation-app-write`
|
| 26 |
+
4. Type : **Write** (important!)
|
| 27 |
+
5. Cliquez "Generate token"
|
| 28 |
+
6. **Copiez le token** (format `hf_xxxxxxxxxxxx`)
|
| 29 |
+
|
| 30 |
+
⚠️ Gardez ce token en sécurité, ne le partagez pas!
|
| 31 |
+
|
| 32 |
+
## Étape 3 : Configurer les Secrets (2 min)
|
| 33 |
+
|
| 34 |
+
1. Allez sur votre Space : https://huggingface.co/spaces/matis35/feedbacks-scoring-app
|
| 35 |
+
2. Cliquez sur **Settings** (en haut)
|
| 36 |
+
3. Scrollez vers "Variables and secrets"
|
| 37 |
+
4. Ajoutez ces 3 secrets :
|
| 38 |
+
|
| 39 |
+
### Secret 1 : APP_PASSWORD
|
| 40 |
+
- Nom : `APP_PASSWORD`
|
| 41 |
+
- Valeur : Choisissez un mot de passe fort (ex: `Annotator2025!`)
|
| 42 |
+
- Type : Secret
|
| 43 |
+
|
| 44 |
+
### Secret 2 : HF_TOKEN
|
| 45 |
+
- Nom : `HF_TOKEN`
|
| 46 |
+
- Valeur : Le token copié à l'étape 2 (`hf_xxxxx`)
|
| 47 |
+
- Type : Secret
|
| 48 |
+
|
| 49 |
+
### Secret 3 : HF_DATASET_REPO
|
| 50 |
+
- Nom : `HF_DATASET_REPO`
|
| 51 |
+
- Valeur : `matis35/ffgen-annotations-storage`
|
| 52 |
+
- Type : Secret
|
| 53 |
+
|
| 54 |
+
✅ Les 3 secrets doivent être visibles dans la liste
|
| 55 |
+
|
| 56 |
+
## Étape 4 : Configurer les Annotateurs (2 min)
|
| 57 |
+
|
| 58 |
+
### Option A : Configuration basique (3 annotateurs)
|
| 59 |
+
|
| 60 |
+
Copiez le fichier exemple :
|
| 61 |
+
```bash
|
| 62 |
+
cd feedbacks-scoring-app
|
| 63 |
+
cp data/annotators.json.example data/annotators.json
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Éditez `data/annotators.json` selon votre dataset :
|
| 67 |
+
```json
|
| 68 |
+
{
|
| 69 |
+
"annotator_1": {
|
| 70 |
+
"name": "Alice",
|
| 71 |
+
"start_idx": 0,
|
| 72 |
+
"end_idx": 100
|
| 73 |
+
},
|
| 74 |
+
"annotator_2": {
|
| 75 |
+
"name": "Bob",
|
| 76 |
+
"start_idx": 100,
|
| 77 |
+
"end_idx": 200
|
| 78 |
+
},
|
| 79 |
+
"annotator_3": {
|
| 80 |
+
"name": "Charlie",
|
| 81 |
+
"start_idx": 200,
|
| 82 |
+
"end_idx": 300
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Option B : Utiliser vos subsets FFGen
|
| 88 |
+
|
| 89 |
+
Si vous avez utilisé `create_annotation_study.py` :
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
# Vous avez créé 10 subsets avec create_annotation_study.py
|
| 93 |
+
# Configurez 10 annotateurs correspondants
|
| 94 |
+
|
| 95 |
+
# Exemple pour 10 annotateurs, 40 items chacun
|
| 96 |
+
python -c "
|
| 97 |
+
import json
|
| 98 |
+
|
| 99 |
+
config = {}
|
| 100 |
+
for i in range(10):
|
| 101 |
+
config[f'annotator_{i+1}'] = {
|
| 102 |
+
'name': f'Annotateur {i+1}',
|
| 103 |
+
'start_idx': i * 40,
|
| 104 |
+
'end_idx': (i + 1) * 40,
|
| 105 |
+
'description': f'Subset {i+1}/10'
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
with open('data/annotators.json', 'w') as f:
|
| 109 |
+
json.dump(config, f, indent=2)
|
| 110 |
+
|
| 111 |
+
print('✅ Config créée pour 10 annotateurs')
|
| 112 |
+
"
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## Étape 5 : Pousser sur HF (1 min)
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
cd feedbacks-scoring-app
|
| 119 |
+
|
| 120 |
+
# Vérifier les changements
|
| 121 |
+
git status
|
| 122 |
+
|
| 123 |
+
# Commiter
|
| 124 |
+
git add .
|
| 125 |
+
git commit -m "Add secure authentication and HF persistence"
|
| 126 |
+
|
| 127 |
+
# Pousser vers HF Spaces
|
| 128 |
+
git push origin main
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
Le Space va rebuilder automatiquement (3-5 minutes).
|
| 132 |
+
|
| 133 |
+
## Étape 6 : Tester (2 min)
|
| 134 |
+
|
| 135 |
+
1. Attendez que le Space soit "Running" (vert)
|
| 136 |
+
2. Ouvrez l'app : https://huggingface.co/spaces/matis35/feedbacks-scoring-app
|
| 137 |
+
3. Testez la connexion :
|
| 138 |
+
- Entrez le mot de passe (`APP_PASSWORD`)
|
| 139 |
+
- Sélectionnez un annotateur
|
| 140 |
+
- Vérifiez que ça fonctionne
|
| 141 |
+
|
| 142 |
+
## Étape 7 : Distribuer aux Annotateurs
|
| 143 |
+
|
| 144 |
+
Envoyez ce message à vos annotateurs :
|
| 145 |
+
|
| 146 |
+
```
|
| 147 |
+
Bonjour,
|
| 148 |
+
|
| 149 |
+
Voici les informations pour accéder à l'outil d'annotation :
|
| 150 |
+
|
| 151 |
+
URL : https://huggingface.co/spaces/matis35/feedbacks-scoring-app
|
| 152 |
+
Mot de passe : [VOTRE_APP_PASSWORD]
|
| 153 |
+
Votre identifiant : [annotator_X]
|
| 154 |
+
|
| 155 |
+
Instructions :
|
| 156 |
+
1. Ouvrez l'URL et entrez le mot de passe
|
| 157 |
+
2. Sélectionnez votre identifiant dans la liste
|
| 158 |
+
3. Chargez le dataset (je vous enverrai le lien/fichier)
|
| 159 |
+
4. Annotez les feedbacks selon les critères ci-dessous
|
| 160 |
+
5. IMPORTANT : Cliquez sur "☁️ Sauvegarder sur HF" toutes les 30-60 minutes
|
| 161 |
+
6. Vous pouvez fermer et reprendre plus tard
|
| 162 |
+
|
| 163 |
+
Critères d'annotation :
|
| 164 |
+
- Score 1 : [DÉFINIR]
|
| 165 |
+
- Score 2 : [DÉFINIR]
|
| 166 |
+
- Score 3 : [DÉFINIR]
|
| 167 |
+
- Score 4 : [DÉFINIR]
|
| 168 |
+
- Score 5 : [DÉFINIR]
|
| 169 |
+
|
| 170 |
+
Questions ? Contactez-moi : [VOTRE_EMAIL]
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
## Vérification Post-Déploiement
|
| 174 |
+
|
| 175 |
+
✅ Checklist de vérification :
|
| 176 |
+
|
| 177 |
+
- [ ] Le Space démarre sans erreur
|
| 178 |
+
- [ ] L'authentification fonctionne
|
| 179 |
+
- [ ] La sélection d'annotateur fonctionne
|
| 180 |
+
- [ ] Le chargement de dataset fonctionne
|
| 181 |
+
- [ ] Le filtrage par portion fonctionne (vérifier les nombres)
|
| 182 |
+
- [ ] La sauvegarde HF fonctionne (vérifier dans le dataset)
|
| 183 |
+
- [ ] L'export JSONL fonctionne
|
| 184 |
+
- [ ] Les annotateurs peuvent se connecter
|
| 185 |
+
|
| 186 |
+
## Commandes Utiles
|
| 187 |
+
|
| 188 |
+
### Voir les logs du Space
|
| 189 |
+
```bash
|
| 190 |
+
# Via l'interface web : Settings > Logs
|
| 191 |
+
# Ou regarder en temps réel depuis l'onglet "Logs"
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
### Vérifier les annotations sauvegardées
|
| 195 |
+
```bash
|
| 196 |
+
# Allez sur : https://huggingface.co/datasets/matis35/ffgen-annotations-storage
|
| 197 |
+
# Vous devriez voir un dossier annotations/ avec des fichiers .json
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### Télécharger toutes les annotations
|
| 201 |
+
```bash
|
| 202 |
+
huggingface-cli download \
|
| 203 |
+
matis35/ffgen-annotations-storage \
|
| 204 |
+
--repo-type dataset \
|
| 205 |
+
--local-dir ./collected_annotations
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
### Analyser l'accord inter-annotateurs (si gold items)
|
| 209 |
+
```bash
|
| 210 |
+
cd FFGen/3_data_processing
|
| 211 |
+
|
| 212 |
+
python analyze_agreement.py \
|
| 213 |
+
../../collected_annotations/annotations/*.json \
|
| 214 |
+
--gold-file annotation_study/gold_standard.json
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
## Dépannage Express
|
| 218 |
+
|
| 219 |
+
### Le Space ne démarre pas
|
| 220 |
+
```bash
|
| 221 |
+
# Vérifiez les logs
|
| 222 |
+
# Problème courant : secret mal configuré
|
| 223 |
+
# Solution : Vérifiez Settings > Variables and secrets
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
### "HF Storage not configured"
|
| 227 |
+
```bash
|
| 228 |
+
# Il manque HF_TOKEN ou HF_DATASET_REPO
|
| 229 |
+
# Ajoutez-les dans Settings > Secrets
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
### "Authentication failed"
|
| 233 |
+
```bash
|
| 234 |
+
# APP_PASSWORD incorrect ou manquant
|
| 235 |
+
# Vérifiez Settings > Secrets
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### Un annotateur voit tout le dataset
|
| 239 |
+
```bash
|
| 240 |
+
# Problème dans annotators.json
|
| 241 |
+
# Vérifiez start_idx et end_idx
|
| 242 |
+
# Rechargez le dataset après correction
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
### Les annotations disparaissent
|
| 246 |
+
```bash
|
| 247 |
+
# Les annotateurs n'ont pas sauvegardé sur HF
|
| 248 |
+
# Rappelez-leur de cliquer sur "☁️ Sauvegarder sur HF"
|
| 249 |
+
# La sauvegarde locale est perdue tous les 48h
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
## Support
|
| 253 |
+
|
| 254 |
+
Documentation complète : [CONFIGURATION.md](CONFIGURATION.md)
|
| 255 |
+
|
| 256 |
+
Problèmes ?
|
| 257 |
+
- Vérifiez d'abord les logs du Space
|
| 258 |
+
- Consultez la section Dépannage de CONFIGURATION.md
|
| 259 |
+
- Ouvrez une issue GitHub si nécessaire
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
+
**Temps total : ~15 minutes**
|
| 264 |
+
|
| 265 |
+
Prochain fichier à lire : [CONFIGURATION.md](CONFIGURATION.md) pour les détails complets.
|
README.md
CHANGED
|
@@ -1,19 +1,148 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
app_port:
|
| 8 |
tags:
|
| 9 |
- streamlit
|
|
|
|
|
|
|
|
|
|
| 10 |
pinned: false
|
| 11 |
-
short_description:
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Feedback Scoring App
|
| 3 |
+
emoji: 🔐
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
tags:
|
| 9 |
- streamlit
|
| 10 |
+
- annotation
|
| 11 |
+
- feedback
|
| 12 |
+
- secure
|
| 13 |
pinned: false
|
| 14 |
+
short_description: Secure code feedback annotation with multi-annotator support
|
| 15 |
---
|
| 16 |
|
| 17 |
+
# Feedback Scoring Tool - Production Version
|
| 18 |
|
| 19 |
+
Application Streamlit sécurisée pour l'annotation et le scoring de feedbacks de code par plusieurs annotateurs. Version prête pour le déploiement en production avec authentification, dispatch et persistance cloud.
|
| 20 |
|
| 21 |
+
## Fonctionnalités
|
| 22 |
+
|
| 23 |
+
### Annotation
|
| 24 |
+
- 📊 **Interface de scoring intuitive** : Notation de 1 à 5 pour chaque feedback
|
| 25 |
+
- 📈 **Statistiques en temps réel** : Suivi de votre progression et distribution des scores
|
| 26 |
+
- 💬 **Commentaires** : Ajoutez des notes sur chaque annotation
|
| 27 |
+
- 📥 **Import flexible** : Support des fichiers JSONL locaux et datasets HuggingFace
|
| 28 |
+
|
| 29 |
+
### Sécurité et Collaboration
|
| 30 |
+
- 🔐 **Authentification** : Accès protégé par mot de passe
|
| 31 |
+
- 👥 **Multi-annotateurs** : Système de dispatch automatique par portion
|
| 32 |
+
- 🎯 **Isolation** : Chaque annotateur ne voit que sa portion du dataset
|
| 33 |
+
- 📋 **Traçabilité** : Toutes les annotations sont identifiées par annotateur
|
| 34 |
+
|
| 35 |
+
### Persistance
|
| 36 |
+
- ☁️ **Sauvegarde cloud** : Stockage permanent sur HuggingFace Dataset privé
|
| 37 |
+
- 💾 **Auto-sauvegarde** : Progression sauvegardée automatiquement en local
|
| 38 |
+
- 🔄 **Reprise de session** : Continuez où vous en étiez, même après redémarrage
|
| 39 |
+
- 📤 **Export JSONL** : Téléchargez vos annotations à tout moment
|
| 40 |
+
|
| 41 |
+
## Configuration pour Production
|
| 42 |
+
|
| 43 |
+
Cette application nécessite une configuration avant déploiement. Voir **[CONFIGURATION.md](CONFIGURATION.md)** pour le guide complet.
|
| 44 |
+
|
| 45 |
+
### Configuration rapide
|
| 46 |
+
|
| 47 |
+
1. **Créer un dataset HF privé** pour stocker les annotations
|
| 48 |
+
2. **Configurer les secrets** dans Settings du Space :
|
| 49 |
+
- `APP_PASSWORD` : Mot de passe d'accès
|
| 50 |
+
- `HF_TOKEN` : Token avec droits d'écriture
|
| 51 |
+
- `HF_DATASET_REPO` : Nom du dataset de stockage
|
| 52 |
+
3. **Configurer les annotateurs** dans `data/annotators.json`
|
| 53 |
+
4. **Pousser sur HF Spaces**
|
| 54 |
+
|
| 55 |
+
📖 **Documentation complète** : [CONFIGURATION.md](CONFIGURATION.md)
|
| 56 |
+
|
| 57 |
+
## Utilisation (Annotateurs)
|
| 58 |
+
|
| 59 |
+
### Workflow
|
| 60 |
+
1. **Connexion** : Entrez le mot de passe fourni
|
| 61 |
+
2. **Identification** : Sélectionnez votre identifiant
|
| 62 |
+
3. **Chargement** : L'app charge automatiquement votre portion
|
| 63 |
+
4. **Annotation** : Scorez les feedbacks (1-5) avec commentaires
|
| 64 |
+
5. **Sauvegarde** : Cliquez sur "☁️ Sauvegarder sur HF" régulièrement
|
| 65 |
+
6. **Reprise** : Vous pouvez fermer et reprendre plus tard
|
| 66 |
+
|
| 67 |
+
### Format du dataset
|
| 68 |
+
|
| 69 |
+
```json
|
| 70 |
+
{
|
| 71 |
+
"anchor": "code source",
|
| 72 |
+
"positive": "feedback positif",
|
| 73 |
+
"language": "python"
|
| 74 |
+
}
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Architecture
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
backend/
|
| 81 |
+
├── auth.py # Authentification
|
| 82 |
+
├── annotator_config.py # Configuration annotateurs
|
| 83 |
+
├── hf_storage.py # Sauvegarde HuggingFace
|
| 84 |
+
├── data_loader.py # Chargement datasets
|
| 85 |
+
├── persistence.py # Sauvegarde locale
|
| 86 |
+
├── export.py # Export JSONL
|
| 87 |
+
└── statistics.py # Métriques
|
| 88 |
+
|
| 89 |
+
frontend/
|
| 90 |
+
├── components.py # Composants UI
|
| 91 |
+
├── styles.py # Styles CSS
|
| 92 |
+
└── help_page.py # Page d'aide
|
| 93 |
+
|
| 94 |
+
data/
|
| 95 |
+
├── annotators.json # Config annotateurs
|
| 96 |
+
└── [sessions locales] # Sauvegardes temporaires
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## Développement
|
| 100 |
+
|
| 101 |
+
### Local
|
| 102 |
+
```bash
|
| 103 |
+
pip install -r requirements.txt
|
| 104 |
+
|
| 105 |
+
# Définir les variables d'environnement
|
| 106 |
+
export APP_PASSWORD="dev"
|
| 107 |
+
export HF_TOKEN="hf_xxxxx"
|
| 108 |
+
export HF_DATASET_REPO="username/annotations"
|
| 109 |
+
|
| 110 |
+
streamlit run app.py
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Docker
|
| 114 |
+
```bash
|
| 115 |
+
docker build -t feedback-scoring-app .
|
| 116 |
+
docker run -p 7860:7860 \
|
| 117 |
+
-e APP_PASSWORD="dev" \
|
| 118 |
+
-e HF_TOKEN="hf_xxxxx" \
|
| 119 |
+
-e HF_DATASET_REPO="username/annotations" \
|
| 120 |
+
feedback-scoring-app
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## Intégration avec FFGen
|
| 124 |
+
|
| 125 |
+
Cette application s'intègre avec le pipeline [FFGen](https://github.com/YOUR_USERNAME/FFGen) :
|
| 126 |
+
|
| 127 |
+
1. **Préparation** : Utilisez `create_annotation_study.py` pour créer les subsets
|
| 128 |
+
2. **Annotation** : Utilisez cette app pour distribuer et collecter les scores
|
| 129 |
+
3. **Analyse** : Utilisez `analyze_agreement.py` pour calculer l'accord
|
| 130 |
+
4. **Fusion** : Utilisez `merge_scores.py` pour créer le dataset final
|
| 131 |
+
|
| 132 |
+
## Sécurité
|
| 133 |
+
|
| 134 |
+
- ✅ Authentification par mot de passe
|
| 135 |
+
- ✅ Dataset de stockage privé
|
| 136 |
+
- ✅ Isolation des annotateurs
|
| 137 |
+
- ✅ Traçabilité complète
|
| 138 |
+
- ✅ Pas de données sensibles en clair dans le code
|
| 139 |
+
|
| 140 |
+
## Support
|
| 141 |
+
|
| 142 |
+
- 📖 Documentation : [CONFIGURATION.md](CONFIGURATION.md)
|
| 143 |
+
- 🐛 Issues : [GitHub Issues](https://github.com/YOUR_USERNAME/feedbacks-scoring-app/issues)
|
| 144 |
+
- 📧 Contact : your.email@example.com
|
| 145 |
+
|
| 146 |
+
## Licence
|
| 147 |
+
|
| 148 |
+
Ce projet est open source.
|
app.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Application Streamlit standalone pour le scoring de feedbacks
|
| 3 |
+
Architecture backend/frontend modulaire avec persistance
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import streamlit as st
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
from backend.data_loader import (
|
| 10 |
+
load_dataset_from_jsonl,
|
| 11 |
+
load_dataset_from_hf,
|
| 12 |
+
filter_items_with_positive
|
| 13 |
+
)
|
| 14 |
+
from backend.export import prepare_export, export_to_jsonl
|
| 15 |
+
from backend.statistics import (
|
| 16 |
+
compute_progress,
|
| 17 |
+
compute_score_distribution,
|
| 18 |
+
compute_average_score,
|
| 19 |
+
compute_most_common_score,
|
| 20 |
+
find_unscored_indices
|
| 21 |
+
)
|
| 22 |
+
from backend.persistence import SessionManager
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
from frontend.styles import apply_custom_css
|
| 26 |
+
from frontend.components import (
|
| 27 |
+
render_navigation,
|
| 28 |
+
render_code_block,
|
| 29 |
+
render_feedback_block,
|
| 30 |
+
render_score_slider,
|
| 31 |
+
render_comment_field,
|
| 32 |
+
render_progress_metrics,
|
| 33 |
+
render_statistics,
|
| 34 |
+
render_export_section,
|
| 35 |
+
render_quick_actions
|
| 36 |
+
)
|
| 37 |
+
from frontend.help_page import render_help_page
|
| 38 |
+
|
| 39 |
+
# Configuration de la page
|
| 40 |
+
st.set_page_config(
|
| 41 |
+
page_title="Feedback Scoring Tool",
|
| 42 |
+
page_icon="⭐",
|
| 43 |
+
layout="wide",
|
| 44 |
+
initial_sidebar_state="expanded"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
apply_custom_css()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
DATA_DIR = Path("/app/data")
|
| 52 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 53 |
+
|
| 54 |
+
# Initialize session manager
|
| 55 |
+
session_manager = SessionManager(DATA_DIR)
|
| 56 |
+
|
| 57 |
+
# Initialize session state
|
| 58 |
+
if 'dataset' not in st.session_state:
|
| 59 |
+
st.session_state.dataset = None
|
| 60 |
+
if 'feedback_scores' not in st.session_state:
|
| 61 |
+
st.session_state.feedback_scores = {}
|
| 62 |
+
if 'scoring_index' not in st.session_state:
|
| 63 |
+
st.session_state.scoring_index = 0
|
| 64 |
+
if 'feedback_comments' not in st.session_state:
|
| 65 |
+
st.session_state.feedback_comments = {}
|
| 66 |
+
if 'items_with_positive' not in st.session_state:
|
| 67 |
+
st.session_state.items_with_positive = []
|
| 68 |
+
if 'show_help' not in st.session_state:
|
| 69 |
+
st.session_state.show_help = False
|
| 70 |
+
if 'session_loaded' not in st.session_state:
|
| 71 |
+
st.session_state.session_loaded = False
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if not st.session_state.session_loaded:
|
| 75 |
+
session_info = session_manager.get_session_info()
|
| 76 |
+
if session_info:
|
| 77 |
+
st.session_state.session_loaded = True
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# Sidebar - Configuration
|
| 81 |
+
with st.sidebar:
|
| 82 |
+
st.markdown('<div class="section-header">Menu</div>', unsafe_allow_html=True)
|
| 83 |
+
|
| 84 |
+
# Help button - Change text based on current state
|
| 85 |
+
button_text = "Retour aux annotations" if st.session_state.show_help else "Aide & Documentation"
|
| 86 |
+
button_type = "secondary" if st.session_state.show_help else "primary"
|
| 87 |
+
|
| 88 |
+
if st.button(button_text, use_container_width=True, type=button_type):
|
| 89 |
+
st.session_state.show_help = not st.session_state.show_help
|
| 90 |
+
st.rerun()
|
| 91 |
+
|
| 92 |
+
# Check for saved session
|
| 93 |
+
session_info = session_manager.get_session_info()
|
| 94 |
+
if session_info and st.session_state.dataset is None:
|
| 95 |
+
st.markdown('<div class="section-header">Session Sauvegardée</div>', unsafe_allow_html=True)
|
| 96 |
+
|
| 97 |
+
st.info(f"""
|
| 98 |
+
**Session trouvée**
|
| 99 |
+
- Dataset: {session_info['dataset_size']} exemples
|
| 100 |
+
- Scorés: {session_info['total_scored']}
|
| 101 |
+
- Position: {session_info['scoring_index'] + 1}
|
| 102 |
+
- Dernière sauvegarde: {session_info['last_saved'][:19]}
|
| 103 |
+
""")
|
| 104 |
+
|
| 105 |
+
col1, col2 = st.columns(2)
|
| 106 |
+
with col1:
|
| 107 |
+
if st.button("Reprendre", use_container_width=True, type="primary"):
|
| 108 |
+
with st.spinner("Chargement de la session..."):
|
| 109 |
+
dataset, _ = session_manager.load_dataset()
|
| 110 |
+
session_data = session_manager.load_session()
|
| 111 |
+
|
| 112 |
+
st.session_state.dataset = dataset
|
| 113 |
+
st.session_state.feedback_scores = session_data['feedback_scores']
|
| 114 |
+
st.session_state.feedback_comments = session_data['feedback_comments']
|
| 115 |
+
st.session_state.scoring_index = session_data['scoring_index']
|
| 116 |
+
st.session_state.show_help = False
|
| 117 |
+
|
| 118 |
+
st.success("Session reprise")
|
| 119 |
+
st.rerun()
|
| 120 |
+
|
| 121 |
+
with col2:
|
| 122 |
+
if st.button("Nouvelle", use_container_width=True):
|
| 123 |
+
if 'confirm_clear' not in st.session_state:
|
| 124 |
+
st.session_state.confirm_clear = True
|
| 125 |
+
st.warning("Cliquer encore pour confirmer")
|
| 126 |
+
else:
|
| 127 |
+
session_manager.clear_session()
|
| 128 |
+
st.session_state.confirm_clear = False
|
| 129 |
+
st.success("Session effacée")
|
| 130 |
+
st.rerun()
|
| 131 |
+
|
| 132 |
+
st.markdown("---")
|
| 133 |
+
|
| 134 |
+
st.markdown('<div class="section-header">Nouveau Dataset</div>', unsafe_allow_html=True)
|
| 135 |
+
|
| 136 |
+
# Charger un dataset
|
| 137 |
+
upload_option = st.radio(
|
| 138 |
+
"Source:",
|
| 139 |
+
["Fichier local (.jsonl)", "HuggingFace Hub"],
|
| 140 |
+
label_visibility="collapsed"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
if upload_option == "Fichier local (.jsonl)":
|
| 144 |
+
uploaded_file = st.file_uploader("Fichier JSONL", type=['jsonl'], label_visibility="collapsed")
|
| 145 |
+
if uploaded_file is not None:
|
| 146 |
+
if st.button("Charger", use_container_width=True):
|
| 147 |
+
with st.spinner("Chargement..."):
|
| 148 |
+
dataset = load_dataset_from_jsonl(uploaded_file)
|
| 149 |
+
|
| 150 |
+
# Save dataset
|
| 151 |
+
session_manager.save_dataset(dataset, {
|
| 152 |
+
'source': 'local_file',
|
| 153 |
+
'filename': uploaded_file.name
|
| 154 |
+
})
|
| 155 |
+
|
| 156 |
+
st.session_state.dataset = dataset
|
| 157 |
+
st.session_state.scoring_index = 0
|
| 158 |
+
st.session_state.feedback_scores = {}
|
| 159 |
+
st.session_state.feedback_comments = {}
|
| 160 |
+
st.session_state.show_help = False
|
| 161 |
+
|
| 162 |
+
# Save initial empty session
|
| 163 |
+
session_manager.save_session(
|
| 164 |
+
st.session_state.feedback_scores,
|
| 165 |
+
st.session_state.feedback_comments,
|
| 166 |
+
st.session_state.scoring_index
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
st.success(f"Dataset chargé: {len(dataset)} exemples")
|
| 170 |
+
st.rerun()
|
| 171 |
+
|
| 172 |
+
else: # HuggingFace Hub
|
| 173 |
+
hf_dataset = st.text_input("Dataset HF", placeholder="username/dataset", label_visibility="collapsed")
|
| 174 |
+
hf_split = st.text_input("Split", value="train", label_visibility="collapsed")
|
| 175 |
+
|
| 176 |
+
if st.button("Charger depuis HF", use_container_width=True):
|
| 177 |
+
if hf_dataset:
|
| 178 |
+
with st.spinner(f"Téléchargement de {hf_dataset}..."):
|
| 179 |
+
try:
|
| 180 |
+
dataset = load_dataset_from_hf(hf_dataset, hf_split)
|
| 181 |
+
|
| 182 |
+
# Save dataset locally
|
| 183 |
+
session_manager.save_dataset(dataset, {
|
| 184 |
+
'source': 'huggingface',
|
| 185 |
+
'dataset_name': hf_dataset,
|
| 186 |
+
'split': hf_split
|
| 187 |
+
})
|
| 188 |
+
|
| 189 |
+
st.session_state.dataset = dataset
|
| 190 |
+
st.session_state.scoring_index = 0
|
| 191 |
+
st.session_state.feedback_scores = {}
|
| 192 |
+
st.session_state.feedback_comments = {}
|
| 193 |
+
st.session_state.show_help = False
|
| 194 |
+
|
| 195 |
+
# Save initial empty session
|
| 196 |
+
session_manager.save_session(
|
| 197 |
+
st.session_state.feedback_scores,
|
| 198 |
+
st.session_state.feedback_comments,
|
| 199 |
+
st.session_state.scoring_index
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
st.success(f"Dataset chargé et sauvegardé: {len(dataset)} exemples")
|
| 203 |
+
st.rerun()
|
| 204 |
+
except Exception as e:
|
| 205 |
+
st.error(f"Erreur: {str(e)}")
|
| 206 |
+
else:
|
| 207 |
+
st.warning("Entrez un nom de dataset")
|
| 208 |
+
|
| 209 |
+
# Main content - Show help or scoring interface
|
| 210 |
+
if st.session_state.show_help:
|
| 211 |
+
render_help_page()
|
| 212 |
+
st.stop()
|
| 213 |
+
|
| 214 |
+
# Titre
|
| 215 |
+
st.title("Scoring des Feedbacks")
|
| 216 |
+
|
| 217 |
+
# Main content
|
| 218 |
+
if not st.session_state.dataset:
|
| 219 |
+
st.warning("Aucun dataset chargé.")
|
| 220 |
+
st.info("Vérifiez la sidebar : vous avez peut-être une session sauvegardée à reprendre")
|
| 221 |
+
st.info("Cliquez sur Aide & Documentation pour le guide complet")
|
| 222 |
+
st.stop()
|
| 223 |
+
|
| 224 |
+
# Filter items with positive feedback
|
| 225 |
+
dataset = st.session_state.dataset
|
| 226 |
+
items_with_positive = filter_items_with_positive(dataset)
|
| 227 |
+
st.session_state.items_with_positive = items_with_positive
|
| 228 |
+
|
| 229 |
+
if not items_with_positive:
|
| 230 |
+
st.error("Aucun feedback positif trouvé dans le dataset.")
|
| 231 |
+
st.info("Le dataset doit contenir un champ 'positive' avec du texte.")
|
| 232 |
+
st.stop()
|
| 233 |
+
|
| 234 |
+
# Navigation
|
| 235 |
+
new_index = render_navigation(st.session_state.scoring_index, len(items_with_positive))
|
| 236 |
+
if new_index is not None:
|
| 237 |
+
st.session_state.scoring_index = new_index
|
| 238 |
+
# Auto-save on navigation
|
| 239 |
+
session_manager.save_session(
|
| 240 |
+
st.session_state.feedback_scores,
|
| 241 |
+
st.session_state.feedback_comments,
|
| 242 |
+
st.session_state.scoring_index
|
| 243 |
+
)
|
| 244 |
+
st.rerun()
|
| 245 |
+
|
| 246 |
+
st.markdown("---")
|
| 247 |
+
|
| 248 |
+
# Get current item
|
| 249 |
+
original_idx, current_item = items_with_positive[st.session_state.scoring_index]
|
| 250 |
+
|
| 251 |
+
# Display code
|
| 252 |
+
code_text = current_item.get('anchor', current_item.get('code', 'N/A'))
|
| 253 |
+
language = current_item.get('language', 'python')
|
| 254 |
+
render_code_block(code_text, language)
|
| 255 |
+
|
| 256 |
+
st.markdown("---")
|
| 257 |
+
|
| 258 |
+
# Display positive feedback
|
| 259 |
+
positive_feedback = current_item.get('positive', 'N/A')
|
| 260 |
+
render_feedback_block(positive_feedback)
|
| 261 |
+
|
| 262 |
+
st.markdown("---")
|
| 263 |
+
|
| 264 |
+
# Scoring interface
|
| 265 |
+
score_key = f"score_{original_idx}"
|
| 266 |
+
current_score = st.session_state.feedback_scores.get(original_idx, 3)
|
| 267 |
+
score = render_score_slider(score_key, current_score)
|
| 268 |
+
|
| 269 |
+
# Auto-save when score changes
|
| 270 |
+
if score != current_score:
|
| 271 |
+
st.session_state.feedback_scores[original_idx] = score
|
| 272 |
+
session_manager.save_session(
|
| 273 |
+
st.session_state.feedback_scores,
|
| 274 |
+
st.session_state.feedback_comments,
|
| 275 |
+
st.session_state.scoring_index
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
st.session_state.feedback_scores[original_idx] = score
|
| 279 |
+
|
| 280 |
+
# Comment field
|
| 281 |
+
comment_key = f"comment_{original_idx}"
|
| 282 |
+
current_comment = st.session_state.feedback_comments.get(original_idx, "")
|
| 283 |
+
comment = render_comment_field(comment_key, current_comment)
|
| 284 |
+
st.session_state.feedback_comments[original_idx] = comment
|
| 285 |
+
|
| 286 |
+
# Auto-save when comment changes
|
| 287 |
+
if comment != current_comment:
|
| 288 |
+
session_manager.save_session(
|
| 289 |
+
st.session_state.feedback_scores,
|
| 290 |
+
st.session_state.feedback_comments,
|
| 291 |
+
st.session_state.scoring_index
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
st.markdown("---")
|
| 295 |
+
|
| 296 |
+
# Progress and statistics
|
| 297 |
+
scored_items = len(st.session_state.feedback_scores)
|
| 298 |
+
total_items = len(items_with_positive)
|
| 299 |
+
progress_pct = compute_progress(scored_items, total_items) * 100
|
| 300 |
+
|
| 301 |
+
render_progress_metrics(
|
| 302 |
+
total=total_items,
|
| 303 |
+
scored=scored_items,
|
| 304 |
+
remaining=total_items - scored_items,
|
| 305 |
+
progress_pct=progress_pct
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Statistics
|
| 309 |
+
if st.session_state.feedback_scores:
|
| 310 |
+
avg_score = compute_average_score(st.session_state.feedback_scores)
|
| 311 |
+
most_common = compute_most_common_score(st.session_state.feedback_scores)
|
| 312 |
+
score_counts = compute_score_distribution(st.session_state.feedback_scores)
|
| 313 |
+
|
| 314 |
+
render_statistics(avg_score, most_common, score_counts)
|
| 315 |
+
|
| 316 |
+
st.markdown("---")
|
| 317 |
+
|
| 318 |
+
# Export section
|
| 319 |
+
export_data = prepare_export(
|
| 320 |
+
items_with_positive,
|
| 321 |
+
st.session_state.feedback_scores,
|
| 322 |
+
st.session_state.feedback_comments
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
col1, col2 = render_export_section(export_data)
|
| 326 |
+
|
| 327 |
+
# Téléchargement JSONL
|
| 328 |
+
with col2:
|
| 329 |
+
if export_data:
|
| 330 |
+
jsonl_content = export_to_jsonl(export_data)
|
| 331 |
+
|
| 332 |
+
st.download_button(
|
| 333 |
+
label="Télécharger JSONL",
|
| 334 |
+
data=jsonl_content,
|
| 335 |
+
file_name="feedback_scores.jsonl",
|
| 336 |
+
mime="application/jsonl",
|
| 337 |
+
use_container_width=True,
|
| 338 |
+
type="primary"
|
| 339 |
+
)
|
| 340 |
+
else:
|
| 341 |
+
st.button(
|
| 342 |
+
"📥 Télécharger JSONL",
|
| 343 |
+
disabled=True,
|
| 344 |
+
use_container_width=True,
|
| 345 |
+
help="Aucun score enregistré"
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
# Show export preview
|
| 349 |
+
if export_data:
|
| 350 |
+
with st.expander("Aperçu Export (5 premiers)"):
|
| 351 |
+
preview_items = export_data[:5]
|
| 352 |
+
st.json(preview_items)
|
| 353 |
+
|
| 354 |
+
# Quick actions
|
| 355 |
+
st.markdown("---")
|
| 356 |
+
|
| 357 |
+
reset_requested, jump_to_unscored, jump_to_index = render_quick_actions(
|
| 358 |
+
items_with_positive,
|
| 359 |
+
st.session_state.feedback_scores,
|
| 360 |
+
st.session_state.scoring_index
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
# Handle quick actions
|
| 364 |
+
if reset_requested:
|
| 365 |
+
if st.session_state.feedback_scores:
|
| 366 |
+
if 'confirm_reset' not in st.session_state:
|
| 367 |
+
st.session_state.confirm_reset = True
|
| 368 |
+
st.warning("Cliquez à nouveau pour confirmer la suppression de tous les scores")
|
| 369 |
+
else:
|
| 370 |
+
st.session_state.feedback_scores = {}
|
| 371 |
+
st.session_state.feedback_comments = {}
|
| 372 |
+
st.session_state.confirm_reset = False
|
| 373 |
+
|
| 374 |
+
# Save cleared session
|
| 375 |
+
session_manager.save_session(
|
| 376 |
+
st.session_state.feedback_scores,
|
| 377 |
+
st.session_state.feedback_comments,
|
| 378 |
+
st.session_state.scoring_index
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
st.success("Scores réinitialisés")
|
| 382 |
+
st.rerun()
|
| 383 |
+
|
| 384 |
+
if jump_to_unscored:
|
| 385 |
+
unscored_indices = find_unscored_indices(items_with_positive, st.session_state.feedback_scores)
|
| 386 |
+
if unscored_indices:
|
| 387 |
+
for pos, (idx, _) in enumerate(items_with_positive):
|
| 388 |
+
if idx == unscored_indices[0]:
|
| 389 |
+
st.session_state.scoring_index = pos
|
| 390 |
+
|
| 391 |
+
# Save position
|
| 392 |
+
session_manager.save_session(
|
| 393 |
+
st.session_state.feedback_scores,
|
| 394 |
+
st.session_state.feedback_comments,
|
| 395 |
+
st.session_state.scoring_index
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
st.rerun()
|
| 399 |
+
break
|
| 400 |
+
|
| 401 |
+
if jump_to_index is not None:
|
| 402 |
+
st.session_state.scoring_index = jump_to_index
|
| 403 |
+
|
| 404 |
+
# Save position
|
| 405 |
+
session_manager.save_session(
|
| 406 |
+
st.session_state.feedback_scores,
|
| 407 |
+
st.session_state.feedback_comments,
|
| 408 |
+
st.session_state.scoring_index
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
st.rerun()
|
| 412 |
+
|
| 413 |
+
# Footer
|
| 414 |
+
st.markdown("---")
|
| 415 |
+
st.caption("Sauvegarde automatique activée | Vous pouvez fermer et reprendre plus tard")
|
backend/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Backend module for scoring app
|
| 3 |
+
"""
|
backend/annotator_config.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration et gestion des annotateurs
|
| 3 |
+
Permet de définir qui annote quelle partie du dataset
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import streamlit as st
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Configuration par défaut des annotateurs
|
| 13 |
+
DEFAULT_ANNOTATOR_CONFIG = {
|
| 14 |
+
"annotator_1": {
|
| 15 |
+
"name": "Expert A",
|
| 16 |
+
"start_idx": 0,
|
| 17 |
+
"end_idx": 100,
|
| 18 |
+
"description": "Première portion du dataset"
|
| 19 |
+
},
|
| 20 |
+
"annotator_2": {
|
| 21 |
+
"name": "Expert B",
|
| 22 |
+
"start_idx": 100,
|
| 23 |
+
"end_idx": 200,
|
| 24 |
+
"description": "Deuxième portion du dataset"
|
| 25 |
+
},
|
| 26 |
+
"annotator_3": {
|
| 27 |
+
"name": "Expert C",
|
| 28 |
+
"start_idx": 200,
|
| 29 |
+
"end_idx": 300,
|
| 30 |
+
"description": "Troisième portion du dataset"
|
| 31 |
+
},
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_annotator_config():
|
| 36 |
+
"""
|
| 37 |
+
Charge la configuration des annotateurs.
|
| 38 |
+
|
| 39 |
+
Ordre de priorité:
|
| 40 |
+
1. Fichier annotators.json dans /app/data/
|
| 41 |
+
2. st.secrets["ANNOTATOR_CONFIG"]
|
| 42 |
+
3. Variable d'environnement ANNOTATOR_CONFIG
|
| 43 |
+
4. Configuration par défaut
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
dict: Configuration des annotateurs
|
| 47 |
+
"""
|
| 48 |
+
# 1. Essayer de charger depuis un fichier local
|
| 49 |
+
config_file = Path("/app/data/annotators.json")
|
| 50 |
+
if config_file.exists():
|
| 51 |
+
try:
|
| 52 |
+
with open(config_file, 'r', encoding='utf-8') as f:
|
| 53 |
+
config = json.load(f)
|
| 54 |
+
return config
|
| 55 |
+
except Exception as e:
|
| 56 |
+
st.warning(f"⚠️ Erreur lors du chargement de annotators.json: {e}")
|
| 57 |
+
|
| 58 |
+
# 2. Essayer st.secrets (HF Spaces)
|
| 59 |
+
try:
|
| 60 |
+
config_str = st.secrets.get("ANNOTATOR_CONFIG")
|
| 61 |
+
if config_str:
|
| 62 |
+
return json.loads(config_str)
|
| 63 |
+
except (FileNotFoundError, KeyError, json.JSONDecodeError):
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
# 3. Essayer variable d'environnement
|
| 67 |
+
config_str = os.getenv("ANNOTATOR_CONFIG")
|
| 68 |
+
if config_str:
|
| 69 |
+
try:
|
| 70 |
+
return json.loads(config_str)
|
| 71 |
+
except json.JSONDecodeError:
|
| 72 |
+
st.warning("⚠️ ANNOTATOR_CONFIG mal formaté")
|
| 73 |
+
|
| 74 |
+
# 4. Retourner la config par défaut
|
| 75 |
+
return DEFAULT_ANNOTATOR_CONFIG
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def save_annotator_config(config):
|
| 79 |
+
"""
|
| 80 |
+
Sauvegarde la configuration des annotateurs dans un fichier local.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
config: Dict de configuration
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
bool: True si succès
|
| 87 |
+
"""
|
| 88 |
+
try:
|
| 89 |
+
config_file = Path("/app/data/annotators.json")
|
| 90 |
+
config_file.parent.mkdir(exist_ok=True)
|
| 91 |
+
|
| 92 |
+
with open(config_file, 'w', encoding='utf-8') as f:
|
| 93 |
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
| 94 |
+
|
| 95 |
+
return True
|
| 96 |
+
except Exception as e:
|
| 97 |
+
st.error(f"❌ Erreur lors de la sauvegarde: {e}")
|
| 98 |
+
return False
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def get_annotator_config(annotator_id):
|
| 102 |
+
"""
|
| 103 |
+
Récupère la configuration d'un annotateur spécifique.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
annotator_id: ID de l'annotateur
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
dict ou None: Configuration de l'annotateur
|
| 110 |
+
"""
|
| 111 |
+
config = load_annotator_config()
|
| 112 |
+
return config.get(annotator_id)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def filter_dataset_for_annotator(dataset, annotator_config):
|
| 116 |
+
"""
|
| 117 |
+
Filtre un dataset pour ne garder que la portion d'un annotateur.
|
| 118 |
+
|
| 119 |
+
Args:
|
| 120 |
+
dataset: Liste d'items du dataset
|
| 121 |
+
annotator_config: Config de l'annotateur avec start_idx et end_idx
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
list: Portion filtrée du dataset
|
| 125 |
+
"""
|
| 126 |
+
start = annotator_config.get("start_idx", 0)
|
| 127 |
+
end = annotator_config.get("end_idx", len(dataset))
|
| 128 |
+
|
| 129 |
+
# S'assurer que les indices sont valides
|
| 130 |
+
start = max(0, min(start, len(dataset)))
|
| 131 |
+
end = max(start, min(end, len(dataset)))
|
| 132 |
+
|
| 133 |
+
return dataset[start:end]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def validate_annotator_config(config):
|
| 137 |
+
"""
|
| 138 |
+
Valide une configuration d'annotateurs.
|
| 139 |
+
|
| 140 |
+
Args:
|
| 141 |
+
config: Dict de configuration
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
(bool, list): (is_valid, list_of_errors)
|
| 145 |
+
"""
|
| 146 |
+
errors = []
|
| 147 |
+
|
| 148 |
+
if not isinstance(config, dict):
|
| 149 |
+
errors.append("La configuration doit être un dictionnaire")
|
| 150 |
+
return False, errors
|
| 151 |
+
|
| 152 |
+
for ann_id, ann_config in config.items():
|
| 153 |
+
if not isinstance(ann_config, dict):
|
| 154 |
+
errors.append(f"{ann_id}: La configuration doit être un dict")
|
| 155 |
+
continue
|
| 156 |
+
|
| 157 |
+
# Vérifier les champs requis
|
| 158 |
+
required_fields = ["name", "start_idx", "end_idx"]
|
| 159 |
+
for field in required_fields:
|
| 160 |
+
if field not in ann_config:
|
| 161 |
+
errors.append(f"{ann_id}: Champ '{field}' manquant")
|
| 162 |
+
|
| 163 |
+
# Vérifier les types
|
| 164 |
+
if "start_idx" in ann_config and not isinstance(ann_config["start_idx"], int):
|
| 165 |
+
errors.append(f"{ann_id}: start_idx doit être un entier")
|
| 166 |
+
|
| 167 |
+
if "end_idx" in ann_config and not isinstance(ann_config["end_idx"], int):
|
| 168 |
+
errors.append(f"{ann_id}: end_idx doit être un entier")
|
| 169 |
+
|
| 170 |
+
# Vérifier la logique
|
| 171 |
+
if "start_idx" in ann_config and "end_idx" in ann_config:
|
| 172 |
+
if ann_config["start_idx"] >= ann_config["end_idx"]:
|
| 173 |
+
errors.append(f"{ann_id}: start_idx doit être < end_idx")
|
| 174 |
+
|
| 175 |
+
return len(errors) == 0, errors
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def create_annotator_config_from_chunks(num_annotators, total_items):
|
| 179 |
+
"""
|
| 180 |
+
Crée automatiquement une configuration pour diviser un dataset en chunks.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
num_annotators: Nombre d'annotateurs
|
| 184 |
+
total_items: Nombre total d'items dans le dataset
|
| 185 |
+
|
| 186 |
+
Returns:
|
| 187 |
+
dict: Configuration générée
|
| 188 |
+
"""
|
| 189 |
+
items_per_annotator = total_items // num_annotators
|
| 190 |
+
config = {}
|
| 191 |
+
|
| 192 |
+
for i in range(num_annotators):
|
| 193 |
+
ann_id = f"annotator_{i+1}"
|
| 194 |
+
start_idx = i * items_per_annotator
|
| 195 |
+
|
| 196 |
+
# Le dernier annotateur prend tout ce qui reste
|
| 197 |
+
if i == num_annotators - 1:
|
| 198 |
+
end_idx = total_items
|
| 199 |
+
else:
|
| 200 |
+
end_idx = (i + 1) * items_per_annotator
|
| 201 |
+
|
| 202 |
+
config[ann_id] = {
|
| 203 |
+
"name": f"Annotateur {i+1}",
|
| 204 |
+
"start_idx": start_idx,
|
| 205 |
+
"end_idx": end_idx,
|
| 206 |
+
"description": f"Items {start_idx} à {end_idx-1}"
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
return config
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def show_annotator_config_editor():
|
| 213 |
+
"""
|
| 214 |
+
Affiche un éditeur de configuration des annotateurs (admin).
|
| 215 |
+
"""
|
| 216 |
+
st.markdown("## ⚙️ Configuration des Annotateurs")
|
| 217 |
+
|
| 218 |
+
config = load_annotator_config()
|
| 219 |
+
|
| 220 |
+
st.info("Cette section permet de configurer les portions du dataset pour chaque annotateur")
|
| 221 |
+
|
| 222 |
+
# Afficher la config actuelle
|
| 223 |
+
st.json(config)
|
| 224 |
+
|
| 225 |
+
# Option pour créer une nouvelle config
|
| 226 |
+
with st.expander("Créer une nouvelle configuration"):
|
| 227 |
+
col1, col2 = st.columns(2)
|
| 228 |
+
|
| 229 |
+
with col1:
|
| 230 |
+
num_annotators = st.number_input(
|
| 231 |
+
"Nombre d'annotateurs",
|
| 232 |
+
min_value=1,
|
| 233 |
+
max_value=20,
|
| 234 |
+
value=3
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
with col2:
|
| 238 |
+
total_items = st.number_input(
|
| 239 |
+
"Nombre total d'items",
|
| 240 |
+
min_value=1,
|
| 241 |
+
value=300
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
if st.button("Générer la configuration", type="primary"):
|
| 245 |
+
new_config = create_annotator_config_from_chunks(num_annotators, total_items)
|
| 246 |
+
st.json(new_config)
|
| 247 |
+
|
| 248 |
+
if st.button("Sauvegarder cette configuration"):
|
| 249 |
+
if save_annotator_config(new_config):
|
| 250 |
+
st.success("✅ Configuration sauvegardée")
|
| 251 |
+
st.rerun()
|
| 252 |
+
|
| 253 |
+
# Éditeur manuel
|
| 254 |
+
with st.expander("Éditer manuellement (JSON)"):
|
| 255 |
+
st.markdown("**Format attendu:**")
|
| 256 |
+
st.code(json.dumps(DEFAULT_ANNOTATOR_CONFIG, indent=2), language="json")
|
| 257 |
+
|
| 258 |
+
config_text = st.text_area(
|
| 259 |
+
"Configuration JSON",
|
| 260 |
+
value=json.dumps(config, indent=2),
|
| 261 |
+
height=300
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
if st.button("Valider et sauvegarder"):
|
| 265 |
+
try:
|
| 266 |
+
new_config = json.loads(config_text)
|
| 267 |
+
is_valid, errors = validate_annotator_config(new_config)
|
| 268 |
+
|
| 269 |
+
if is_valid:
|
| 270 |
+
if save_annotator_config(new_config):
|
| 271 |
+
st.success("✅ Configuration validée et sauvegardée")
|
| 272 |
+
st.rerun()
|
| 273 |
+
else:
|
| 274 |
+
st.error("❌ Configuration invalide:")
|
| 275 |
+
for error in errors:
|
| 276 |
+
st.error(f" - {error}")
|
| 277 |
+
|
| 278 |
+
except json.JSONDecodeError as e:
|
| 279 |
+
st.error(f"❌ JSON invalide: {e}")
|
backend/auth.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module d'authentification pour l'application de scoring
|
| 3 |
+
Gère l'authentification par mot de passe et l'identification des annotateurs
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import streamlit as st
|
| 7 |
+
import os
|
| 8 |
+
import warnings
|
| 9 |
+
|
| 10 |
+
# Supprimer les warnings de secrets manquants
|
| 11 |
+
warnings.filterwarnings('ignore', message='.*secrets.*')
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def check_password():
|
| 15 |
+
"""
|
| 16 |
+
Vérifie que l'utilisateur a le bon mot de passe.
|
| 17 |
+
Returns True si authentifié, False sinon.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def password_entered():
|
| 21 |
+
"""Vérifie si le mot de passe est correct"""
|
| 22 |
+
# Support pour secrets (HF Spaces) ou variable d'environnement
|
| 23 |
+
correct_password = None
|
| 24 |
+
|
| 25 |
+
# Essayer d'abord st.secrets (HF Spaces) - sans message d'erreur
|
| 26 |
+
try:
|
| 27 |
+
correct_password = st.secrets.get("APP_PASSWORD")
|
| 28 |
+
except:
|
| 29 |
+
pass
|
| 30 |
+
|
| 31 |
+
# Fallback sur variable d'environnement
|
| 32 |
+
if not correct_password:
|
| 33 |
+
correct_password = os.getenv("APP_PASSWORD")
|
| 34 |
+
|
| 35 |
+
# Fallback sur mot de passe par défaut
|
| 36 |
+
if not correct_password:
|
| 37 |
+
correct_password = "annotator2025"
|
| 38 |
+
|
| 39 |
+
if st.session_state["password"] == correct_password:
|
| 40 |
+
st.session_state["password_correct"] = True
|
| 41 |
+
del st.session_state["password"] # Ne pas garder le mot de passe en clair
|
| 42 |
+
else:
|
| 43 |
+
st.session_state["password_correct"] = False
|
| 44 |
+
|
| 45 |
+
# Premier affichage ou non authentifié
|
| 46 |
+
if "password_correct" not in st.session_state:
|
| 47 |
+
st.markdown("## 🔐 Authentification Annotateur")
|
| 48 |
+
st.info("Entrez le mot de passe fourni par l'équipe de recherche")
|
| 49 |
+
st.text_input(
|
| 50 |
+
"Mot de passe",
|
| 51 |
+
type="password",
|
| 52 |
+
on_change=password_entered,
|
| 53 |
+
key="password",
|
| 54 |
+
label_visibility="collapsed"
|
| 55 |
+
)
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
elif not st.session_state["password_correct"]:
|
| 59 |
+
st.markdown("## 🔐 Authentification Annotateur")
|
| 60 |
+
st.text_input(
|
| 61 |
+
"Mot de passe",
|
| 62 |
+
type="password",
|
| 63 |
+
on_change=password_entered,
|
| 64 |
+
key="password",
|
| 65 |
+
label_visibility="collapsed"
|
| 66 |
+
)
|
| 67 |
+
st.error("❌ Mot de passe incorrect")
|
| 68 |
+
return False
|
| 69 |
+
|
| 70 |
+
else:
|
| 71 |
+
# Authentifié
|
| 72 |
+
return True
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_annotator_identity():
|
| 76 |
+
"""
|
| 77 |
+
Récupère ou demande l'identité de l'annotateur.
|
| 78 |
+
Returns: (annotator_id, annotator_name) ou None si non défini
|
| 79 |
+
"""
|
| 80 |
+
if "annotator_id" not in st.session_state:
|
| 81 |
+
st.session_state.annotator_id = None
|
| 82 |
+
st.session_state.annotator_name = None
|
| 83 |
+
|
| 84 |
+
return st.session_state.annotator_id, st.session_state.annotator_name
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def set_annotator_identity(annotator_id, annotator_name):
|
| 88 |
+
"""
|
| 89 |
+
Définit l'identité de l'annotateur dans la session
|
| 90 |
+
"""
|
| 91 |
+
st.session_state.annotator_id = annotator_id
|
| 92 |
+
st.session_state.annotator_name = annotator_name
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def show_annotator_selector(annotator_config):
|
| 96 |
+
"""
|
| 97 |
+
Affiche un sélecteur d'annotateur basé sur la configuration.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
annotator_config: Dict avec la configuration des annotateurs
|
| 101 |
+
Format: {
|
| 102 |
+
"annotator_1": {
|
| 103 |
+
"name": "Expert A",
|
| 104 |
+
"start_idx": 0,
|
| 105 |
+
"end_idx": 100
|
| 106 |
+
},
|
| 107 |
+
...
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
(annotator_id, config) ou (None, None) si pas sélectionné
|
| 112 |
+
"""
|
| 113 |
+
st.markdown("## 👤 Sélection de l'annotateur")
|
| 114 |
+
st.info("Sélectionnez votre identifiant pour charger votre portion du dataset")
|
| 115 |
+
|
| 116 |
+
# Créer la liste des options
|
| 117 |
+
options = ["-- Choisissez votre identifiant --"]
|
| 118 |
+
annotator_mapping = {}
|
| 119 |
+
|
| 120 |
+
for ann_id, ann_config in annotator_config.items():
|
| 121 |
+
display_name = f"{ann_config['name']} (Items {ann_config['start_idx']}-{ann_config['end_idx']})"
|
| 122 |
+
options.append(display_name)
|
| 123 |
+
annotator_mapping[display_name] = (ann_id, ann_config)
|
| 124 |
+
|
| 125 |
+
selected = st.selectbox(
|
| 126 |
+
"Annotateur",
|
| 127 |
+
options,
|
| 128 |
+
key="annotator_selector",
|
| 129 |
+
label_visibility="collapsed"
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
if selected == options[0]:
|
| 133 |
+
return None, None
|
| 134 |
+
|
| 135 |
+
annotator_id, config = annotator_mapping[selected]
|
| 136 |
+
|
| 137 |
+
# Afficher un résumé
|
| 138 |
+
st.success(f"""
|
| 139 |
+
✅ **{config['name']}**
|
| 140 |
+
- Items à annoter: {config['end_idx'] - config['start_idx']}
|
| 141 |
+
- Range: [{config['start_idx']}, {config['end_idx']}[
|
| 142 |
+
""")
|
| 143 |
+
|
| 144 |
+
if st.button("Confirmer et continuer", type="primary", use_container_width=True):
|
| 145 |
+
set_annotator_identity(annotator_id, config['name'])
|
| 146 |
+
return annotator_id, config
|
| 147 |
+
|
| 148 |
+
return None, None
|
backend/data_loader.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data loading utilities
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from datasets import load_dataset
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_dataset_from_jsonl(file):
|
| 10 |
+
"""Charge un dataset depuis un fichier JSONL"""
|
| 11 |
+
data = []
|
| 12 |
+
content = file.getvalue().decode('utf-8')
|
| 13 |
+
for line in content.split('\n'):
|
| 14 |
+
if line.strip():
|
| 15 |
+
data.append(json.loads(line))
|
| 16 |
+
return data
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_dataset_from_hf(dataset_name, split='train'):
|
| 20 |
+
"""Charge un dataset depuis HuggingFace"""
|
| 21 |
+
dataset = load_dataset(dataset_name, split=split)
|
| 22 |
+
return list(dataset)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def filter_items_with_positive(dataset):
|
| 26 |
+
"""Filtre les items qui ont un champ 'positive' non vide"""
|
| 27 |
+
items_with_positive = []
|
| 28 |
+
for idx, item in enumerate(dataset):
|
| 29 |
+
if 'positive' in item and item['positive']:
|
| 30 |
+
items_with_positive.append((idx, item))
|
| 31 |
+
return items_with_positive
|
backend/export.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Export utilities
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def prepare_export(items_with_positive, feedback_scores, feedback_comments=None):
|
| 9 |
+
"""
|
| 10 |
+
Prépare les données pour l'export avec identifiants uniques
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
items_with_positive: Liste de tuples (original_idx, item)
|
| 14 |
+
feedback_scores: Dict {idx: score}
|
| 15 |
+
feedback_comments: Dict {idx: comment} (optionnel)
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
Liste de dicts prêts pour l'export
|
| 19 |
+
"""
|
| 20 |
+
export_data = []
|
| 21 |
+
|
| 22 |
+
for original_idx, item in items_with_positive:
|
| 23 |
+
if original_idx in feedback_scores:
|
| 24 |
+
export_item = {
|
| 25 |
+
'code': item.get('anchor', item.get('code', '')),
|
| 26 |
+
'positive_feedback': item.get('positive', ''),
|
| 27 |
+
'score': feedback_scores[original_idx],
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
# Add unique identifiers for merging
|
| 31 |
+
# Priority: code_id > author_id > hash of content
|
| 32 |
+
if 'code_id' in item:
|
| 33 |
+
export_item['code_id'] = item['code_id']
|
| 34 |
+
if 'author_id' in item:
|
| 35 |
+
export_item['author_id'] = item['author_id']
|
| 36 |
+
|
| 37 |
+
# Add optional fields
|
| 38 |
+
if 'language' in item:
|
| 39 |
+
export_item['language'] = item['language']
|
| 40 |
+
|
| 41 |
+
# Add comment if exists
|
| 42 |
+
if feedback_comments and original_idx in feedback_comments:
|
| 43 |
+
comment = feedback_comments[original_idx]
|
| 44 |
+
if comment.strip():
|
| 45 |
+
export_item['comment'] = comment
|
| 46 |
+
|
| 47 |
+
# Add original index for reference (local to this file)
|
| 48 |
+
export_item['original_index'] = original_idx
|
| 49 |
+
|
| 50 |
+
export_data.append(export_item)
|
| 51 |
+
|
| 52 |
+
return export_data
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def export_to_jsonl(export_data):
|
| 56 |
+
"""
|
| 57 |
+
Convertit les données en format JSONL
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
export_data: Liste de dicts
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
String JSONL
|
| 64 |
+
"""
|
| 65 |
+
return "\n".join(json.dumps(item, ensure_ascii=False) for item in export_data)
|
backend/hf_storage.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module de sauvegarde des annotations sur HuggingFace Dataset
|
| 3 |
+
Permet une persistance permanente même si le Space redémarre
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import tempfile
|
| 11 |
+
import streamlit as st
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def get_hf_config():
|
| 15 |
+
"""
|
| 16 |
+
Récupère la configuration HuggingFace depuis les secrets ou variables d'env.
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
(hf_token, dataset_repo) ou (None, None) si non configuré
|
| 20 |
+
"""
|
| 21 |
+
hf_token = None
|
| 22 |
+
dataset_repo = None
|
| 23 |
+
|
| 24 |
+
# Essayer d'abord st.secrets (HF Spaces)
|
| 25 |
+
try:
|
| 26 |
+
hf_token = st.secrets.get("HF_TOKEN")
|
| 27 |
+
dataset_repo = st.secrets.get("HF_DATASET_REPO")
|
| 28 |
+
except (FileNotFoundError, KeyError):
|
| 29 |
+
pass
|
| 30 |
+
|
| 31 |
+
# Fallback sur variables d'environnement
|
| 32 |
+
if not hf_token:
|
| 33 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 34 |
+
if not dataset_repo:
|
| 35 |
+
dataset_repo = os.getenv("HF_DATASET_REPO")
|
| 36 |
+
|
| 37 |
+
return hf_token, dataset_repo
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def is_hf_storage_enabled():
|
| 41 |
+
"""
|
| 42 |
+
Vérifie si la sauvegarde HF est configurée et disponible.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
bool: True si configuré, False sinon
|
| 46 |
+
"""
|
| 47 |
+
hf_token, dataset_repo = get_hf_config()
|
| 48 |
+
return hf_token is not None and dataset_repo is not None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def save_annotations_to_hf(
|
| 52 |
+
annotator_id,
|
| 53 |
+
annotator_name,
|
| 54 |
+
feedback_scores,
|
| 55 |
+
feedback_comments,
|
| 56 |
+
dataset_metadata=None
|
| 57 |
+
):
|
| 58 |
+
"""
|
| 59 |
+
Sauvegarde les annotations sur un Dataset HuggingFace.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
annotator_id: ID de l'annotateur
|
| 63 |
+
annotator_name: Nom de l'annotateur
|
| 64 |
+
feedback_scores: Dict des scores {item_id: score}
|
| 65 |
+
feedback_comments: Dict des commentaires {item_id: comment}
|
| 66 |
+
dataset_metadata: Metadata du dataset annoté (optionnel)
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
bool: True si succès, False sinon
|
| 70 |
+
"""
|
| 71 |
+
try:
|
| 72 |
+
from huggingface_hub import HfApi
|
| 73 |
+
|
| 74 |
+
hf_token, dataset_repo = get_hf_config()
|
| 75 |
+
|
| 76 |
+
if not hf_token or not dataset_repo:
|
| 77 |
+
st.error("❌ Configuration HuggingFace manquante (HF_TOKEN ou HF_DATASET_REPO)")
|
| 78 |
+
return False
|
| 79 |
+
|
| 80 |
+
# Préparer les données d'annotation
|
| 81 |
+
timestamp = datetime.now().isoformat()
|
| 82 |
+
annotation_data = {
|
| 83 |
+
"annotator_id": annotator_id,
|
| 84 |
+
"annotator_name": annotator_name,
|
| 85 |
+
"timestamp": timestamp,
|
| 86 |
+
"scores": feedback_scores,
|
| 87 |
+
"comments": feedback_comments,
|
| 88 |
+
"num_scored": len(feedback_scores),
|
| 89 |
+
"metadata": dataset_metadata or {}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# Créer un fichier temporaire
|
| 93 |
+
filename = f"annotation_{annotator_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 94 |
+
|
| 95 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
|
| 96 |
+
json.dump(annotation_data, f, indent=2, ensure_ascii=False)
|
| 97 |
+
temp_path = f.name
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
# Upload vers le Dataset HF
|
| 101 |
+
api = HfApi(token=hf_token)
|
| 102 |
+
|
| 103 |
+
# Créer le repo s'il n'existe pas
|
| 104 |
+
try:
|
| 105 |
+
api.create_repo(
|
| 106 |
+
repo_id=dataset_repo,
|
| 107 |
+
repo_type="dataset",
|
| 108 |
+
private=True,
|
| 109 |
+
exist_ok=True
|
| 110 |
+
)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
# Le repo existe déjà, c'est OK
|
| 113 |
+
pass
|
| 114 |
+
|
| 115 |
+
# Upload le fichier
|
| 116 |
+
api.upload_file(
|
| 117 |
+
path_or_fileobj=temp_path,
|
| 118 |
+
path_in_repo=f"annotations/{filename}",
|
| 119 |
+
repo_id=dataset_repo,
|
| 120 |
+
repo_type="dataset",
|
| 121 |
+
commit_message=f"Add annotations from {annotator_name} ({annotator_id})"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
return True
|
| 125 |
+
|
| 126 |
+
finally:
|
| 127 |
+
# Nettoyer le fichier temporaire
|
| 128 |
+
try:
|
| 129 |
+
os.unlink(temp_path)
|
| 130 |
+
except:
|
| 131 |
+
pass
|
| 132 |
+
|
| 133 |
+
except ImportError:
|
| 134 |
+
st.error("❌ Package 'huggingface_hub' non installé")
|
| 135 |
+
return False
|
| 136 |
+
except Exception as e:
|
| 137 |
+
st.error(f"❌ Erreur lors de la sauvegarde HF: {str(e)}")
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def load_annotations_from_hf(annotator_id):
|
| 142 |
+
"""
|
| 143 |
+
Charge les annotations d'un annotateur depuis le Dataset HF.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
annotator_id: ID de l'annotateur
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
dict ou None: Données d'annotation si trouvées, None sinon
|
| 150 |
+
"""
|
| 151 |
+
try:
|
| 152 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 153 |
+
|
| 154 |
+
hf_token, dataset_repo = get_hf_config()
|
| 155 |
+
|
| 156 |
+
if not hf_token or not dataset_repo:
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
api = HfApi(token=hf_token)
|
| 160 |
+
|
| 161 |
+
# Lister les fichiers dans le repo
|
| 162 |
+
try:
|
| 163 |
+
files = api.list_repo_files(repo_id=dataset_repo, repo_type="dataset")
|
| 164 |
+
except Exception:
|
| 165 |
+
# Le repo n'existe pas encore
|
| 166 |
+
return None
|
| 167 |
+
|
| 168 |
+
# Chercher les fichiers de cet annotateur
|
| 169 |
+
annotation_files = [
|
| 170 |
+
f for f in files
|
| 171 |
+
if f.startswith(f"annotations/annotation_{annotator_id}_")
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
+
if not annotation_files:
|
| 175 |
+
return None
|
| 176 |
+
|
| 177 |
+
# Prendre le plus récent
|
| 178 |
+
latest_file = sorted(annotation_files)[-1]
|
| 179 |
+
|
| 180 |
+
# Télécharger et charger
|
| 181 |
+
local_path = hf_hub_download(
|
| 182 |
+
repo_id=dataset_repo,
|
| 183 |
+
filename=latest_file,
|
| 184 |
+
repo_type="dataset",
|
| 185 |
+
token=hf_token
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
with open(local_path, 'r', encoding='utf-8') as f:
|
| 189 |
+
return json.load(f)
|
| 190 |
+
|
| 191 |
+
except ImportError:
|
| 192 |
+
return None
|
| 193 |
+
except Exception as e:
|
| 194 |
+
st.warning(f"⚠️ Impossible de charger les annotations HF: {str(e)}")
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def get_all_annotator_files(annotator_id=None):
|
| 199 |
+
"""
|
| 200 |
+
Liste tous les fichiers d'annotations (optionnellement pour un annotateur).
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
annotator_id: ID de l'annotateur (None = tous)
|
| 204 |
+
|
| 205 |
+
Returns:
|
| 206 |
+
list: Liste des chemins de fichiers
|
| 207 |
+
"""
|
| 208 |
+
try:
|
| 209 |
+
from huggingface_hub import HfApi
|
| 210 |
+
|
| 211 |
+
hf_token, dataset_repo = get_hf_config()
|
| 212 |
+
|
| 213 |
+
if not hf_token or not dataset_repo:
|
| 214 |
+
return []
|
| 215 |
+
|
| 216 |
+
api = HfApi(token=hf_token)
|
| 217 |
+
|
| 218 |
+
try:
|
| 219 |
+
files = api.list_repo_files(repo_id=dataset_repo, repo_type="dataset")
|
| 220 |
+
except Exception:
|
| 221 |
+
return []
|
| 222 |
+
|
| 223 |
+
# Filtrer les fichiers d'annotation
|
| 224 |
+
annotation_files = [f for f in files if f.startswith("annotations/")]
|
| 225 |
+
|
| 226 |
+
if annotator_id:
|
| 227 |
+
annotation_files = [
|
| 228 |
+
f for f in annotation_files
|
| 229 |
+
if f.startswith(f"annotations/annotation_{annotator_id}_")
|
| 230 |
+
]
|
| 231 |
+
|
| 232 |
+
return sorted(annotation_files)
|
| 233 |
+
|
| 234 |
+
except ImportError:
|
| 235 |
+
return []
|
| 236 |
+
except Exception:
|
| 237 |
+
return []
|
backend/persistence.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Persistence utilities for datasets and scores
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class SessionManager:
|
| 11 |
+
"""Gère la persistance des sessions de scoring"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, data_dir="/app/data"):
|
| 14 |
+
self.data_dir = Path(data_dir)
|
| 15 |
+
self.data_dir.mkdir(exist_ok=True)
|
| 16 |
+
|
| 17 |
+
self.dataset_file = self.data_dir / "dataset.jsonl"
|
| 18 |
+
self.session_file = self.data_dir / "session.json"
|
| 19 |
+
|
| 20 |
+
def save_dataset(self, dataset, source_info=None):
|
| 21 |
+
"""
|
| 22 |
+
Sauvegarde le dataset en JSONL
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
dataset: Liste de dicts
|
| 26 |
+
source_info: Dict avec info sur la source (nom HF, fichier, etc.)
|
| 27 |
+
"""
|
| 28 |
+
# Sauvegarder le dataset
|
| 29 |
+
with open(self.dataset_file, 'w', encoding='utf-8') as f:
|
| 30 |
+
for item in dataset:
|
| 31 |
+
f.write(json.dumps(item, ensure_ascii=False) + '\n')
|
| 32 |
+
|
| 33 |
+
# Sauvegarder les métadonnées
|
| 34 |
+
metadata = {
|
| 35 |
+
'saved_at': datetime.now().isoformat(),
|
| 36 |
+
'total_items': len(dataset),
|
| 37 |
+
'source_info': source_info or {}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
metadata_file = self.data_dir / "dataset_metadata.json"
|
| 41 |
+
with open(metadata_file, 'w', encoding='utf-8') as f:
|
| 42 |
+
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
| 43 |
+
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
+
def load_dataset(self):
|
| 47 |
+
"""
|
| 48 |
+
Charge le dataset sauvegardé
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Tuple (dataset, metadata) ou (None, None) si pas de dataset sauvegardé
|
| 52 |
+
"""
|
| 53 |
+
if not self.dataset_file.exists():
|
| 54 |
+
return None, None
|
| 55 |
+
|
| 56 |
+
# Charger le dataset
|
| 57 |
+
dataset = []
|
| 58 |
+
with open(self.dataset_file, 'r', encoding='utf-8') as f:
|
| 59 |
+
for line in f:
|
| 60 |
+
if line.strip():
|
| 61 |
+
dataset.append(json.loads(line))
|
| 62 |
+
|
| 63 |
+
# Charger les métadonnées
|
| 64 |
+
metadata_file = self.data_dir / "dataset_metadata.json"
|
| 65 |
+
metadata = None
|
| 66 |
+
if metadata_file.exists():
|
| 67 |
+
with open(metadata_file, 'r', encoding='utf-8') as f:
|
| 68 |
+
metadata = json.load(f)
|
| 69 |
+
|
| 70 |
+
return dataset, metadata
|
| 71 |
+
|
| 72 |
+
def save_session(self, feedback_scores, feedback_comments, scoring_index):
|
| 73 |
+
"""
|
| 74 |
+
Sauvegarde l'état de la session (scores, commentaires, position)
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
feedback_scores: Dict {idx: score}
|
| 78 |
+
feedback_comments: Dict {idx: comment}
|
| 79 |
+
scoring_index: Index actuel
|
| 80 |
+
"""
|
| 81 |
+
session_data = {
|
| 82 |
+
'feedback_scores': {str(k): v for k, v in feedback_scores.items()},
|
| 83 |
+
'feedback_comments': {str(k): v for k, v in feedback_comments.items()},
|
| 84 |
+
'scoring_index': scoring_index,
|
| 85 |
+
'last_saved': datetime.now().isoformat(),
|
| 86 |
+
'total_scored': len(feedback_scores)
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
with open(self.session_file, 'w', encoding='utf-8') as f:
|
| 90 |
+
json.dump(session_data, f, indent=2, ensure_ascii=False)
|
| 91 |
+
|
| 92 |
+
return True
|
| 93 |
+
|
| 94 |
+
def load_session(self):
|
| 95 |
+
"""
|
| 96 |
+
Charge l'état de la session sauvegardée
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
Dict avec feedback_scores, feedback_comments, scoring_index
|
| 100 |
+
ou None si pas de session sauvegardée
|
| 101 |
+
"""
|
| 102 |
+
if not self.session_file.exists():
|
| 103 |
+
return None
|
| 104 |
+
|
| 105 |
+
with open(self.session_file, 'r', encoding='utf-8') as f:
|
| 106 |
+
session_data = json.load(f)
|
| 107 |
+
|
| 108 |
+
# Convertir les clés en int
|
| 109 |
+
session_data['feedback_scores'] = {
|
| 110 |
+
int(k): v for k, v in session_data['feedback_scores'].items()
|
| 111 |
+
}
|
| 112 |
+
session_data['feedback_comments'] = {
|
| 113 |
+
int(k): v for k, v in session_data['feedback_comments'].items()
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
return session_data
|
| 117 |
+
|
| 118 |
+
def has_saved_session(self):
|
| 119 |
+
"""Vérifie s'il existe une session sauvegardée"""
|
| 120 |
+
return self.dataset_file.exists() and self.session_file.exists()
|
| 121 |
+
|
| 122 |
+
def clear_session(self):
|
| 123 |
+
"""Supprime la session sauvegardée"""
|
| 124 |
+
if self.dataset_file.exists():
|
| 125 |
+
self.dataset_file.unlink()
|
| 126 |
+
if self.session_file.exists():
|
| 127 |
+
self.session_file.unlink()
|
| 128 |
+
|
| 129 |
+
metadata_file = self.data_dir / "dataset_metadata.json"
|
| 130 |
+
if metadata_file.exists():
|
| 131 |
+
metadata_file.unlink()
|
| 132 |
+
|
| 133 |
+
return True
|
| 134 |
+
|
| 135 |
+
def get_session_info(self):
|
| 136 |
+
"""
|
| 137 |
+
Récupère les informations sur la session sauvegardée
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
Dict avec les infos ou None
|
| 141 |
+
"""
|
| 142 |
+
if not self.has_saved_session():
|
| 143 |
+
return None
|
| 144 |
+
|
| 145 |
+
dataset, dataset_metadata = self.load_dataset()
|
| 146 |
+
session_data = self.load_session()
|
| 147 |
+
|
| 148 |
+
if dataset and session_data:
|
| 149 |
+
return {
|
| 150 |
+
'dataset_size': len(dataset),
|
| 151 |
+
'total_scored': session_data['total_scored'],
|
| 152 |
+
'last_saved': session_data['last_saved'],
|
| 153 |
+
'scoring_index': session_data['scoring_index'],
|
| 154 |
+
'source_info': dataset_metadata.get('source_info', {}) if dataset_metadata else {}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return None
|
backend/statistics.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Statistics computation utilities
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def compute_progress(scored_count, total_count):
|
| 7 |
+
"""Calcule le pourcentage de progression"""
|
| 8 |
+
if total_count == 0:
|
| 9 |
+
return 0.0
|
| 10 |
+
return scored_count / total_count
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def compute_score_distribution(feedback_scores):
|
| 14 |
+
"""
|
| 15 |
+
Calcule la distribution des scores
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
feedback_scores: Dict {idx: score}
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
Dict {score: count} pour scores 0-5
|
| 22 |
+
"""
|
| 23 |
+
scores_list = list(feedback_scores.values())
|
| 24 |
+
return {i: scores_list.count(i) for i in range(6)}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def compute_average_score(feedback_scores):
|
| 28 |
+
"""Calcule le score moyen"""
|
| 29 |
+
if not feedback_scores:
|
| 30 |
+
return 0.0
|
| 31 |
+
scores_list = list(feedback_scores.values())
|
| 32 |
+
return sum(scores_list) / len(scores_list)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def compute_most_common_score(feedback_scores):
|
| 36 |
+
"""
|
| 37 |
+
Trouve le score le plus fréquent
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
Tuple (score, count)
|
| 41 |
+
"""
|
| 42 |
+
distribution = compute_score_distribution(feedback_scores)
|
| 43 |
+
return max(distribution.items(), key=lambda x: x[1])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def find_unscored_indices(items_with_positive, feedback_scores):
|
| 47 |
+
"""
|
| 48 |
+
Trouve les indices des items non encore scorés
|
| 49 |
+
|
| 50 |
+
Returns:
|
| 51 |
+
Liste d'indices
|
| 52 |
+
"""
|
| 53 |
+
return [idx for idx, _ in items_with_positive if idx not in feedback_scores]
|
frontend/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Frontend module for scoring app
|
| 3 |
+
"""
|
frontend/components.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UI Components
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def render_navigation(current_index, total_items):
|
| 9 |
+
"""
|
| 10 |
+
Affiche les boutons de navigation
|
| 11 |
+
|
| 12 |
+
Returns:
|
| 13 |
+
Nouvelle valeur de l'index si changement, None sinon
|
| 14 |
+
"""
|
| 15 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 16 |
+
|
| 17 |
+
new_index = None
|
| 18 |
+
|
| 19 |
+
with col1:
|
| 20 |
+
if st.button("Précédent", use_container_width=True, disabled=current_index == 0):
|
| 21 |
+
new_index = current_index - 1
|
| 22 |
+
|
| 23 |
+
with col2:
|
| 24 |
+
st.markdown(f"<div class='center-text'>Exemple {current_index + 1} / {total_items}</div>", unsafe_allow_html=True)
|
| 25 |
+
|
| 26 |
+
with col3:
|
| 27 |
+
if st.button("Suivant", use_container_width=True, disabled=current_index >= total_items - 1):
|
| 28 |
+
new_index = current_index + 1
|
| 29 |
+
|
| 30 |
+
return new_index
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def render_code_block(code_text, language="python"):
|
| 34 |
+
"""Affiche un bloc de code avec syntax highlighting"""
|
| 35 |
+
st.markdown("### Code")
|
| 36 |
+
st.code(code_text, language=language)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def render_feedback_block(feedback_text):
|
| 40 |
+
"""Affiche le feedback dans un bloc stylisé adapté au thème"""
|
| 41 |
+
st.markdown("### Feedback")
|
| 42 |
+
|
| 43 |
+
# Utilise la classe CSS qui s'adapte automatiquement au thème
|
| 44 |
+
st.markdown(f"""
|
| 45 |
+
<div class="feedback-block">
|
| 46 |
+
{feedback_text}
|
| 47 |
+
</div>
|
| 48 |
+
""", unsafe_allow_html=True)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def render_score_slider(score_key, current_score=3):
|
| 52 |
+
"""
|
| 53 |
+
Affiche le slider de notation
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
Score sélectionné
|
| 57 |
+
"""
|
| 58 |
+
st.markdown("### Notez le feedback")
|
| 59 |
+
st.caption("À quel point ce feedback est-il utile et pertinent pour ce code ?")
|
| 60 |
+
|
| 61 |
+
score = st.slider(
|
| 62 |
+
"Score",
|
| 63 |
+
min_value=0,
|
| 64 |
+
max_value=5,
|
| 65 |
+
value=current_score,
|
| 66 |
+
step=1,
|
| 67 |
+
format="%d",
|
| 68 |
+
key=score_key,
|
| 69 |
+
help="0 = Pas utile du tout, 5 = Extrêmement utile"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Show score meaning
|
| 73 |
+
score_meanings = {
|
| 74 |
+
0: "Pas utile / Incorrect",
|
| 75 |
+
1: "Très peu utile",
|
| 76 |
+
2: "Peu utile",
|
| 77 |
+
3: "Moyennement utile",
|
| 78 |
+
4: "Utile",
|
| 79 |
+
5: "Extrêmement utile"
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
st.info(f"{score} - {score_meanings[score]}")
|
| 83 |
+
|
| 84 |
+
return score
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def render_comment_field(comment_key, current_comment=""):
|
| 88 |
+
"""
|
| 89 |
+
Affiche le champ de commentaire optionnel
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
Commentaire saisi
|
| 93 |
+
"""
|
| 94 |
+
with st.expander("Ajouter un commentaire (optionnel)"):
|
| 95 |
+
comment = st.text_area(
|
| 96 |
+
"Commentaire",
|
| 97 |
+
value=current_comment,
|
| 98 |
+
key=comment_key,
|
| 99 |
+
placeholder="Pourquoi ce score ? (optionnel)",
|
| 100 |
+
label_visibility="collapsed"
|
| 101 |
+
)
|
| 102 |
+
return comment
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def render_progress_metrics(total, scored, remaining, progress_pct):
|
| 106 |
+
"""Affiche les métriques de progression"""
|
| 107 |
+
st.markdown("### Progression")
|
| 108 |
+
|
| 109 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 110 |
+
|
| 111 |
+
with col1:
|
| 112 |
+
st.metric("Total", total)
|
| 113 |
+
|
| 114 |
+
with col2:
|
| 115 |
+
st.metric("Scorés", scored)
|
| 116 |
+
|
| 117 |
+
with col3:
|
| 118 |
+
st.metric("Restants", remaining)
|
| 119 |
+
|
| 120 |
+
with col4:
|
| 121 |
+
st.metric("Progression", f"{progress_pct:.0f}%")
|
| 122 |
+
|
| 123 |
+
st.progress(progress_pct / 100)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def render_statistics(avg_score, most_common_score, score_counts):
|
| 127 |
+
"""Affiche les statistiques détaillées"""
|
| 128 |
+
st.markdown("### Statistiques des Scores")
|
| 129 |
+
|
| 130 |
+
col1, col2 = st.columns(2)
|
| 131 |
+
|
| 132 |
+
with col1:
|
| 133 |
+
st.metric("Score Moyen", f"{avg_score:.2f}")
|
| 134 |
+
|
| 135 |
+
with col2:
|
| 136 |
+
st.metric("Score le plus fréquent", f"{most_common_score[0]} ({most_common_score[1]}x)")
|
| 137 |
+
|
| 138 |
+
st.bar_chart(score_counts, height=200)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def render_export_section(export_data):
|
| 142 |
+
"""Affiche la section d'export"""
|
| 143 |
+
st.markdown("### Export")
|
| 144 |
+
|
| 145 |
+
col1, col2 = st.columns(2)
|
| 146 |
+
|
| 147 |
+
with col1:
|
| 148 |
+
st.metric("Items à exporter", len(export_data))
|
| 149 |
+
|
| 150 |
+
return col2
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def render_quick_actions(items_with_positive, feedback_scores, current_index):
|
| 154 |
+
"""
|
| 155 |
+
Affiche les actions rapides
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
Tuple (reset_requested, jump_to_unscored, jump_to_index)
|
| 159 |
+
"""
|
| 160 |
+
st.markdown("### Actions Rapides")
|
| 161 |
+
|
| 162 |
+
col1, col2, col3 = st.columns(3)
|
| 163 |
+
|
| 164 |
+
reset_requested = False
|
| 165 |
+
jump_to_unscored = False
|
| 166 |
+
jump_to_index = None
|
| 167 |
+
|
| 168 |
+
with col1:
|
| 169 |
+
if st.button("Réinitialiser tous les scores", use_container_width=True):
|
| 170 |
+
reset_requested = True
|
| 171 |
+
|
| 172 |
+
with col2:
|
| 173 |
+
unscored_indices = [idx for idx, _ in items_with_positive if idx not in feedback_scores]
|
| 174 |
+
if unscored_indices and st.button("Aller au prochain non-scoré", use_container_width=True):
|
| 175 |
+
jump_to_unscored = True
|
| 176 |
+
|
| 177 |
+
with col3:
|
| 178 |
+
jump_to = st.number_input(
|
| 179 |
+
"Aller à l'exemple",
|
| 180 |
+
min_value=1,
|
| 181 |
+
max_value=len(items_with_positive),
|
| 182 |
+
value=current_index + 1,
|
| 183 |
+
step=1,
|
| 184 |
+
key="jump_to"
|
| 185 |
+
)
|
| 186 |
+
if st.button("Aller", use_container_width=True):
|
| 187 |
+
jump_to_index = jump_to - 1
|
| 188 |
+
|
| 189 |
+
return reset_requested, jump_to_unscored, jump_to_index
|
frontend/help_page.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Page d'aide détaillée
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def render_help_page():
|
| 9 |
+
"""Affiche la page d'aide complète"""
|
| 10 |
+
|
| 11 |
+
st.title("Guide d'utilisation")
|
| 12 |
+
st.markdown("Tout ce que vous devez savoir pour utiliser l'application de scoring de feedbacks")
|
| 13 |
+
|
| 14 |
+
st.markdown("---")
|
| 15 |
+
|
| 16 |
+
# Section 1: Introduction
|
| 17 |
+
st.markdown("## Objectif de l'application")
|
| 18 |
+
st.markdown("""
|
| 19 |
+
Cette application permet de **noter la qualité des feedbacks** générés pour des snippets de code.
|
| 20 |
+
Vous allez évaluer si les feedbacks sont utiles, pertinents et bien formulés.
|
| 21 |
+
|
| 22 |
+
**Échelle de notation : 0 à 5**
|
| 23 |
+
- **0** : Pas utile / Incorrect
|
| 24 |
+
- **1** : Très peu utile
|
| 25 |
+
- **2** : Peu utile
|
| 26 |
+
- **3** : Moyennement utile
|
| 27 |
+
- **4** : Utile
|
| 28 |
+
- **5** : Extrêmement utile
|
| 29 |
+
""")
|
| 30 |
+
|
| 31 |
+
st.markdown("---")
|
| 32 |
+
|
| 33 |
+
# Section 2: Charger un dataset
|
| 34 |
+
st.markdown("## Charger un dataset")
|
| 35 |
+
|
| 36 |
+
col1, col2 = st.columns(2)
|
| 37 |
+
|
| 38 |
+
with col1:
|
| 39 |
+
st.markdown("### Option 1 : Fichier local")
|
| 40 |
+
st.markdown("""
|
| 41 |
+
1. Cliquez sur **"Choisissez une option: Fichier local (.jsonl)"**
|
| 42 |
+
2. Cliquez sur **"Browse files"**
|
| 43 |
+
3. Sélectionnez votre fichier `.jsonl`
|
| 44 |
+
4. Cliquez sur **"Charger le fichier"**
|
| 45 |
+
|
| 46 |
+
**Format attendu du JSONL :**
|
| 47 |
+
```json
|
| 48 |
+
{"anchor": "code...", "positive": "feedback..."}
|
| 49 |
+
{"anchor": "code...", "positive": "feedback..."}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Chaque ligne doit contenir au minimum :
|
| 53 |
+
- `anchor` ou `code` : le code source
|
| 54 |
+
- `positive` : le feedback à évaluer
|
| 55 |
+
""")
|
| 56 |
+
|
| 57 |
+
with col2:
|
| 58 |
+
st.markdown("### Option 2 : HuggingFace Hub")
|
| 59 |
+
st.markdown("""
|
| 60 |
+
1. Cliquez sur **"Choisissez une option: HuggingFace Hub"**
|
| 61 |
+
2. Entrez le nom du dataset (ex: `username/dataset-name`)
|
| 62 |
+
3. Choisissez le split (généralement `train`)
|
| 63 |
+
4. Cliquez sur **" Charger depuis HF"**
|
| 64 |
+
|
| 65 |
+
**Exemples de datasets :**
|
| 66 |
+
- `username/my-feedback-dataset`
|
| 67 |
+
- `organization/code-feedbacks-v1`
|
| 68 |
+
|
| 69 |
+
Le dataset doit être public ou vous devez être authentifié
|
| 70 |
+
""")
|
| 71 |
+
|
| 72 |
+
st.markdown("---")
|
| 73 |
+
|
| 74 |
+
# Section 3: Navigation
|
| 75 |
+
st.markdown("## Navigation entre les exemples")
|
| 76 |
+
|
| 77 |
+
st.markdown("""
|
| 78 |
+
### Boutons de navigation
|
| 79 |
+
- **< Précédent** : Revenir à l'exemple précédent
|
| 80 |
+
- **Suivant >** : Passer à l'exemple suivant
|
| 81 |
+
- **Compteur central** : Affiche votre position (ex: "Exemple 5 / 100")
|
| 82 |
+
|
| 83 |
+
### Navigation rapide
|
| 84 |
+
- **Aller au prochain non-scoré** : Saute directement au prochain exemple sans score
|
| 85 |
+
- **Aller à l'exemple N** : Entrez un numéro et cliquez sur "Aller" pour y accéder directement
|
| 86 |
+
|
| 87 |
+
**Astuce** : Utilisez les boutons Précédent/Suivant pour naviguer rapidement
|
| 88 |
+
""")
|
| 89 |
+
|
| 90 |
+
st.markdown("---")
|
| 91 |
+
|
| 92 |
+
# Section 4: Scoring
|
| 93 |
+
st.markdown("## Noter un feedback")
|
| 94 |
+
|
| 95 |
+
st.markdown("""
|
| 96 |
+
### 1. Lisez le code
|
| 97 |
+
Le code source s'affiche en haut avec coloration syntaxique.
|
| 98 |
+
|
| 99 |
+
### 2. Lisez le feedback
|
| 100 |
+
Le feedback à évaluer s'affiche dans un encadré vert.
|
| 101 |
+
|
| 102 |
+
### 3. Attribuez un score
|
| 103 |
+
Déplacez le **slider de 0 à 5** selon votre évaluation :
|
| 104 |
+
|
| 105 |
+
| Score | Signification | Quand l'utiliser |
|
| 106 |
+
|-------|---------------|------------------|
|
| 107 |
+
| **0** | Pas utile / Incorrect | Le feedback est faux, inutile ou hors sujet |
|
| 108 |
+
| **1** | Très peu utile | Le feedback est vague ou très générique |
|
| 109 |
+
| **2** | Peu utile | Le feedback manque de précision ou de profondeur |
|
| 110 |
+
| **3** | Moyennement utile | Le feedback est correct mais sans détails |
|
| 111 |
+
| **4** | Utile | Le feedback est pertinent et bien expliqué |
|
| 112 |
+
| **5** | Extrêmement utile | Le feedback est excellent, précis et actionnable |
|
| 113 |
+
|
| 114 |
+
### 4. Ajoutez un commentaire (optionnel)
|
| 115 |
+
Cliquez sur **"Ajouter un commentaire"** pour justifier votre score.
|
| 116 |
+
Utile pour :
|
| 117 |
+
- Expliquer un score particulier
|
| 118 |
+
- Noter des détails spécifiques
|
| 119 |
+
- Documenter votre raisonnement
|
| 120 |
+
""")
|
| 121 |
+
|
| 122 |
+
st.markdown("---")
|
| 123 |
+
|
| 124 |
+
# Section 5: Progression
|
| 125 |
+
st.markdown("## Suivre votre progression")
|
| 126 |
+
|
| 127 |
+
st.markdown("""
|
| 128 |
+
### Métriques en temps réel
|
| 129 |
+
- **Total** : Nombre total d'exemples dans le dataset
|
| 130 |
+
- **Scorés** : Nombre d'exemples que vous avez notés
|
| 131 |
+
- **Restants** : Nombre d'exemples non scorés
|
| 132 |
+
- **Progression** : Pourcentage d'avancement
|
| 133 |
+
|
| 134 |
+
### Barre de progression
|
| 135 |
+
Une barre visuelle vous montre votre avancement global.
|
| 136 |
+
|
| 137 |
+
### Statistiques des scores
|
| 138 |
+
Quand vous avez noté au moins un exemple, vous verrez :
|
| 139 |
+
- **Score moyen** : Moyenne de tous vos scores
|
| 140 |
+
- **Score le plus fréquent** : Le score que vous utilisez le plus
|
| 141 |
+
- **Graphique de distribution** : Répartition de vos scores
|
| 142 |
+
|
| 143 |
+
Ces statistiques vous aident à voir si vous êtes cohérent dans vos notations
|
| 144 |
+
""")
|
| 145 |
+
|
| 146 |
+
st.markdown("---")
|
| 147 |
+
|
| 148 |
+
# Section 6: Export
|
| 149 |
+
st.markdown("## Exporter vos scores")
|
| 150 |
+
|
| 151 |
+
st.markdown("""
|
| 152 |
+
### Quand exporter ?
|
| 153 |
+
- À la fin de votre session de notation
|
| 154 |
+
- Régulièrement pour sauvegarder votre travail
|
| 155 |
+
- Quand vous voulez partager vos résultats
|
| 156 |
+
|
| 157 |
+
### Comment exporter ?
|
| 158 |
+
1. Scrollez jusqu'à la section **"Export"**
|
| 159 |
+
2. Vérifiez le nombre d'items à exporter
|
| 160 |
+
3. Cliquez sur **"Télécharger JSONL"**
|
| 161 |
+
4. Le fichier sera téléchargé : `feedback_scores.jsonl`
|
| 162 |
+
|
| 163 |
+
### Format du fichier exporté
|
| 164 |
+
```json
|
| 165 |
+
{
|
| 166 |
+
"code": "code source...",
|
| 167 |
+
"feedback": "le feedback évalué...",
|
| 168 |
+
"score": 4,
|
| 169 |
+
"comment": "votre commentaire...",
|
| 170 |
+
"original_index": 42
|
| 171 |
+
}
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### Aperçu de l'export
|
| 175 |
+
Cliquez sur **"Aperçu Export (5 premiers)"** pour voir à quoi ressemblera votre fichier.
|
| 176 |
+
|
| 177 |
+
**Important** : Les scores sont automatiquement enregistrés, mais téléchargez régulièrement votre fichier JSONL !
|
| 178 |
+
""")
|
| 179 |
+
|
| 180 |
+
st.markdown("---")
|
| 181 |
+
|
| 182 |
+
# Section 7: Actions avancées
|
| 183 |
+
st.markdown("## Actions avancées")
|
| 184 |
+
|
| 185 |
+
st.markdown("""
|
| 186 |
+
### Réinitialiser tous les scores
|
| 187 |
+
**Attention** : Cette action supprime TOUS vos scores !
|
| 188 |
+
1. Cliquez sur **"Réinitialiser tous les scores"**
|
| 189 |
+
2. Un avertissement apparaît
|
| 190 |
+
3. Cliquez à nouveau pour confirmer
|
| 191 |
+
|
| 192 |
+
Pensez à exporter avant de réinitialiser !
|
| 193 |
+
|
| 194 |
+
### Modifier un score existant
|
| 195 |
+
Vous pouvez revenir sur un exemple déjà noté et changer le score.
|
| 196 |
+
Le nouveau score remplacera automatiquement l'ancien.
|
| 197 |
+
""")
|
| 198 |
+
|
| 199 |
+
st.markdown("---")
|
| 200 |
+
|
| 201 |
+
# Section 8: Bonnes pratiques
|
| 202 |
+
st.markdown("## ✅ Bonnes pratiques")
|
| 203 |
+
|
| 204 |
+
col1, col2 = st.columns(2)
|
| 205 |
+
|
| 206 |
+
with col1:
|
| 207 |
+
st.markdown("""
|
| 208 |
+
### Pour une notation cohérente
|
| 209 |
+
- Lisez d'abord plusieurs exemples avant de commencer
|
| 210 |
+
- Définissez vos critères de notation
|
| 211 |
+
- Utilisez les commentaires pour les cas limites
|
| 212 |
+
- Relisez régulièrement vos premiers scores
|
| 213 |
+
- Vérifiez votre distribution de scores
|
| 214 |
+
""")
|
| 215 |
+
|
| 216 |
+
with col2:
|
| 217 |
+
st.markdown("""
|
| 218 |
+
### Pour éviter les erreurs
|
| 219 |
+
- Exportez régulièrement (toutes les 50 notations)
|
| 220 |
+
- N'utilisez pas "Réinitialiser" par erreur
|
| 221 |
+
- Lisez bien le code ET le feedback
|
| 222 |
+
- Faites des pauses régulières
|
| 223 |
+
- En cas de doute, mettez 3 et commentez
|
| 224 |
+
""")
|
| 225 |
+
|
| 226 |
+
st.markdown("---")
|
| 227 |
+
|
| 228 |
+
# Section 9: FAQ
|
| 229 |
+
st.markdown("## Questions fréquentes")
|
| 230 |
+
|
| 231 |
+
with st.expander("Mes scores sont-ils sauvegardés automatiquement ?"):
|
| 232 |
+
st.markdown("""
|
| 233 |
+
**Oui !** Chaque score est enregistré dès que vous bougez le slider.
|
| 234 |
+
|
| 235 |
+
Cependant, ils sont stockés temporairement dans l'application.
|
| 236 |
+
**Pensez à télécharger votre fichier JSONL** régulièrement pour ne pas perdre votre travail.
|
| 237 |
+
""")
|
| 238 |
+
|
| 239 |
+
with st.expander("Puis-je fermer l'application et revenir plus tard ?"):
|
| 240 |
+
st.markdown("""
|
| 241 |
+
**Attention** : Si vous fermez le navigateur ou redémarrez Docker, vos scores seront perdus.
|
| 242 |
+
|
| 243 |
+
**Solution** : Téléchargez votre fichier JSONL avant de fermer, et rechargez-le à votre retour.
|
| 244 |
+
""")
|
| 245 |
+
|
| 246 |
+
with st.expander("Comment gérer les feedbacks ambigus ?"):
|
| 247 |
+
st.markdown("""
|
| 248 |
+
Pour les feedbacks difficiles à évaluer :
|
| 249 |
+
1. Mettez un score **3** (moyennement utile)
|
| 250 |
+
2. Ajoutez un **commentaire** expliquant pourquoi c'est ambigu
|
| 251 |
+
3. Continuez et revenez-y plus tard si besoin
|
| 252 |
+
""")
|
| 253 |
+
|
| 254 |
+
with st.expander("Que faire si un feedback est partiellement correct ?"):
|
| 255 |
+
st.markdown("""
|
| 256 |
+
Utilisez l'échelle graduée :
|
| 257 |
+
- Partiellement incorrect → **1-2**
|
| 258 |
+
- Partiellement utile → **2-3**
|
| 259 |
+
- Majoritairement utile avec petits défauts → **3-4**
|
| 260 |
+
|
| 261 |
+
Le score **3** est parfait pour "c'est correct mais sans plus".
|
| 262 |
+
""")
|
| 263 |
+
|
| 264 |
+
with st.expander("Le dataset ne se charge pas ?"):
|
| 265 |
+
st.markdown("""
|
| 266 |
+
Vérifiez :
|
| 267 |
+
- Le fichier est bien au format `.jsonl`
|
| 268 |
+
- Chaque ligne contient `{"anchor": "...", "positive": "..."}`
|
| 269 |
+
- Le fichier n'est pas corrompu
|
| 270 |
+
- Pour HuggingFace : le dataset existe et est accessible
|
| 271 |
+
""")
|
| 272 |
+
|
| 273 |
+
with st.expander("Comment partager mes résultats ?"):
|
| 274 |
+
st.markdown("""
|
| 275 |
+
1. Téléchargez votre fichier JSONL
|
| 276 |
+
2. Envoyez-le par email, Slack, Drive, etc.
|
| 277 |
+
3. Le fichier contient tous vos scores et commentaires
|
| 278 |
+
|
| 279 |
+
Le fichier est lisible et peut être rechargé dans l'application.
|
| 280 |
+
""")
|
| 281 |
+
|
| 282 |
+
st.markdown("---")
|
| 283 |
+
|
| 284 |
+
# Section 10: Raccourcis
|
| 285 |
+
st.markdown("## ⌨Raccourcis et astuces")
|
| 286 |
+
|
| 287 |
+
st.markdown("""
|
| 288 |
+
### Navigation rapide
|
| 289 |
+
- Utilisez **Tab** pour naviguer entre les boutons
|
| 290 |
+
- **Espace** ou **Entrée** pour cliquer sur un bouton
|
| 291 |
+
- Cliquez directement sur le slider pour changer le score rapidement
|
| 292 |
+
|
| 293 |
+
### Workflow efficace
|
| 294 |
+
1. **Première passe** : Notez rapidement tous les exemples évidents
|
| 295 |
+
2. **Deuxième passe** : Revenez sur les cas ambigus avec "Aller au prochain non-scoré"
|
| 296 |
+
3. **Révision** : Vérifiez vos statistiques et ajustez si besoin
|
| 297 |
+
4. **Export** : Téléchargez votre travail
|
| 298 |
+
|
| 299 |
+
### Organisation
|
| 300 |
+
- Notez par sessions de 20-50 exemples
|
| 301 |
+
- Exportez à chaque fin de session
|
| 302 |
+
- Nommez vos exports : `scores_session1.jsonl`, `scores_session2.jsonl`
|
| 303 |
+
""")
|
| 304 |
+
|
| 305 |
+
st.markdown("---")
|
| 306 |
+
|
| 307 |
+
st.success("Vous êtes maintenant prêt à noter des feedbacks efficacement !")
|
| 308 |
+
st.caption("Pour toute question, consultez cette page d'aide ou contactez matis.codjia@epita.fr")
|
frontend/styles.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Custom CSS styling
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import streamlit as st
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def apply_custom_css():
|
| 9 |
+
"""Apply custom CSS styling to the Streamlit app with dark mode support"""
|
| 10 |
+
st.markdown("""
|
| 11 |
+
<style>
|
| 12 |
+
/* Clean typography */
|
| 13 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 14 |
+
|
| 15 |
+
* {
|
| 16 |
+
font-family: 'Inter', sans-serif;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* Main header - Light & Dark */
|
| 20 |
+
.main-header {
|
| 21 |
+
font-size: 2.8rem;
|
| 22 |
+
font-weight: 700;
|
| 23 |
+
margin-bottom: 0.5rem;
|
| 24 |
+
letter-spacing: -0.02em;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.subtitle {
|
| 28 |
+
font-size: 1rem;
|
| 29 |
+
font-weight: 400;
|
| 30 |
+
margin-bottom: 2rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Clean buttons - Adapt to theme */
|
| 34 |
+
.stButton > button {
|
| 35 |
+
border-radius: 8px;
|
| 36 |
+
font-weight: 500;
|
| 37 |
+
transition: all 0.2s;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.stButton > button:hover {
|
| 41 |
+
border-color: #3b82f6;
|
| 42 |
+
box-shadow: 0 4px 12px rgba(59,130,246,0.15);
|
| 43 |
+
transform: translateY(-1px);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.stButton > button:active {
|
| 47 |
+
transform: translateY(0);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* Primary button */
|
| 51 |
+
button[kind="primary"] {
|
| 52 |
+
background: #3b82f6 !important;
|
| 53 |
+
color: white !important;
|
| 54 |
+
border: none !important;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
button[kind="primary"]:hover {
|
| 58 |
+
background: #2563eb !important;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Metrics */
|
| 62 |
+
[data-testid="stMetricValue"] {
|
| 63 |
+
font-size: 2rem;
|
| 64 |
+
font-weight: 600;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* Section headers - Light mode */
|
| 68 |
+
[data-theme="light"] .section-header {
|
| 69 |
+
font-size: 1.25rem;
|
| 70 |
+
font-weight: 600;
|
| 71 |
+
color: #1a1a1a;
|
| 72 |
+
margin: 1.5rem 0 1rem 0;
|
| 73 |
+
padding-bottom: 0.5rem;
|
| 74 |
+
border-bottom: 2px solid #e5e7eb;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Section headers - Dark mode */
|
| 78 |
+
[data-theme="dark"] .section-header {
|
| 79 |
+
font-size: 1.25rem;
|
| 80 |
+
font-weight: 600;
|
| 81 |
+
color: #f3f4f6;
|
| 82 |
+
margin: 1.5rem 0 1rem 0;
|
| 83 |
+
padding-bottom: 0.5rem;
|
| 84 |
+
border-bottom: 2px solid #374151;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Sidebar - Light mode */
|
| 88 |
+
[data-theme="light"] [data-testid="stSidebar"] {
|
| 89 |
+
background: #fafafa;
|
| 90 |
+
border-right: 1px solid #e5e7eb;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* Sidebar - Dark mode */
|
| 94 |
+
[data-theme="dark"] [data-testid="stSidebar"] {
|
| 95 |
+
background: #1f2937;
|
| 96 |
+
border-right: 1px solid #374151;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* Info boxes */
|
| 100 |
+
.stAlert {
|
| 101 |
+
border-radius: 8px;
|
| 102 |
+
border-left: 3px solid;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Feedback block - Light mode */
|
| 106 |
+
.feedback-block {
|
| 107 |
+
padding: 1.25rem;
|
| 108 |
+
border-radius: 8px;
|
| 109 |
+
border-left: 3px solid #10b981;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
[data-theme="light"] .feedback-block {
|
| 113 |
+
background: linear-gradient(to bottom right, #ecfdf5, #f0fdf4);
|
| 114 |
+
border: 1px solid #d1fae5;
|
| 115 |
+
color: #064e3b;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Feedback block - Dark mode */
|
| 119 |
+
[data-theme="dark"] .feedback-block {
|
| 120 |
+
background: linear-gradient(to bottom right, #064e3b, #065f46);
|
| 121 |
+
border: 1px solid #065f46;
|
| 122 |
+
color: #d1fae5;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* Center text style */
|
| 126 |
+
.center-text {
|
| 127 |
+
text-align: center;
|
| 128 |
+
padding: 10px;
|
| 129 |
+
font-weight: bold;
|
| 130 |
+
}
|
| 131 |
+
</style>
|
| 132 |
+
""", unsafe_allow_html=True)
|
requirements.txt
CHANGED
|
@@ -1,3 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.29.0
|
| 2 |
+
datasets==2.16.1
|
| 3 |
+
pandas==2.1.4
|
| 4 |
+
numpy==1.26.2
|
| 5 |
+
huggingface_hub>=0.20.0
|